【问题标题】:How to Ignore Error If Batch Script Can't Write to Output File如果批处理脚本无法写入输出文件,如何忽略错误
【发布时间】:2026-02-24 14:20:02
【问题描述】:

我们有一个转换过程,它调用许多 Windows 批处理脚本文件并将输出和错误写入单个输出文件。有时,输出文件上的锁会导致脚本终止。因此,批处理文件终止并引发错误代码。如果输出文件被锁定,有没有办法忽略错误?例如,如果下例中的ScriptOutput文件被锁定,是否可以忽略重定向输出的错误并继续执行脚本?

@echo( >> D:\Conversion\log\ScriptOutput.log 2>>&1

D:\Conversion\log\ScriptOutput.log 2>>&1

@echo %date% %time% BEGIN OUTPUT FOR MoveTransDataToReporting.dtsx Script >> D:\Conversion\log\ScriptOutput.log 2>>&1
@echo(  >> D:\Conversion\log\ScriptOutput.log 2>>&1

"C:\Program Files (x86)\Microsoft SQL Server\120\DTS\Binn\DTExec.exe" /FILE "\"D:\Conversion\sql\MoveTransDataToReporting3Years.dtsx\"" /CHECKPOINTING OFF  /REPORTING EWCDI >> D:\Conversion\log\ScriptOutput.log 2>&1

@echo(  >> D:\Conversion\log\ScriptOutput.log 2>>&1
@echo %date% %time% END OUTPUT FOR MoveTransDataToReporting.dtsx Script >> D:\Conversion\log\ScriptOutput.log 2>>&1
@echo ************************************************************************************************************************************** >> D:\Conversion\log\ScriptOutput.log 2>>&1

【问题讨论】:

  • 将所有写入输出文件的echo命令放在(/)之间,并通过>> D:\Conversion\log\ScriptOutput.log 2>>&1重定向整个块;在整个事情周围加上另一个(/),包括重定向;附加类似&& (action upon success) || (action upon failure)...

标签: batch-file scripting io-redirection


【解决方案1】:

我建议使用以下批处理文件来解决这个问题:

@echo off
for /F "tokens=2,3 delims==.+-" %%I in ('%SystemRoot%\System32\wbem\wmic.exe OS GET LocalDateTime /VALUE') do set "LogFileName=%TEMP%\%~n0_%%I%%J.log"

>>"%LogFileName%" echo/
>>"%LogFileName%" echo %date% %time% BEGIN OUTPUT FOR MoveTransDataToReporting.dtsx Script
>>"%LogFileName%" echo/

"C:\Program Files (x86)\Microsoft SQL Server\120\DTS\Binn\DTExec.exe" /FILE "D:\Conversion\sql\MoveTransDataToReporting3Years.dtsx" /CHECKPOINTING OFF /REPORTING EWCDI >>"%LogFileName%" 2>&1

>>"%LogFileName%" echo/
>>"%LogFileName%" echo %date% %time% END OUTPUT FOR MoveTransDataToReporting.dtsx Script
>>"%LogFileName%" echo **************************************************************************************************************************************

set "RetryCount=0"

:MergeLogs
for %%I in ("%TEMP%\%~n0_????????????????????.log") do (
    ( type "%%I" >>"D:\Conversion\log\ScriptOutput.log" ) 2>nul && (
        del "%%I" & if "%%I" == "%LogFileName%" goto :EOF
    ) || goto NextRetry
)
goto :EOF

:NextRetry
set /A RetryCount+=1
if %RetryCount% == 30 goto :EOF
echo Retry %RetryCount% to merge the logs.
if exist %SystemRoot%\System32\timeout.exe (
    %SystemRoot%\System32\timeout.exe /T 1 /NOBREAK >nul
) else (
    %SystemRoot%\System32\ping.exe -n 2 127.0.0.1 >nul
)
goto MergeLogs

第一个 FOR 循环在由 FOR 在后台启动的单独命令进程中执行以下命令行:

C:\System32\wbem\wmic.exe OS GET LocalDateTime /VALUE

从该命令行输出数据的行中获取当前日期和时间以及当前微秒(实际上只有当前毫秒),格式为 yyyyMMddHHmmss.microsecond。详细解释见答案:Why does %date% produce a different result in batch file executed as scheduled task?yyyyMMddHHmms 格式的当前日期和时间分配给循环变量I,当前微秒分配给循环变量J

此日期/时间字符串与%TEMP% 表示的文件夹路径和%~n0 表示的批处理文件的名称连接在一起,并在带有扩展名.log 的完整限定日志文件名后面加上下划线。

因此,每次启动批处理文件MoveData.bat 时都会在文件夹中为临时文件创建一个日志文件,名称类似于MoveData_20180617105952343000.log,但批处理文件在同一毫秒内启动两次,但希望永远不会发生。

只要批处理文件中的 ECHO 命令行没有尾随空格/制表符,ECHO 命令行就会被重定向到临时日志文件中,而不会出现尾随空格。有问题的批处理代码中的 ECHO 命令行将重定向运算符 >> 之前的空格作为尾随空格写入日志文件。

在带有DTExec.exe 的命令行上,\" 都被删除了,因为我认为这里没有必要。同样在 Windows 命令行上,也无法定义包含在双引号中的参数字符串,其中包含 " 本身作为参数字符串的文字字符。 " 不能在 Windows 命令行上转义,既不能使用 \ 也不能使用 ^,Windows 命令处理器将其解释为转义字符。

使用当前日期/时间(包括临时文件文件夹中的当前毫秒)创建日志文件永远不会失败。

接下来,应将临时日志文件附加到记录所有转换的日志文件中。例如,如果在应用程序中打开日志文件以查看其内容,并且应用程序使用写锁打开该文件,这可能会失败。

因此,FOR 循环用于将与临时文件文件夹中指定通配符模式匹配的每个日志文件附加到摘要日志文件中。

日期/时间格式yyyyMMddHHmmssµs的主要优点是包含按字母顺序排列的这种格式的日期/时间的文件名同时也按日期排序,最旧的在前,最新的在后。默认情况下,临时文件的文件夹位于 NTFS 分区上。新技术文件系统返回与通配符模式匹配的文件名列表,通配符总是按字母顺序排序。因此 FOR 循环从最旧到最新处理此批处理文件的现有临时日志文件。

FOR循环获取此批处理文件创建的具有完整路径的临时日志文件的文件名,并执行命令行type "%%I" >>"D:\Conversion\log\ScriptOutput.log"将此日志文件的内容附加到摘要日志文件中.命令行包含在带有() 的命令块中,以验证是成功还是失败。

临时日志文件在成功将临时日志文件附加到摘要日志文件时被删除。并且如果成功附加的日志文件与之前刚刚创建的这个批处理文件是同一个日志文件,则退出批处理文件执行。否则,附加的日志文件是旧的,无法在先前执行批处理文件时附加到摘要日志文件,因此下一个临时日志文件由 FOR 处理。

但是,如果由于摘要日志文件当前因某种原因被写保护而将临时日志文件附加到摘要日志文件失败,则退出 FOR 循环并跳转到标签 NextRetry

重试计数器增加,如果批处理文件尚未尝试 30 次将临时日志文件附加到摘要日志文件,它会使用 TIMEOUT(Windows 7 或 Windows Server 2008 或任何更高的 Window 版本)或 PING(旧的 Windows 版本),然后再次尝试将最旧的现有临时日志文件附加到摘要日志文件中。

这种方式对日志文件管理的优点:

  • 即使摘要日志文件在批处理文件执行期间受到写保护,也会始终记录每个批处理文件执行。
  • 由于摘要日志文件在应用程序中打开几分钟以查看其内容而进行了 30 次尝试,但仍无法将其附加到摘要日志文件的临时创建的日志文件稍后由批处理文件附加执行哪个摘要日志文件不再写保护。
  • 由于日期/时间格式yyyyMMddHHmmssµs 和 NTFS 返回的文件名列表匹配按字母顺序排序的模式,这意味着在这种情况下从最旧到最新的文件,以前创建的临时日志文件以正确的时间顺序附加到摘要日志文件.
  • 批处理文件仅将旧执行的临时日志文件和当前执行创建的临时日志文件附加到摘要日志文件。因此,如果批处理文件在已经运行的情况下再次执行,因此已经创建了一个新的临时日志文件,该文件尚未包含所有信息,因为DTExec.exe 当前正在运行,执行将忽略这个新的临时日志文件首先启动的批处理文件的实例。

通过以下操作可能更容易理解日志文件管理:

  1. ECHO 放在以DTExec.exe 开头的命令行左侧并保存批处理文件。
  2. 打开命令提示符窗口并在命令提示符窗口中运行一次批处理文件以创建D:\Conversion\log\ScriptOutput.log
  3. 执行命令attrib +r D:\Conversion\log\ScriptOutput.log 对摘要日志文件进行写保护,以模拟应用程序使用写锁打开文件。
  4. 再次从命令提示符窗口运行批处理文件。可以看出,在等待超过 30 秒时追加临时日志文件失败 30 次。
  5. 打开第二个命令提示符窗口并准备执行命令
    attrib -r D:\Conversion\log\ScriptOutput.log,只需键入此命令行即可。
  6. 切换到第一个命令提示符窗口并第三次运行批处理文件。当重试在每次尝试之间暂停 1 秒时执行,切换到第二个命令提示符窗口并执行已经准备好的 ATTRIB 命令行以从摘要日志文件中删除只读属性。
  7. 切换回第一个命令提示符窗口,可以看到重试循环退出了。
  8. 从以DTExec.exe 开始的命令行中删除ECHO并保存批处理文件。

在文本查看器/编辑器中打开D:\Conversion\log\ScriptOutput.log,它包含所有三个批处理文件执行的信息。

只剩下一个问题。一个较旧的临时日志文件可能会两次附加到摘要日志文件中。在 30 秒内对当前写保护的摘要日志文件执行此批处理文件两次或更多次时会发生这种情况,并且当批处理文件的运行实例都在重试循环中并且 1 秒超时结束时,写锁定被删除几乎同时在批处理文件的至少两个运行实例中的机会。

我不知道批处理文件的执行频率以及DTExec.exe 完成任务需要多长时间。所以我不知道在实际使用批处理文件时是否会出现摘要日志文件中的信息重复。例如,可以将重试计数从30 减少到5,以避免这种最有可能非常罕见的情况发生。但是,发生这种情况时不会丢失任何信息;只有一次批处理文件的执行会在摘要日志文件中记录两次。


如果摘要日志文件当前被写保护,那么执行命令当然更容易,并且只需丢失所有信息。

@echo off
setlocal EnableDelayedExpansion
(
echo/
echo %date% %time% BEGIN OUTPUT FOR MoveTransDataToReporting.dtsx Script
echo/

"C:\Program Files (x86)\Microsoft SQL Server\120\DTS\Binn\DTExec.exe" /FILE "D:\Conversion\sql\MoveTransDataToReporting3Years.dtsx" /CHECKPOINTING OFF /REPORTING EWCDI 2>&1

echo/
echo !date! !time! END OUTPUT FOR MoveTransDataToReporting.dtsx Script
echo **************************************************************************************************************************************
) >>D:\Conversion\log\ScriptOutput.log
endlocal

Delayed expansion 被启用以引用动态环境变量 DATETIME 第二次使用延迟环境变量扩展 !date! !time!

Windows 命令处理器使用语法 %variable% 将每个环境变量引用替换为已在解析以 ( 开头并以匹配 ) 结尾的整个命令块时已引用的环境变量的当前值,然后再执行命令中的命令堵塞。因此,在此命令块中使用两次%date% %time% 而不使用延迟扩展将导致在日志文件中记录两次相同的日期和时间。

还可以通过使用更多命令块将其重定向到设备 NUL 来抑制当前被写保护的日志文件上的访问被拒绝错误消息输出。

@echo off
setlocal EnableDelayedExpansion
((
echo/
echo %date% %time% BEGIN OUTPUT FOR MoveTransDataToReporting.dtsx Script
echo/

"C:\Program Files (x86)\Microsoft SQL Server\120\DTS\Binn\DTExec.exe" /FILE "D:\Conversion\sql\MoveTransDataToReporting3Years.dtsx" /CHECKPOINTING OFF /REPORTING EWCDI 2>&1

echo/
echo !date! !time! END OUTPUT FOR MoveTransDataToReporting.dtsx Script
echo **************************************************************************************************************************************
) >>D:\Conversion\log\ScriptOutput.log ) 2>nul
endlocal

注意:带有DTExec.exe 的命令行最后只包含上面发布的两个简化批处理文件中的2>&1,因为编写的所有内容都是为了使用命令处理STDOUT块被重定向并附加(如果可能)到日志文件。


要了解所使用的命令及其工作原理,请打开命令提示符窗口,在其中执行以下命令,并仔细阅读每个命令显示的所有帮助页面。

  • del /?
  • echo /?
  • endlocal /?
  • for /?
  • goto /?
  • if /?
  • ping /?
  • set /?
  • setlocal /?
  • timeout /?
  • type /?
  • wmic /?
  • wmic os /?
  • wmic os get /?
  • wmic os get localdateime /?

另见

【讨论】:

    【解决方案2】:
    set "LogFile=ScriptOutput.log"
    
    :retry
    ((
        REM Do stuff here...
        verify>nul  &:: Sets errolevel to 0. This is important
    )>>"%LogFile%" 2>&1)2>nul || (set "LogFile=con" & echo Log file inaccessible, Bypassing LogFile... & goto :retry)
    

    LOG 块中的最后一条命令必须显式地将errorlevel 设置为0,因此只有在无法打开LogFile 时才会执行错误条件。 verify>nul 将完成这项工作。可以改用任何其他将 errorlevel 设置为 0 的命令。例如:(call )(注意call 和右括号之间的空格。)

    外部块将隐藏与打开 LogFile 相关的错误消息,因此如果需要,可以在错误条件块中显示自定义消息。

    它将通过将 LogFile 设置为 con 将输出显示到控制台来重试该过程。如果需要,可以将其重定向到nul 或其他文件。

    【讨论】: