在大多数情况下,
||
是检测错误最可靠的方法。但是你遇到了一种罕见的情况,其中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和返回代码并不完全相同的证据之一。这肯定会让人感到困惑。
你可以使用CALL将test.bat视为一个单元命令,并获得所需的行为。
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,但当你请求一个有效命令的帮助信息时,如“help rem”,它返回1。
当执行外部命令时,“||”运算符不会清除ERRORLEVEL。如果进程退出代码是非零的,则检测到并触发“||”,而ERRORLEVEL仍然与退出代码匹配。话虽如此,出现在“&&”和/或“||”后面的命令可能会修改ERRORLEVEL,因此必须小心。
但是除了外部命令以外,开发人员还关注许多其他情况的成功/失败和返回码/ERRORLEVEL,包括:
- 内部命令的执行
- 重定向操作符“<”、“>”和“>>”
- 批处理脚本的执行
- 无效命令执行失败
不幸的是,CMD.EXE在处理这些情况的错误条件时不太一致。CMD.EXE有多个内部点必须检测错误,可能通过某种形式的内部返回代码来检测,而不一定是ERRORLEVEL,在这些点上,CMD.EXE有能力根据它所发现的情况来设置ERRORLEVEL。
对于下面的测试用例,请注意,带空格的“(call )”是清除每个测试前的ERRORLEVEL的晦涩语法。稍后,我还将使用不带空格的“(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
请参考
https://stackoverflow.com/a/32068760/1012053,了解更多与DEL错误相关的信息。
同样地,当标准输出已成功重定向到USB设备上的文件,但在ECHO尝试写入设备之前设备被移除时,ECHO将失败并出现错误消息到stderr,但是
||
不会触发,ERRORLEVEL也不会被设置为非零。请参见
http://www.dostips.com/forum/viewtopic.php?f=3&t=6881以获取更多信息。
接下来是批处理脚本执行的情况 - 这是OP问题的实际主题。如果没有
CALL
,
||
运算符将响应脚本内执行的最后一个命令。使用
CALL
,
||
运算符将响应
CALL
命令返回的值,这是批处理终止时存在的最终ERRORLEVEL。
最后,我们有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:如果被调用的命令没有设置ERRORLEVEL,则清除ERRORLEVEL。
示例:call echo OK
- CD
- CHDIR
- COLOR
- COPY
- DATE
- DEL:即使DEL失败也始终清除ERRORLEVEL
- DIR
- ERASE:即使ERASE失败也始终清除ERRORLEVEL
- MD
- MKDIR
- MKLINK
- MOVE
- PUSHD
- REN
- RENAME
- SETLOCAL
- TIME
- TYPE
- VER
- VERIFY
- VOL
下一组命令在成功时永远不会将ERRORLEVEL清除为0,而是保留任何现有的非零值ERRORLEVEL:
- BREAK:停止当前的FOR循环或者某个标签下的命令行。
- CLS:清除屏幕上的所有文本。
- ECHO:打印消息或开启/关闭命令回显。
- ENDLOCAL:结束对局部变量的使用,将控制权返回到调用程序。
- EXIT:退出当前批处理程序或命令提示符。
EXIT /B 0
可以清除ERRORLEVEL,但是没有值的EXIT /B
会保留之前的ERRORLEVEL。
- FOR:执行一个指定操作的命令序列,可在文件集合上进行迭代。
- GOTO:将控制转移到有标签的行。
- IF:根据条件执行一条或多条命令。
- KEYS:显示当前的键盘映射。
- PAUSE:暂停批处理程序并显示消息"Press any key to continue..."。
- POPD:更改当前目录为先前使用PUSHD命令推送的目录。
- RD:删除目录。
- REM:添加注释到批处理文件中。
- RMDIR:删除目录,该目录必须为空。
- SHIFT:向左移位命令行参数。
- START:启动单独的窗口以运行指定的程序或命令。
- TITLE:设置命令提示符窗口的标题。
此外,这些命令在从带有.cmd
扩展名的脚本中调用时,不会清除ERRORLEVEL,但是如果从带有.bat
扩展名的脚本或命令行中调用,则会将ERRORLEVEL清除为0。详情请参考https://dev59.com/43VC5IYBdhLWcg3w51ny#148991和https://groups.google.com/forum/#!msg/microsoft.public.win2000.cmdprompt.admin/XHeUq8oe2wk/LIEViGNmkK0J。
- ASSOC:显示或修改文件扩展名关联。
- DPATH:设置或显示搜索路径。
- FTYPE:显示或修改文件类型关联。
- PATH:设置或显示可执行文件搜索路径。
- PROMPT:更改命令提示符。
- SET:设置、显示或删除Windows环境变量。
无论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。但是有一些罕见的例外情况。
||
会正确设置ERRORLEVEL,否则RD或重定向失败时可能会错过该值。
||
在无效命令执行失败时设置不同的ERRORLEVEL,然后如果未使用||
,则会发生(1 vs. 9009)。
最后,
||
不会将批处理脚本返回的非零ERRORLEVEL作为错误检测,除非使用CALL命令。
如果您严格依赖
if errorlevel 1 ...
或
if %errorlevel% neq 0 ...
来检测错误,那么您可能会错过RD和重定向(等等)可能引发的错误,并且您也可能会错误地认为某些内部命令失败了,而实际上它可能是先前命令失败的遗留问题。
<failed command> || echo %errorlevel%
然后下一行echo %errorlevel%
,第一个与第二个不同。 - RLHfor /F
命令在解析的文件/字符串/命令输出为空的情况下将退出代码设置为非零值,但在这种情况下它不会设置ErrorLevel
。这再次证明了退出代码和ErrorLevel
是两个不同的概念。 - aschipfl