如何在批处理脚本中比较文件的时间戳?

31
在DOS批处理文件中,实现某些任务的方法有些难以理解。幸运的是,有一个非常好的批处理脚本参考网站:Simon Sheppard's SS64。(该网站还提供了大量有关Bash的信息。)
其中一个困难在于根据目录是否为空来执行分支操作。显然的if exist "%dir%\*.*"无法起作用。但可以通过此条件执行技巧来完成:
( dir /b /a "%dir%" | findstr . ) > nul && (
  echo %dir% non-empty
) || (
  echo %dir% empty
)

另一个棘手的问题是根据文件内容进行分支。同样,可以这样做:

( fc /B "%file1%" "%file2%" | find "FC: no differences encountered" ) > nul && (
  echo "%file1%" and "%file2%" are the same
) || (
  echo "%file1%" and "%file2%" are different
)
那么,我的问题是:
是否有一种根据文件时间戳进行分支的方法?

我想要的东西是这样的:

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
)

感谢。


请查看下面@Sir-Steven-Patrick的答案。 - Kochise
8个回答

38
你可以使用一行批处理脚本来找到两个文件中更新的那个。只需按日期顺序列出文件,从旧到新排序,这意味着列表中最后一个文件就是更新的文件。因此,如果每次保存文件名,最后放入变量的名称将是最新的文件。
例如:
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.

不幸的是,这并不能处理日期戳相同的情况。因此,我们需要先检查文件是否具有相同的日期和时间戳:

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

