31

In DOS batch files, the method of achieving certain things is somewhat obfuscated. Fortunately, there is a fantastic reference site for batch scripting: Simon Sheppard's SS64. (The same site also has plenty of information about Bash.)

One difficulty is branching execution based on whether a directory is empty. The obvious if exist "%dir%\*.*" doesn't work. But it can be done with this conditional execution trick:

( dir /b /a "%dir%" | findstr . ) > nul && (
  echo %dir% non-empty
) || (
  echo %dir% empty
)

Another awkward problem is branching according to file contents. Again that can be done like this:

( fc /B "%file1%" "%file2%" | find "FC: no differences encountered" ) > nul && (
  echo "%file1%" and "%file2%" are the same
) || (
  echo "%file1%" and "%file2%" are different
)

So, my question is:
Is there a way to do branch according to the time-stamps of files?

This is the sort of thing I want:

REM *** pseudo-code!
if "%file1%" is_newer_than "%file2%" (
  echo "%file1%" is newer
) else if "%file1%" is_older_than "%file2%" (
  echo "%file2%" is newer
) else (
  echo "%file1%" and "%file2%" are the same age
)

Thanks.

aschipfl
  • 33,626
  • 12
  • 54
  • 99
Rhubbarb
  • 4,248
  • 6
  • 36
  • 40

8 Answers8

38

You can find the newer of two files with one line of batch script. Just list the files in date order, oldest first, which means the last file listed must be the newer file. So if you save the file name each time, the last name put in your variable will be the newest file.

For, example:

SET FILE1=foo.txt
SET FILE2=bar.txt
FOR /F %%i IN ('DIR /B /O:D %FILE1% %FILE2%') DO SET NEWEST=%%i
ECHO %NEWEST% is (probably) newer.

This unfortunately doesn't cope with the date stamps being the same. So we just need to check if the files have the same date and time stamp first:

SET FILE1=foo.txt
SET FILE2=bar.txt

FOR %%i IN (%FILE1%) DO SET DATE1=%%~ti
FOR %%i IN (%FILE2%) DO SET DATE2=%%~ti
IF "%DATE1%"=="%DATE2%" ECHO Files have same age && GOTO END

FOR /F %%i IN ('DIR /B /O:D %FILE1% %FILE2%') DO SET NEWEST=%%i
ECHO Newer file is %NEWEST%

:END
David Webb
  • 190,537
  • 57
  • 313
  • 299
  • 15
    I'd like to add that this solution only works if the two files are in the same folder. – Pete Magsig Aug 29 '13 at 18:14
  • 1
    That can be resolved if one of the files is copied to the other's folder beforehand. In my case, that worked fine. – Dave Knight Sep 19 '14 at 10:16
  • Hi, could you please explain `('DIR /B /O:D %FILE1% %FILE2%')`? I really don't understand this line. – user15964 May 14 '15 at 12:01
  • 2
    @user15964 `DIR %FILE1% %FILE2%` does a directory listing searching for the two files and so should always return the two files. Adding `/O:D` puts them in date order and `/B` makes the output "bare" so it's only the file names and not the file size etc. – David Webb May 14 '15 at 12:40
  • @PeteMagsig Seems to work fine if the files aren't in the same folder, but you have to specify the full path to each, and all but the actual name gets stripped so you may have to do some comparison magic to figure out which is which from the results. – Perkins Feb 28 '19 at 00:45
  • @Perkins I have tried it, gave full path names for each file, but it does not work. The newest file is not shown on the last line. :-( I am using Windows 7. – Mario Klebsch Sep 25 '19 at 09:34
11

Dave Webb's soution while a great one will of course only work on files in the same directory.

Here is a solution that will work on any two files.

First get the file time (see How to get file's last modified date on Windows command line?).

for %%a in (MyFile1.txt) do set File1Date=%%~ta
for %%a in (MyFile2.txt) do set File2Date=%%~ta 

However one has then to manually break the date and time into it's components since Cmd.exe will compare them as a sting thus 2 > 10 and 10:00AM > 2:00PM.

Compare first the years, then the months, then the day, then AM/PM, then the hour, and then the minute and second, (actually time consuming, but I don't have on the minute a better idea), see the final code at the end.

However this solution will not work if the files are in the same minute but different by the second.

If you are to this level of precision then get the filetime by using the "forfiles" command (see https://superuser.com/questions/91287/windows-7-file-properties-date-modified-how-do-you-show-seconds).

for /F "tokens=*" %%a in ('forfiles /m MyFile1.txt /c "cmd /c echo @fdate @ftime"') 
    do set File1Date=%%a
for /F "tokens=*" %%a in ('forfiles /m MyFile2.txt /c "cmd /c echo @fdate @ftime"') 
    do set File2Date=%%a    

Note that "ForFiles" has a limitation that it can't take a path with spaces, so if you have a path with spaces you will have to change to that directory first, see forfiles - spaces in folder path

Comparison Code

:compareFileTime
set "originalFileTime=%1"
set "secondFileTime=%2"

for /F "tokens=1,2,3 delims= " %%a in (%originalFileTime%) do ( 
    set "originalDatePart=%%a"  
    set "originalTimePart=%%b"  
    set "originalAmPmPart=%%c"  
)
for /F "tokens=1,2,3 delims= " %%a in (%secondFileTime%) do (
    set "secondDatePart=%%a"
    set "secondTimePart=%%b"
    set "secondAmPmPart=%%c"
)
for /F "tokens=1,2,3 delims=/" %%a in ("%originalDatePart%") do (
        set "originalMonthPart=%%a"
        set "originalMonthDayPart=%%b"
        set "originalYearPart=%%c"

        rem We need to ensure that the year is in a 4 digit format and if not we add 2000 to it
        rem Cmd considers "50" > "100" but 50 < 100, so don't surround it with qoutes
    if %%c LSS 100 set "originalYearPart=20%%c
)
for /F "tokens=1,2,3 delims=/" %%a in ("%secondDatePart%") do (
    set "secondMonthPart=%%a"
    set "secondMonthDayPart=%%b"
    set "secondYearPart=%%c"    

    rem We need to ensure that the year is in a 4 digit format and if not we add 2000 to it
    rem Cmd considers "50" > "100" but 50 < 100, so don't surround it with quotes
        if %%c LSS 100 set "secondYearPart=20%%c    
)

if %originalYearPart% GTR %secondYearPart% goto newer
if %originalYearPart% LSS %secondYearPart% goto older

rem We reach here only if the year is identical
rem Cmd considers "2" > "10" but 2 < 10, so don't surround it with quotes or you will have to set the width explicitly
if %originalMonthPart% GTR %secondMonthPart% goto newer
if %originalMonthPart% LSS %secondMonthPart% goto older

if %originalMonthDayPart% GTR %secondMonthDayPart% goto newer
if %originalMonthDayPart% LSS %secondMonthDayPart% goto older

rem We reach here only if it is the same date
if %originalAmPmPart% GTR %secondAmPmPart% goto newer
if %originalAmPmPart% LSS %secondAmPmPart% goto older

rem we reach here only if i=t is the same date, and also the same AM/PM
for /F "tokens=1 delims=:" %%a in ("%originalTimePart%") do set "originalHourPart=%%a"

for /F "tokens=1 delims=:" %%a in ("%secondTimePart%") do set "secondHourPart=%%a"

rem Cmd considers "2" > "10" but 2 < 10, so don't surround it with qoutes or you will have to set the width explicitly
if %originalHourPart% GTR %secondHourPart% goto newer
if %originalHourPart% LSS %secondHourPart% goto older

rem The minutes and seconds can be compared directly
if %originalTimePart% GTR %secondTimePart% goto newer
if %originalTimePart% LSS %secondTimePart% goto older
if %originalTimePart% EQU %secondTimePart% goto same

goto older
exit /b

:newer
echo "newer"
exit /b

:older
echo "older"
exit /b

:same
echo "same"
exit /b
Community
  • 1
  • 1
yoel halb
  • 12,188
  • 3
  • 57
  • 52
  • 1
    There is one issue I found with this today. This will think 12:00 PM is newer than 1:00 PM since 12 is greater than 1. I changed "if %originalHourPart% LSS %secondHourPart% goto older" to "if %originalHourPart% LSS %secondHourPart% goto check12" and added this - :check12 if %secondHourPart% EQU 12 goto newer goto older – Evan Zimmerman Oct 02 '15 at 14:30
  • 2
    instead of reinventing date time comparison, why not copy one file to the other ones folder and use the original answer? much faster, and much cleaner. Just take note to preserve file modified time when copying. – ciuly Nov 26 '15 at 23:14
  • I just experimented with forfiles on the command line a bit. It can accept quoted filenames, and you can specify paths with spaces in them. To specify files in a different directory, you must use the `/p` command switch. I believe you almost have a complete working solution here :) – Merlyn Morgan-Graham Aug 27 '18 at 23:55
  • Maybe a more generic solution would be to first create a function that simply returns the absolute time of a file, by creating an integer of the expanded date/time fields you mentioned. Then you can get use that integer value any way you want. – ThermoX Apr 23 '20 at 15:01
10

I would use xcopy for this:

xcopy /L /D /Y PATH_TO_FILE1 PATH_TO_FILE2|findstr /B /C:"1 " && echo FILE1 is newer!

Because the xcopy always returns true in this case you need to filter its output with the findstr command.

  • The /L is for listing only, nothing is copied.
  • The /D is doing the time comparision. Hint: By swapping File1 and File2 you can decide how to handle identical files.
  • The /Y in only necessary to avoid an 'Overwrite existing' question.

That's all and it works with different paths.

  • 1
    The ABSOLUTELY RIGHT answer. You may want to improve it a but further though with '>nul' to avoid the findstr output : |findstr /B /C:"1 ">nul && – Kochise Mar 20 '23 at 16:39
  • conclude **set /a f1newer=0** \ **xcopy /L /D /Y PATH_TO_FILE1 PATH_TO_FILE2|findstr /B /C:"1 " >nul && set /a f1newer=1 >nul** delivers brancheable condition **f1newer** – Sam Ginrich Jun 03 '23 at 14:27
8

seriously, you should start to learn something else. Its not a joke. DOS(cmd.exe) seriously lacks date manipulation capabilities and many more deficiencies. Here's the next better alternative natively provided besides DOS batch, vbscript

Set objFS = CreateObject("Scripting.FileSystemObject")
Set objArgs = WScript.Arguments
strFile1 = objArgs(0)
strFile2 = objArgs(1)
Set objFile1 = objFS.GetFile(strFile1)
Set objFile2 = objFS.GetFile(strFile2)
If objFile1.DateLastModified < objFile2.DateLastModified Then
    WScript.Echo "File1: "&strFile1&" is older than "&strFile2
Else
    WScript.Echo "File1: "&strFile1&" is newer than "&strFile2
End If 

run it on command line

C:\test>dir
 Volume in drive C has no label.
 Volume Serial Number is 08AC-4F03

 Directory of C:\test

11/06/2009  07:40 PM    <DIR>          .
11/06/2009  07:40 PM    <DIR>          ..
11/06/2009  06:26 PM               135 file
11/02/2009  04:31 PM             4,516 m.txt

C:\test>cscript /nologo test.vbs file m.txt
File1: file is newer than m.txt

Of course, in newer versions of windows, you may want to try out Powershell...

ghostdog74
  • 327,991
  • 56
  • 259
  • 343
  • Thanks. You're right about DOS. For personal scripting, I use Bash in Cygwin. When writing small utilities to be used by the team, I need to use a language available to all. Python is quite widespread here. (It also has the cross-platform advantage.) But, Python is a "scripting language" rather than a "shell language", and so spawning sub-processes requires more work. Your suggestion of VBScript is a good one; perhaps I should start to get to know that. I have avoided the "Powershells" deliberately, because Microsoft don't seem to be able to make up their minds to support one properly. – Rhubbarb Nov 06 '09 at 12:11
  • 1
    @rhubbarb - Powershell comes installed on Windows 7 and Windows Server 2008 R2 and I think Powershell is also used pretty extensively by Exchange 2007. So it's definitely supported properly now. It's worth checking out as it's pretty awesome. – David Webb Nov 06 '09 at 12:18
  • 1
    If you are using Python, you should use it throughout. It doesn't matter whether its a scripting language or shell language. Its just a name. What's important is, Python (and its nemesis Perl) can do almost everything shell or (DOS) can. Moving/copying files, parsing, grabbing web page...practically, you just only need 1 good and easy to use tool to do all your admin/programming tasks. AS for the above checking for older/newer files, you can use Python's os.getmtime() to check modified time. – ghostdog74 Nov 06 '09 at 12:23
  • ghostdog74: Thanks, I agree. I use Python a great deal. But it genuinely is not so easy for certain tasks, such as launching external programs (such as Subversion), and capturing, redirecting and piping the results. DOS has pipes. Bash has pipes (|) and back-ticks (`...`). Python can do all this, but it involves choosing from the plethora of 'popen'/'subprocess' modules. There might be a better-encapsulated, better-abstracted method in Python, but I don't know it. – Rhubbarb Nov 06 '09 at 12:35
  • 1
    when such a situation occurs, always the first thing to go for, check pypi (or search internet) for modules that do the thing you want. I am sure you can find modules for svn that works with Python. that way you don't need to call external process. If really no choice you have to use subprocess module, you would still have the advantage of Python's excellent parsing capabilities.:) – ghostdog74 Nov 06 '09 at 12:48
  • 1
    You right about scripting, and I would say that DOS is the worst scripting language ever, a language that is centered around "for files"... however in visual studio (at least prior to VS 2015) a build event is only possible in DOS syntax. – yoel halb Jul 01 '15 at 06:54
7

Here's an easier solution. By concatenating the date parts as a single string from most significant to least significant, we can just do a simple string compare on the result.

In other words, comparing YYYYMMDDAMPMHHMM values will give the desired result without having to individually compare each segment of the date. This value is obtained by concatenating the various parts of the date string as extracted by the second FOR command.

call :getfiledatestr path\file1.txt file1time
call :getfiledatestr path\file2.txt file2time
if %file1time% equ %file2time% (
  echo path\file1.txt is the same age as path\file2.txt to within a minute
) else if %file1time% lss %file2time% (
  echo path\file1.txt is older than path\file2.txt
) else (
  echo path\file1.txt is newer than path\file2.txt
)
goto :eof

@REM usage:
@REM :getfiledatestr file-path envvar
@REM result returned in %envvar%
:getfiledatestr
for %%f in (%1) do set getfiledatestr=%%~tf
@REM for MM/DD/YYYY HH:MM AMPM use call :appendpadded %2 %%c %%b %%a %%f %%d %%e
@REM for DD/MM/YYYY HH:MM AMPM use call :appendpadded %2 %%c %%b %%a %%f %%d %%e
@REM for YYYY/DD/MM HH:MM AMPM use call :appendpadded %2 %%a %%b %%c %%f %%d %%e
set %2=
for /f "tokens=1,2,3,4,5,6 delims=/: " %%a in ("%getfiledatestr%") do (
    call :appendpadded %2 %%c %%b %%a %%f %%d %%e
)
@goto :eof

@REM Takes an env var as the first parameter
@REM and values to be appended as the remaining parameters,
@REM right-padding all values with leading 0's to 4 places
:appendpadded
set temp_value=000%2
call :expand set %1=%%%1%%%%temp_value:~-4%%
shift /2
if "%2" neq "" goto appendpadded
set temp_value=
@goto :eof

@REM forces all variables to expand fully
:expand
%*
@goto :eof
Wes
  • 420
  • 5
  • 6
  • Since the reviewers seem to be considering my updates "addressing the author" - whatever... I add my updates as comments. Please free to edit. 1. Update: `@ REM Currently supported date part delimiters are /:. and space. You may need to add more delimiters to the following line if your date format contains alternative delimiters too. If you do not do that then the parsed dates will be entirely arbitrarily structured and comparisons misbehave.` `for /f "tokens=1,2,3,4,5,6 delims=/:. " %%a in ("%getfiledatestr%") do (` – Roland Pihlakas Mar 02 '15 at 18:05
  • 1
    Since the reviewers seem to be considering my updates "addressing the author" - whatever... I add my updates as comments. Please free to edit. 2. Update: `set temp_value=` `@ REM cmd is not able to compare integers larger than 1+31 bits, so lets convert them to strings instead. The current implementation generates numeric strings much longer than 31 bit integers worth.` `call :expand set %1="%%%1%%%"` `@ goto :eof` – Roland Pihlakas Mar 02 '15 at 18:06
  • 1
    [meta](http://meta.stackoverflow.com/questions/287233/reject-this-edit-was-intended-to-address-the-author-of-the-post-and-makes-no-se) about the edit-suggestion: [Suggested edit](http://stackoverflow.com/review/suggested-edits/7210334). – Deduplicator Mar 02 '15 at 18:35
  • In order to enable comparing dates to nonexistant file dates (without this update script quits with error; with this update it considers the nonexistant file as older): Update 3: add `if "%getfiledatestr%" equ "" set %2=""` before `@ REM Currently supported date part delimiters`... `for /f "tokens=1,2,3,4,5,6 delims=/:. " %%a in ("%getfiledatestr%") do (` – Roland Pihlakas Mar 03 '15 at 21:50
  • I added more updates for supporting seconds precision and based on all updates created a new answer here: http://stackoverflow.com/a/29019902/193017 – Roland Pihlakas Mar 12 '15 at 20:50
  • This answer looks broken as numbers like 20150012002900140054 are way above the 32bit integer limitation of batch scripting. – Paul Dec 30 '15 at 14:16
  • Paul, see the first sentence, we are not using them as integers but strings. – Wes Dec 31 '15 at 18:11
  • probably a local solution, as date formats differ – Sam Ginrich Jun 03 '23 at 14:15
7

For one specific situation wherein you want to do something in the style of a Makefile, overwriting a destination file based on a source file only if the source file is newer, I came up with this hideous but simple method. This is only an option if you don't care in any way about the existing contents of a destination file that's older than the source file.

for /f "delims=" %%r in ('xcopy /D /Y /f /e "%inputdir%\%source_filename%" "%outputdir%\%dest_filename%"') do (
  IF "%%r" EQU "1 File(s) copied" %build_command% "%inputdir%\%source_filename%" "%outputdir%\%dest_filename%"
)

What this does is, xcopy only overwrites the destination file if the origin file is newer. If it's not newer, %%r is "0 File(s) copied", so the conditional command doesn't execute, and the destination file is never overwritten. If it is newer, %%r is "1 File(s) copied", so your destination file is briefly a copy of the source file, then the build command gets executed, replacing it with a new version of whatever the destination file is actually supposed to be.

I should probably have just written a perl script.

(Note: you can also have xcopy handle the situation where the destination file doesn't initially exist, by putting an asterisk at the end of the destination filename; if you don't do that then xcopy isn't sure whether the destination is a filename or folder name and there is no flag to default the answer to filename.)

Harmlezz
  • 7,972
  • 27
  • 35
Raven Black
  • 349
  • 3
  • 4
  • 7
    Nice trick because it's locale independent. Let's take it one step further: `xcopy /DYLR src "dst*" | findstr /BC:"0" >nul && @echo dst is newer` this doesn't overwrite `dst`, just checks whether it's existing and newer. Yes, [the asterisk is necessary](http://stackoverflow.com/a/26034267/1348138) – robert4 Sep 17 '15 at 15:37
  • 1
    @robert4 - that's the best answer yet! – rich p Jun 09 '16 at 14:51
  • 1
    Thanks for the trailing asterisk trick. This is the first time I've seen mention of it. The only previous solution I'd seen was piping 'echo f' into xcopy, which is tacky. – Richard A Apr 18 '18 at 06:53
0

Based on Wes answer I created updated script. The updates got too numerous to put into comments.

  1. I added support for one more date format: a format including dots in dates. Added instructions about how to enable more date formats.
  2. The Wes answer actually did not work since cmd is not able to compare integers larger than 1+31 bits, so I fixed that with converting the numeric strings to quoted numeric strings.
  3. The script is able to consider missing files as oldest files.
  4. Added support for second precision.

My version is below.

@REM usage:
@REM :getfiledatestr file-path envvar
@REM result returned in %envvar%
:getfiledatestr


for %%A in (%1) do (
    set getfilefolderstr=%%~dpA
    set getfilenamestr=%%~nxA
)

@REM Removing trailing backslash
if %getfilefolderstr:~-1%==\ set getfilefolderstr=%getfilefolderstr:~0,-1%

@REM clear it for case that the forfiles command fails
set getfiledatestr=

for /f "delims=" %%i in ('"forfiles /p ""%getfilefolderstr%"" /m ""%getfilenamestr%"" /c "cmd /c echo @fdate @ftime" "') do set getfiledatestr=%%i
@REM old code: for %%f in (%1) do set getfiledatestr=%%~tf

set %2=

@REM consider missing files as oldest files
if "%getfiledatestr%" equ "" set %2=""

@REM Currently supported date part delimiters are /:. and space. You may need to add more delimiters to the following line if your date format contains alternative delimiters too. If you do not do that then the parsed dates will be entirely arbitrarily structured and comparisons misbehave.
for /f "tokens=1,2,3,4,5,6 delims=/:. " %%a in ("%getfiledatestr%") do (

@REM for MM/DD/YYYY HH:MM:SS AMPM use call :appendpadded %2 %%c %%b %%a %%g %%d %%e %%f
@REM for DD/MM/YYYY HH:MM:SS AMPM use call :appendpadded %2 %%b %%c %%a %%g %%d %%e %%f
@REM for YYYY/DD/MM HH:MM:SS AMPM use call :appendpadded %2 %%a %%b %%c %%g %%d %%e %%f
    call :appendpadded %2 %%c %%b %%a %%g %%d %%e %%f
)

@goto :eof


@REM Takes an env var as the first parameter
@REM and values to be appended as the remaining parameters,
@REM right-padding all values with leading 0's to 4 places
:appendpadded
set temp_value=000%2
call :expand set %1=%%%1%%%%temp_value:~-4%%
shift /2
if "%2" neq "" goto appendpadded
set temp_value=

@REM cmd is not able to compare integers larger than 1+31 bits, so lets convert them to quoted numeric strings instead. The current implementation generates numeric strings much longer than 31 bit integers worth.
call :expand set %1="%%%1%%%"

@goto :eof


@REM forces all variables to expand fully
:expand
%*
@goto :eof
Roland Pihlakas
  • 4,246
  • 2
  • 43
  • 64
0

The freeware command line program WASFILE (https://www.horstmuc.de/wbat32.htm#wasfile) will also do a file date/time comparison for you.

Here's the quick program writeup from the web page:

*WasFile compares ..
.. time&date of two files (or directories),
.. the date of two files, time ignored
.. the date of a file with today-n (days)
.. time&date of a file with now-n (minutes)
Examples:
WasFile this.zip created before that.zip
WasFile this.zip modified after today-8
WasFile this.dat created before now-10
Using file stamps for created, modified (default) or accessed;
comparison: [not] before|after|sametime
Option to compare date only, ignore time: /DateLocal or /DateUTC*