在大多数情况下,|| is the most reliable way to detect an error。但是您偶然发现了 ERRORLEVEL 有效但 || 无效的罕见情况之一。
问题源于您的错误是在批处理脚本中引发的,|| 响应最近执行的命令的返回码。您将 test.bat 视为单个“命令”,但实际上它是一系列命令。脚本中执行的最后一个命令是GOTO :EOF,并且成功执行。所以你的test.bat||echo 99 正在响应GOTO :EOF 的成功。
当您从脚本中删除 ||GOTO :EOF 时,您的 test.bat||echo99 会看到失败的 mkdir 的结果。但是如果你在 test.bat 的末尾添加一个REM 命令,那么test.bat||echo 99 会响应REM 的成功,并且错误会再次被屏蔽。
test.bat||echo 99 之后的 ERRORLEVEL 仍然非零,因为像 GOTO 和 REM 这样的命令在成功时不会清除任何先前的非零 ERRORLEVEL。这是 ERRORLEVEL 和返回码不完全相同的众多证据之一。这肯定会让人感到困惑。
您可以将 test.bat 视为一个单元命令,并通过使用 CALL 获得您想要的行为。
C:\test>call test.bat && echo OK || echo FAIL
FAIL
C:\test>if ERRORLEVEL 1 (echo FAIL2) else echo OK2
FAIL2
这是有效的,因为CALL 命令临时将控制权转移给被调用的脚本。当脚本终止时,控制权返回给CALL 命令,并返回当前的ERRORLEVEL。所以||echo 99 是响应 CALL 命令本身返回的错误,而不是脚本中的最后一个命令。
现在是CMD /C 问题。
CMD /C返回的返回码是最后执行的命令的返回码。
这行得通:
C:\test>cmd /c call test.bat && echo OK || echo FAIL
FAIL
C:\test>if ERRORLEVEL 1 (echo FAIL2) else echo OK2
FAIL2
因为CMD /C返回CALL语句返回的ERRORLEVEL
但这完全失败了:
C:\test>cmd /c test.bat && echo OK || echo FAIL
OK
C:\test>if ERRORLEVEL 1 (echo FAIL2) else echo OK2
OK2
如果没有CALL,CMD /C 返回最后执行命令的返回码,即GOTO :EOF。 CMD /C 还将 ERRORLEVEL 设置为相同的返回码,因此现在没有证据表明脚本中曾经有过错误。
然后我们去兔子洞
R.L.H. 在他的回答和对我的回答中,担心|| 有时会清除 ERRORLEVEL。他提供的证据似乎支持他的结论。但情况并没有那么简单,事实证明|| 是检测错误最可靠(但仍不完美)的方法。
正如我之前所说,所有外部命令在退出时返回的返回码与 cmd.exe ERRORLEVEL 不同。
ERRORLEVEL 是在 cmd.exe 会话本身中维护的状态,与返回码完全不同。
这甚至记录在 EXIT 帮助中的 exitCode 定义中
(help exit 或 exit /?)
EXIT [/B] [exitCode]
/B specifies to exit the current batch script instead of
CMD.EXE. If executed from outside a batch script, it
will quit CMD.EXE
exitCode specifies a numeric number. if /B is specified, sets
ERRORLEVEL that number. If quitting CMD.EXE, sets the process
exit code with that number.
当 CMD.EXE 运行外部命令时,它会检测可执行文件的返回码并设置 ERRORLEVEL 以匹配。请注意,0 表示成功,非零表示错误只是一个约定。一些外部命令可能不遵循该约定。例如,HELP 命令 (help.exe) 不遵循约定 - 如果您指定无效命令(如 help bogus),则返回 0,但如果您请求有效命令的帮助,则返回 1,如 help rem .
|| 运算符在执行外部命令时从不清除 ERRORLEVEL。进程退出代码被检测到并触发||,如果它不为零,并且ERRORLEVEL 仍将匹配退出代码。话虽如此,出现在&& 和/或|| 之后的命令可能会修改ERRORLEVEL,所以要小心。
但除了外部命令之外,还有很多其他情况,我们开发人员关心成功/失败和返回代码/ERRORLEVEL。
- 内部命令的执行
- 重定向运算符
<、> 和>>
- 批处理脚本的执行
- 无效命令执行失败
不幸的是,CMD.EXE 在处理这些情况下的错误条件方面完全不一致。 CMD.EXE 有多个内部点,它必须检测错误,大概是通过某种形式的内部返回代码,不一定是 ERRORLEVEL,并且在每个点 CMD.EXE 都可以根据它找到的内容设置 ERRORLEVEL .
对于我下面的测试用例,请注意(call ) 带有一个空格,是在每次测试之前将 ERRORLEVEL 清除为 0 的神秘语法。稍后,我还将使用(call),不带空格,将ERRORLEVEL设置为1
另请注意,在运行我的测试之前,我的命令会话中使用
cmd /v: on 启用了延迟扩展
绝大多数内部命令在失败时将 ERRORLEVEL 设置为非零值,并且错误条件也会触发 ||。在这些情况下,|| 永远不会清除或修改 ERRORLEVEL。
以下是几个例子:
C:\test>(call ) & set /a 1/0
Divide by zero error.
C:\test>echo !errorlevel!
1073750993
C:\test>(call ) & type notExists
The system cannot find the file specified.
C:\test>echo !errorlevel!
1
C:\test>(call ) & set /a 1/0 && echo OK || echo ERROR !errorlevel!
Divide by zero error.
ERROR 1073750993
C:\test>(call ) & type notExists.txt && echo OK || echo ERROR !errorlevel!
The system cannot find the file specified.
ERROR 1
那么至少有一个命令,RD,(可能更多),以及在出错时触发 || 的重定向运算符,但除非使用 ||,否则不要设置 ERRORLEVEL。
C:\test>(call ) & rd notExists
The system cannot find the file specified.
C:\test>echo !errorlevel!
0
C:\test>(call ) & echo x >\badPath\out.txt
The system cannot find the path specified.
C:\test>echo !errorlevel!
0
C:\test>(call ) & rd notExists && echo OK || echo ERROR !errorlevel!
The system cannot find the file specified.
ERROR 2
C:\test>(call ) & echo x >\badPath\out.txt && echo OK || echo ERROR !errorlevel!
The system cannot find the path specified.
ERROR 1
请参阅"rd" exits with errorlevel set to 0 on error when deletion fails, etc 和File redirection in Windows and %errorlevel% 了解更多信息。
我知道一个内部命令(可能还有其他命令)加上基本失败的 I/O 操作可以向 stderr 发出错误消息,但它们不会触发 || 也不会设置非零 ERRORLEVEL。
如果文件是只读的或不存在,DEL 命令可以打印错误,但它不会触发 || 或将 ERRORLEVEL 设置为非零
C:\test>(call ) & del readOnlyFile
C:\test\readOnlyFile
Access is denied.
C:\test>echo !errorlevel!
0
C:\test>(call ) & del readOnlyFile & echo OK || echo ERROR !errorlevel!
C:\test\readOnlyFile
Access is denied.
OK
有关 DEL 错误的更多信息,请参阅 https://stackoverflow.com/a/32068760/1012053。
类似地,当 stdout 已成功重定向到 USB 设备上的文件,但在 ECHO 等命令尝试写入设备之前删除了设备,则 ECHO 将失败并出现错误向 stderr 发送消息,但 || 不会触发,并且 ERRORLEVEL 未设置为非零。请参阅http://www.dostips.com/forum/viewtopic.php?f=3&t=6881 了解更多信息。
然后我们遇到执行批处理脚本的情况 - OP 问题的实际主题。如果没有CALL,|| 运算符将响应脚本中执行的最后一个命令。使用CALL,|| 运算符响应CALL 命令返回的值,这是批处理终止时存在的最终错误级别。
最后,R.L.H.报告,其中无效命令通常报告为 ERRORLEVEL 9009,但如果使用 ||,则报告为 ERRORLEVEL 1。
C:\test>(call ) & InvalidCommand
'InvalidCommand' is not recognized as an internal or external command,
operable program or batch file.
C:\test>echo !errorlevel!
9009
C:\test>(call ) & InvalidCommand && echo OK || echo ERROR !errorlevel!
'InvalidCommand' is not recognized as an internal or external command,
operable program or batch file.
ERROR 1
我无法证明这一点,但我怀疑在命令执行过程中很晚才检测到命令失败并将 ERRORLEVEL 设置为 9009。我猜|| 在设置 9009 之前拦截了错误检测,此时它将其设置为 1。所以我不认为|| 正在清除9009 错误,而是它是处理和设置错误的替代途径。
此行为的另一种机制是,无效命令始终可以将 ERRORLEVEL 设置为 9009,但返回代码 1 不同。|| 随后可以检测到返回代码 1 并将 ERRORLEVEL 设置为匹配,从而覆盖 9009。
无论如何,我不知道在任何其他情况下,非零 ERRORLEVEL 结果会因是否使用 || 而不同。
这样就可以处理命令失败时发生的情况。但是当内部命令成功时呢?不幸的是,CMD.EXE 的一致性甚至不如错误。它因命令而异,还可能取决于它是从命令提示符执行、从具有.bat 扩展名的批处理脚本执行,还是从具有.cmd 扩展名的批处理脚本执行。
下面的所有讨论都基于 Windows 10 的行为。我怀疑与使用 cmd.exe 的早期 Windows 版本存在差异,但这是可能的。
无论上下文如何,以下命令总是在成功时将 ERRORLEVEL 清除为 0:
- CALL :如果 CALLed 命令没有另外设置,则清除 ERRORLEVEL。
示例:call echo OK
- 光盘
- CHDIR
- 颜色
- 复制
- 日期
- DEL : 始终清除 ERRORLEVEL,即使 DEL 失败
- 目录
- ERASE :始终清除 ERRORLEVEL,即使 ERASE 失败
- 医学博士
- MKDIR
- MKLINK
- 移动
- 推送
- 任
- 重命名
- 本地设置
- 时间
- 类型
- 版本
- 验证
- 音量
无论上下文如何,下一组命令都不会在成功时将 ERRORLEVEL 清除为 0,而是保留任何现有的非零值 ERRORLEVEL:
- 休息
- CLS
- 回声
- ENDLOCAL
- EXIT :显然
EXIT /B 0 清除了ERRORLEVEL,但没有值的EXIT /B 保留了先前的ERRORLEVEL。
- 为
- 转到
- 如果
- 按键
- 暂停
- POPD
- 研发
- 快速移动
- RMDIR
- 移位
- 开始
- 标题
如果从命令行或在扩展名为 .bat 的脚本中发出这些命令,则这些命令在成功时不会清除 ERRORLEVEL,但如果从带有 .cmd 的脚本发出,则将 ERRORLEVEL 清除为 0扩大。请参阅https://stackoverflow.com/a/148991/1012053 和https://groups.google.com/forum/#!msg/microsoft.public.win2000.cmdprompt.admin/XHeUq8oe2wk/LIEViGNmkK0J 了解更多信息。
无论任何 ERRORLEVEL 值如何,&& 运算符都会检测先前的命令是否成功,如果成功则仅执行后续命令。 && 运算符忽略 ERRORLEVEL 的值,并且从不修改它。
这里有两个例子表明如果前面的命令成功,&& 总是会触发,即使 ERRORLEVEL 不为零。 CD 命令是该命令清除任何先前的 ERRORLEVEL 的示例,而 ECHO 命令是该命令不清除先前的 ERRORLEVEL 的示例。 注意,在发出成功的命令之前,我使用(call) 将 ERRORLEVEL 强制为 1。
C:\TEST>(call)
C:\TEST>echo !errorlevel!
1
C:\test>(call) & cd \test
C:\test>echo !errorlevel!
0
C:\test>(call) & cd \test && echo OK !errorlevel! || echo ERROR !errorlevel!
OK 0
C:\test>(call) & echo Successful command
Successful command
C:\test>echo !errorlevel!
1
C:\test>(call) & echo Successful command && echo OK !errorlevel! || echo ERROR !errorlevel!
Successful command
OK 1
在我所有的错误检测代码示例中,我都依赖于这样一个事实,即 ECHO 永远不会清除以前存在的非零 ERRORLEVEL。但下面的脚本是在&& 或|| 之后使用其他命令时可能发生的情况的示例。
@echo off
setlocal enableDelayedExpansion
(call)
echo ERRORLEVEL = !errorlevel!
(call) && echo OK !errorlevel! || echo ERROR !errorlevel!
(call) && (echo OK !errorlevel! & set "err=0") || (echo ERROR !errorlevel! & set "err=1" & echo ERROR !errorlevel!)
echo ERRORLEVEL = !errorlevel!
echo ERR = !ERR!
这是脚本具有.bat 扩展名时的输出:
C:\test>test.bat
ERRORLEVEL = 1
ERROR 1
ERROR 1
ERROR 1
ERRORLEVEL = 1
ERR = 1
这是脚本具有.cmd 扩展名时的输出:
C:\test>test.cmd
ERRORLEVEL = 1
ERROR 1
ERROR 1
ERROR 0
ERRORLEVEL = 0
ERR = 1
请记住,每个执行的命令都有可能改变 ERRORLEVEL。因此,尽管&& 和|| 是检测命令成功或失败的最可靠方法,但如果您关心 ERRORLEVEL 值,则必须注意在这些运算符之后使用什么命令。
现在是时候从这个臭兔子洞里爬出来呼吸新鲜空气了!
那么我们学到了什么?
没有一种完美的方法可以检测任意命令是成功还是失败。但是,&& 和 || 是检测成功和失败的最可靠方法。
一般来说&&和||都不会直接修改ERRORLEVEL。但也有一些罕见的例外。
-
|| 正确设置了 RD 或重定向失败时会丢失的 ERRORLEVEL
-
|| 在无效命令执行失败时设置不同的 ERRORLEVEL,然后在不使用 || 时会发生(1 vs. 9009)。
最后,|| 不会将批处理脚本返回的非零 ERRORLEVEL 检测为错误,除非使用了 CALL 命令。
如果您严格依赖 if errorlevel 1 ... 或 if %errorlevel% neq 0 ... 来检测错误,那么您将面临丢失 RD 和重定向(以及其他?)可能引发的错误的风险,并且您还会冒着错误地认为某些内部错误的风险命令失败,而实际上它可能是先前失败命令的保留。