16
请注意,这个解决方案仅在两个文件位于同一文件夹中时才有效。 - Pete Magsig
1
如果其中一个文件事先复制到另一个文件夹中,这个问题就可以解决。在我的情况下,这种方法很有效。 - Dave Knight
你好,能否请您解释一下 ('DIR /B /O:D %FILE1% %FILE2%') 这行代码的含义?我真的不太理解。 - user15964
2
@user15964 DIR %FILE1% %FILE2% 做一个目录列表搜索这两个文件,所以应该总是返回这两个文件。添加 /O:D 将它们按日期排序,而 /B 让输出变得“简洁”,因此只有文件名而不是文件大小等信息。 - David Webb
@PeteMagsig 如果文件不在同一个文件夹中,似乎工作正常,但您必须指定每个文件的完整路径,并且除实际名称外,所有内容都会被剥离,因此您可能需要进行一些比较操作以从结果中找出哪个是哪个。 - Perkins
@Perkins 我已经尝试了,为每个文件都提供了完整的路径名,但它并不起作用。最新的文件没有显示在最后一行。:-(我正在使用Windows 7。 - Mario Klebsch

13
我会使用xcopy来完成这个任务:
xcopy /L /D /Y PATH_TO_FILE1 PATH_TO_FILE2|findstr /B /C:"1 " && echo FILE1 is newer!

因为xcopy命令在这种情况下总是返回true,所以需要使用findstr命令来过滤其输出。
  • /L选项仅用于列出目录内容,不复制任何文件。
  • /D选项用于进行时间比较。提示:通过交换File1和File2的位置,您可以决定如何处理相同的文件。
  • /Y选项仅是为了避免“覆盖现有文件”的问题。

这就是全部内容,它适用于不同的路径。


1
绝对正确的答案。你可能想用“>nul”来进一步改进它,以避免findstr输出:| findstr /B /C:“1”>nul && - Kochise
1
总结:set /a f1newer=0 \ xcopy /L /D /Y PATH_TO_FILE1 PATH_TO_FILE2|findstr /B /C:"1 " >nul && set /a f1newer=1 >nul 提供可分支条件 f1newer - Sam Ginrich
@SamGinrich,xcopy 命令中的 "" 是用来做什么的? - undefined
zagZzig哦,被认为是换行符。 - undefined

11

这里有一个可以在任何两个文件上使用的解决方案。

首先获取文件时间(参见如何在Windows命令行上获取文件的最后修改日期?)。

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

然而,用户必须手动将日期和时间分解为其组件,因为Cmd.exe会将它们作为字符串进行比较,因此2>10且10:00AM>2:00PM。
首先比较年份,然后是月份,日子,上午/下午,小时,最后是分钟和秒钟(实际上很耗时间,但我暂时没有更好的方法)。请参见最终代码结尾。
然而,如果文件的时间相同,但秒数不同,则此解决方案将无法使用。
如果需要这种精度,请使用“forfiles”命令获取文件时间(请参见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    

请注意,“ForFiles”有一个限制,它不能处理带有空格的路径,因此如果您有带有空格的路径,则必须先更改到该目录,参见forfiles - spaces in folder path比较代码
: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

1
今天我发现了一个问题。这会认为12:00 PM比1:00 PM更新,因为12大于1。我将“if %originalHourPart% LSS %secondHourPart% goto older”更改为“if %originalHourPart% LSS %secondHourPart% goto check12”,并添加了以下内容 - :check12 如果%secondHourPart% EQU 12,则转到newer 转到older - Evan Zimmerman
2
不要重新发明日期时间比较,为什么不将一个文件复制到其他文件夹并使用原始答案?这样更快,更干净。只需注意在复制时保留文件修改时间。 - ciuly
我刚刚在命令行上尝试了一下forfiles。它可以接受带引号的文件名,并且您可以指定带有空格的路径。要指定不同目录中的文件,必须使用“/p”命令开关。我相信你几乎已经有一个完整的工作解决方案了 :) - Merlyn Morgan-Graham
也许一个更加通用的解决方案是首先创建一个函数,它可以通过创建您所提到的扩展日期/时间字段的整数来返回文件的绝对时间。然后,您可以以任何想要的方式使用那个整数值。 - ThermoX

8

说真的,你应该开始学习其他东西了。这不是开玩笑。DOS(cmd.exe)严重缺乏日期操作能力和许多其他方面的不足。以下是除DOS批处理外本地提供的更好的替代方案: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 

在命令行上运行它

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

当然,在较新版本的Windows中,您可能想尝试使用PowerShell...

谢谢。你说得对,DOS已经过时了。对于个人脚本编写,我使用Cygwin中的Bash。当编写小型实用程序供团队使用时,我需要使用所有人都能使用的语言。Python在这里非常普及。(它还具有跨平台优势。)但是,Python是一种“脚本语言”,而不是“shell语言”,因此生成子进程需要更多的工作。你提出的VBScript是一个好建议;也许我应该开始学习它。我故意避免使用“Powershells”,因为微软似乎无法决定支持哪一个。 - Rhubbarb
1
@rhubbarb - Powershell已经预装在Windows 7和Windows Server 2008 R2上,我认为Powershell也被Exchange 2007广泛使用。因此,现在它肯定得到了适当的支持。它值得一试,因为它非常棒。 - David Webb
1
如果你正在使用Python,那么你应该全程使用它。无论它是脚本语言还是shell语言,都没有关系。它只是一个名字。重要的是,Python(以及它的对手Perl)可以做几乎所有shell或(DOS)可以做的事情。移动/复制文件、解析、抓取网页...实际上,你只需要一个好用的工具就可以完成所有的管理/编程任务。至于上面提到的检查旧文件/新文件,你可以使用Python的os.getmtime()来检查修改时间。 - ghostdog74
ghostdog74:谢谢,我同意。我经常使用Python。但对于某些任务,例如启动外部程序(如Subversion)并捕获、重定向和管道化结果,这确实不太容易。DOS有管道。Bash有管道(|)和反引号(...)。Python可以做到这一点,但需要从众多的“popen”/“subprocess”模块中进行选择。在Python中可能有更好封装、更好抽象的方法,但我不知道。 - Rhubbarb
1
当遇到这种情况时,应该首先去查看 PyPI(或者在互联网上搜索),以寻找已经存在的可以完成所需功能的模块。我相信你可以找到一些适用于 Python 的 SVN 模块,这样就不需要调用外部进程了。如果真的没有选择,你仍然可以利用 Python 优秀的解析能力来使用 subprocess 模块。 :) - ghostdog74
1
你说得对,我会说DOS是有史以来最糟糕的脚本语言,它的语法围绕着“for files”...然而在Visual Studio中(至少在VS 2015之前),构建事件只能使用DOS语法。 - yoel halb

7

这里有一个更简单的解决方案。将日期部分从最重要到最不重要连接为一个字符串,然后我们可以对结果进行简单的字符串比较。

换句话说,比较YYYYMMDDAMPMHHMM值将在无需逐个比较日期段的情况下给出所需的结果。该值是通过将第二个FOR命令提取的日期字符串的各个部分连接而获得的。

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

由于评论者似乎正在考虑我的更新“针对作者” - 无论如何...我将我的更新添加为注释。请随意编辑。
  1. 更新: @ REM当前支持的日期部分分隔符为/:.和空格。如果您的日期格式包含其他分隔符,则可能需要向以下行添加更多分隔符。如果不这样做,则解析的日期将完全是任意结构,并且比较会出现问题。
for /f“tokens = 1,2,3,4,5,6 delims = /:.”%%a在(“%getfiledatestr%”)中执行(
- Roland Pihlakas
1
由于审稿人似乎认为我的更新是“针对作者的”,所以我将我的更新作为注释添加。请随意编辑。 2. 更新: set temp_value= @ REM cmd 无法比较大于1+31位的整数,因此我们将它们转换为字符串。当前实现生成的数字字符串比31位整数长得多。 call :expand set %1="%%%1%%%" @ goto :eof - Roland Pihlakas
1
关于编辑建议的元数据建议编辑 - Deduplicator
我添加了更多的更新来支持秒精度,并基于所有更新创建了一个新的答案,链接在这里:https://dev59.com/FnI-5IYBdhLWcg3wwbdM#29019902 - Roland Pihlakas
这个答案看起来有点问题,因为像20150012002900140054这样的数字远超批处理脚本的32位整数限制。 - Paul
显示剩余3条评论

7

如果您想按照Makefile的风格执行某些操作,只有在源文件比目标文件更新时才覆盖目标文件,那么我想出了这种丑陋但简单的方法。只有在您不关心目标文件的现有内容比源文件旧时,才能使用此选项。

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%"
)

这个命令的作用是,只有当源文件比目标文件新时,xcopy 才会覆盖目标文件。如果源文件不是最新的,则 %%r 会显示“0 个文件已复制”,条件命令不会执行,目标文件也不会被覆盖。而如果源文件是最新的,则 %%r 会显示“1 个文件已复制”,此时目标文件会暂时成为源文件的副本,然后执行构建命令,将其替换为实际应该是的目标文件的新版本。
我可能应该写一个 Perl 脚本。
(注意:你还可以在目标文件名末尾加上一个星号,让 xcopy 处理目标文件一开始不存在的情况;如果不这样做,xcopy 就无法确定目标是文件名还是文件夹名,并且没有标志可以默认答案为文件名。)

7
这是一个不受地域限制的好技巧。让我们再往前迈一步: xcopy /DYLR src "dst*" | findstr /BC:"0" >nul && @echo dst is newer 这个命令不会覆盖“dst”,只是检查它是否存在且更新。 是的,星号是必须的 - robert4
1
@robert4 - 那是迄今为止最好的答案! - rich p
1
感谢尾随星号技巧。这是我第一次看到它的提及。我之前看到的唯一解决方案是将“echo f”管道传输到xcopy,这很俗气。 - Richard A

0

根据 Wes answer,我创建了更新的脚本。更新内容太多,无法放在注释中。

  1. 我添加了对另一种日期格式的支持:包含日期点的格式。添加了有关如何启用更多日期格式的说明。
  2. 由于 cmd 无法比较大于 1+31 位整数的整数,因此 Wes 的答案实际上不起作用,所以我通过将数字字符串转换为带引号的数字字符串来修复了这个问题。
  3. 该脚本能够考虑缺失的文件作为最旧的文件。
  4. 添加了对秒精度的支持。

我的版本如下。

@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

0

免费的命令行程序WASFILE(https://www.horstmuc.de/wbat32.htm#wasfile)也可以为您执行文件日期/时间比较。

以下是网页上的快速程序说明:

*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*

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接