批量转到失去错误级别

考虑下面的bat,test.bat(PC01closures):

mkdir \\PC01\\c$\Test || goto :eof 

如果我从命令行运行那个蝙蝠:

 > test.bat || echo 99 > if ERRORLEVEL 1 echo 55 

输出是55.没有99.有一个错误级别,但'||' 运营商没有看到它。

如果我使用cmd / c来运行该蝙蝠 –

 > cmd /c test.bat || echo 99 > if ERRORLEVEL 1 echo 55 

输出是空白的。 错误级别是0。

如果我删除“||” goto:eof',一切都按照人们预测的那样工作,即输出为99.55

有谁知道为什么这半熟的ERRORLEVEL行为发生?

在大多数情况下, || 是检测错误的最可靠的方法 。 但是你偶然发现了一个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仍然是非零的,因为像GOTOREM类的命令在成功之后不会清除任何之前的非零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 

没有CALLCMD /C返回最后执行的命令的返回码,即GOTO :EOFCMD /C还将ERRORLEVEL设置为相同的返回码,所以现在没有证据表明脚本中有错误。

然后我们走下兔子洞

RLH在他的回答和他对我的回答的评论中,担心|| 有时会清除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”的退出代码在错误中也为0 ,Windows中的文件重定向和%errorlevel%以获取更多信息。

我知道一个内部命令(可能有其他的)加上基本的失败的I / O操作,可以发出错误消息到标准错误,但他们不会|| 也不会设置一个非零的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命令返回的值,这是批处理终止时存在的最后一个ERRORLEVEL。

最后,我们有RLH报告的情况,一个无效的命令通常被报告为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结果是不同的 被使用或没有。

这样就可以处理一个命令失败的情况。 但是当内部命令成功时呢? 不幸的是,CMD.EXE比错误更不一致。 它根据命令而有所不同,也可能取决于它是从命令提示符,扩展名为.bat的批处理脚本还是从扩展名为.cmd的批处理脚本执行的。

我基于Windows 10行为下面的所有讨论。 我怀疑与使用cmd.exe的早期Windows版本有差异,但它是可能的。

无论上下文如何,以下命令始终将ERRORLEVEL清除为0:

  • CALL:如果CALLed命令没有设置,则清除ERRORLEVEL。
    例如: call echo OK
  • 光盘
  • CHDIR
  • 颜色
  • 复制
  • 日期
  • DEL:即使DEL失败,也总是清除ERRORLEVEL
  • DIR
  • ERASE:即使ERASE失败,也总是清除ERRORLEVEL
  • MD
  • MKDIR
  • MKLINK
  • 移动
  • PUSHD
  • REN
  • 改名
  • SETLOCAL
  • 时间
  • 类型
  • VER
  • 校验
  • VOL

下一组命令永远不会在成功时将ERRORLEVEL清除为0,而不管上下文,而是保留任何现有的非零值ERRORLEVEL:

  • 打破
  • CLS
  • 回声
  • ENDLOCAL
  • EXIT:显然EXIT /B 0清除了ERRORLEVEL,但是EXIT /B没有值保留了之前的ERRORLEVEL。
  • 对于
  • 如果
  • KEYS
  • 暂停
  • POPD
  • RD
  • REM
  • 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

  • ASSOC
  • DPATH
  • FTYPE
  • 路径
  • 提示

无论有没有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对9009)。

最后, || 除非使用CALL命令,否则不检测批处理脚本返回的非零ERRORLEVEL作为错误。

如果您严格依赖错误if errorlevel 1 ...或者if %errorlevel% neq 0 ...来检测错误,那么您将冒着丢失RD和重定向(及其他?)可能丢失的错误的风险,并且还运行错误地认为某些内部命令失败的风险实际上可能是从先前的命令失败的搁置。

若要在使用goto :eof时设置批处理文件以设置返回码的解决方案,您可以稍微更改脚本。

 mkdir \\\failure || goto :EXIT echo Only on Success exit /b :exit (call) 

现在你可以使用

 test.bat || echo Failed 

这里唯一的缺点是错误级别的丢失。
在这种情况下,返回码设置为falsefalse级别始终设置为1 ,由无效(call)
目前我找不到任何可能的方法来设置错误级别和返回代码到用户定义的值

真正的Errorlevel无法在双管道(失败)运营商中生存。

改用逻辑。 例如,下面是你可以做到的一个方法:

 mkdir \\PC01\c$\Test if "%errorlevel%" == "0" ( echo success ) else ( echo failure ) 

编辑:

如果之前的命令不是单个命令,那么还需要做更多的事情来跟踪错误级别。 例如,如果您调用脚本,那么在该脚本中,您必须管理将错误级别传递回此代码。

编辑2:

以下是ERRORLEVEL应为“9009”及其输出的测试场景。

1)没有故障管道,如果逻辑使用。

 setlocal enableDelayedExpansion mybad.exe if "!errorlevel!" == "0" ( echo success !errorlevel! ) else ( echo Errorlevel is now !errorlevel! echo Errorlevel is now !errorlevel! ) 

“mybad.exe”不被识别为内部或外部命令,可操作程序或批处理文件。

Errorlevel现在是9009

Errorlevel现在是9009

2)失败管道,回声

 setlocal enableDelayedExpansion mybad.exe || echo Errorlevel is now !errorlevel! echo Errorlevel is now !errorlevel! 

“mybad.exe”不被识别为内部或外部命令,可操作程序或批处理文件。

Errorlevel现在是1

Errorlevel现在是1

3)失败管道,转到

 setlocal enableDelayedExpansion mybad.exe || goto :fail exit :fail echo Errorlevel is now !errorlevel! 

“mybad.exe”不被识别为内部或外部命令,可操作程序或批处理文件。

Errorlevel现在是1

所以如果你所关心的是“某事”失败了,那就OK。 代码1与其他任何东西一样好。 但是,如果你需要知道“什么”失败了,那么你不能只管失败,让它消除实际的结果。