【问题标题】:Batch goto loses errorlevel批处理 goto 丢失错误级别
【发布时间】:2016-01-21 23:17:23
【问题描述】:

考虑以下 bat,test.bat(PC01 已关闭):

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

如果我从命令 shell 运行该 bat:

> 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 行为?

【问题讨论】:

    标签: windows batch-file cmd


    【解决方案1】:

    在大多数情况下,|| 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 仍然非零,因为像 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 设置为相同的返回码,因此现在没有证据表明脚本中曾经有过错误。

    然后我们去兔子洞

    R.L.H. 在他的回答和对我的回答中,担心|| 有时会清除 ERRORLEVEL。他提供的证据似乎支持他的结论。但情况并没有那么简单,事实证明|| 是检测错误最可靠(但仍不完美)的方法。

    正如我之前所说,所有外部命令在退出时返回的返回码与 cmd.exe ERRORLEVEL 不同。

    ERRORLEVEL 是在 cmd.exe 会话本身中维护的状态,与返回码完全不同。

    这甚至记录在 EXIT 帮助中的 exitCode 定义中
    help exitexit /?

    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, etcFile 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/1012053https://groups.google.com/forum/#!msg/microsoft.public.win2000.cmdprompt.admin/XHeUq8oe2wk/LIEViGNmkK0J 了解更多信息。

    • 协会
    • 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 vs. 9009)。

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

    如果您严格依赖 if errorlevel 1 ...if %errorlevel% neq 0 ... 来检测错误,那么您将面临丢失 RD 和重定向(以及其他?)可能引发的错误的风险,并且您还会冒着错误地认为某些内部错误的风险命令失败,而实际上它可能是先前失败命令的保留。

    【讨论】:

    • 它不仅仅是调用批处理文件。我在单个批处理文件中得到了相同的错误级别 1 测试结果。 <failed command> || echo %errorlevel% 然后下一行echo %errorlevel%,第一行和第二行不一样。
    • 嘿,确实“呼叫”确实使 ||工作!当我从 c# 启动时,我被 cmd /c 卡住了.. 等等 .. 看起来我可以从 c# 使用 'call test.bat' ...
    • @Patrick - 我不知道。我认为你的问题实际上是一个非常好的问题,所以我只是投了赞成票。
    • DEL 仅在参数错误、无效路径 format 和不存在的驱动器时设置错误级别。 My SO answer of this point.
    • @MichaelBlake - DATE 和 TIME 在该代码中将 ERRORLEVEL 清除为 0,但您在输出中看不到,因为 DATE、TIME 和 ECHO 命令都在同一个代码块中.百分比扩展发生在解析时,整个块被一次解析。因此,您看到的 %ERRORLEVEL% 是输入块之前存在的值(在 DATE 或 TIME 执行之前)。如果在块关闭后 ECHO %ERRORLEVEL%,那么您将看到更新后的值 0。或者如果您启用延迟扩展并使用 ECHO !ERRORLEVEL!,那么您将在代码块中看到 0。
    【解决方案2】:

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

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

    现在你可以使用了

    test.bat || echo Failed
    

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

    【讨论】:

    • 谢谢杰布。是的,对我来说,解决方法是始终使用 cmd /c call xx.bat 调用 bat 文件。 “呼叫”是神奇的粘合剂。我的情况是bat的调用在我的控制之下,bat文件的内容不在。
    • +1,谢谢杰布。您的回答迫使我重新检查 || 如何在没有 CALL 的情况下响应批处理脚本。它让我重新发现没有 CALL 的 || 会响应脚本中最后执行的命令。这是我以前知道但忘记的事情。我已经编辑了我的答案,以正确解释使用 CALL 和不使用 CALL 之间的区别。
    【解决方案3】:

    真正的 Errorlevel 无法在双管道(失败时)运算符中存活。

    改用逻辑。例如,以下是您可以做到的一种方式:

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

    编辑:

    如果前面的命令不是单个命令,则必须执行更多操作来跟踪错误级别。例如,如果您调用一个脚本,那么在该脚本中您必须管理将错误级别传递回此代码。

    编辑 2:

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

    1) 无故障管道,使用 if 逻辑。

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

    'mybad.exe' 未被识别为内部或外部命令, 可运行的程序或批处理文件。

    错误级别现在是 9009

    错误级别现在是 9009

    2) 管道故障,回声

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

    'mybad.exe' 未被识别为内部或外部命令, 可运行的程序或批处理文件。

    错误级别现在为 1

    错误级别现在为 1

    3) 管道故障,转到

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

    'mybad.exe' 未被识别为内部或外部命令, 可运行的程序或批处理文件。

    错误级别现在为 1

    因此,如果您只关心“某事”失败,那么可以。代码 1 和其他代码一样好。但是,如果您需要知道“什么”失败了,那么您不能只是失败管道并让它消除实际结果。

    【讨论】:

    • ||在 test.bat 中的 mkdir 工作正常之后
    • 我对各种命令的测试并没有给出一致的好结果,所以从我的角度来看它是不可靠的。要进行测试,请将后续命令更改为 echo %errorlevel% 而不是您拥有的。这将向您展示每个实例会发生什么。我发布的内容总是有效的。但是,如果您正在调用批处理文件,请记住将错误级别传递到退出语句中,例如 exit %errorlevel%
    • 关于这个答案的一切都是错误的。 || 不会清除 ERRORLEVEL,GOTO :EOF 也不会。修改后的代码也没有解决问题。
    • @dbenham 但是如果 test.bat 只包含 mkdir 命令,没有 ||治疗,然后 99, 55 测试工作正常 - 所以看起来确实像 ||是“吃”吗?我不知道。这些都没有意义,显然^_^
    • @Patrick 和 R L H - 我已经更新了我的答案,解释了为什么我认为这个答案是错误的/误导性的。我告诉过你这很复杂!我理解你为什么得出你的结论,并且在没有其他信息的情况下你的推理是合理的。但是,不幸的是,批处理并不是那么简单。请不要个人反对我的投票。它严格地反映了答案的内容。这不是你努力的反映。实际上,您做了一些很好的测试,却导致您得出错误的结论。
    猜你喜欢
    • 1970-01-01
    • 2023-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-27
    • 2021-02-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多