英文:
Why doesn't Windows batch file `exit` work with `||`?
问题
The usual advice in batch/command scripting is to use exit /b
to exit while setting ERRORLEVEL
to indicate an error. However, this doesn't work well with CMD's ||
and &&
operators. If you run these at the CMD command line:
C:\>echo @exit /b 1 > foo.cmd
C:\>foo.cmd && echo success || echo fail
success
(Expected output is fail
).
According to ss64.com, this is because the "exit code" and ERRORLEVEL
aren't always the same, and exit
can return a success (0
) exit code when exiting with a non-0 ERRORLEVEL
, and exit code is what ||
/&&
are paying attention to.
Interestingly, adding call
makes it work as expected:
C:\>call foo.cmd && echo success || echo fail
fail
But requiring every use of ||
/&&
to also use call
puts the burden on the consumer, rather than the implementation, of the script.
One example of when this might be useful is if you have a build.cmd
script and a test.cmd
script, you might want to run build && test
.
https://ss64.com/nt/syntax-conditional.html and https://ss64.com/nt/exit.html don't mention this behavior, even though that site is usually very thorough about batch weirdness.
Why is CMD like this? What options exist to avoid this problem?
英文:
The usual advice in batch / command scripting is to use exit /b
to exit while setting ERRORLEVEL
to indicate an error. However this doesn't play well with CMD's ||
and &&
operators. If I run these at the CMD command line:
C:\>echo @exit /b 1 > foo.cmd
C:\>foo.cmd && echo success || echo fail
success
(Expected output is fail
).
According to ss64.com this is because "exit code" and ERRORLEVEL
aren't always the same, and that exit
can return a success (0
) exit code when exiting with a non-0 ERRORLEVEL, and exit code is what
||/
&&` are paying attention to.
Interestingly, adding call
makes it work as I'd expect:
C:\>call foo.cmd && echo success || echo fail
fail
But requiring every use of ||
/&&
to also use call
puts the burden on the consumer, rather than the implementation, of the script.
One example of when this might be useful is if I have a build.cmd
script and a test.cmd
script, I might want to run build && test
.
https://ss64.com/nt/syntax-conditional.html and https://ss64.com/nt/exit.html don't mention this behavior, even though that site is usually very thorough about batch weirdness.
Why is CMD like this? What options exist to avoid this problem?
答案1
得分: 3
Three examples using CMD:
C:\> Echo @exit /b 3 > throw_err.cmd
C:\> CMD /c throw_err.cmd && echo Success || echo Error: %errorlevel%
1
C:\> CMD /K throw_err.cmd && echo Success || echo Error: %errorlevel%
C:\> exit
3
C:\> Echo @exit 3 > throw_err.cmd
C:\> CMD /c throw_err.cmd && echo Success || echo Error: %errorlevel%
3
From PowerShell (with or without /b):
PS C:\> ./throw_err.cmd
PS C:\> $lastExitCode
3
Having to call a new CMD instance when you are already at the CMD prompt, just to get vaguely sensible error handling, does seem a little long-winded. I think I prefer calling via PowerShell.
英文:
Three examples using CMD:
C:\> Echo @exit /b 3 > throw_err.cmd
C:\> CMD /c throw_err.cmd && echo Success || echo Error: %errorlevel%
1
C:\> CMD /K throw_err.cmd && echo Success || echo Error: %errorlevel%
C:\> exit
3
C:\> Echo @exit 3 > throw_err.cmd
C:\> CMD /c throw_err.cmd && echo Success || echo Error: %errorlevel%
3
From PowerShell (with or without /b):
PS C:\> ./throw_err.cmd
PS C:\> $lastExitCode
3
Having to call a new CMD instance when you are already at the CMD prompt, just to get vaguely sensible error handling, does seem a little long winded, I think I prefer calling via PowerShell.
答案2
得分: 2
当你在CMD环境中从内部运行子脚本时,流程控制将保留在子脚本,除非你使用“call”返回到父环境。这更明显,如果你没有使用一行命令,而是使用了以下方式:
@echo off
foo.cmd
if "%errorlevel%"=="1" (
echo success
) else (
echo fail
)
这种情况下不返回任何内容(因为流程在子脚本中结束)。
然而,由于你将所有内容放在一行中,解释器会继续执行下一条命令,它会认为:“等等,我有一组其他命令要运行”,然后继续执行下一条命令(参见 https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts 获取更多详细信息)。
正如你发现的那样,使用“call”来调用子脚本可以使父环境的行为正确,因为“call”会在常规命令之前执行。
英文:
When you run a child script from inside of a CMD environment, flow control stays with the child script unless you use call
to return to the parent environment. This is more obvious if instead of using a one-liner, you had used
@echo off
foo.cmd
if "%errorlevel%"=="1" (
echo success
) else (
echo fail
)
which doesn't return anything at all (since flow ended with the child script).
However, since you have everything on one line like that, the interpreter just goes "hang on; I have a block of other commands to run" and moves on to the next command (see https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts for much more detail).
As you discovered, using call
to call child scripts makes the parent environment behave correctly, since call
s are executed before regular commands.
答案3
得分: 2
Jay, 你已正确识别到了通过使用魔法命令 cmd /c exit <code>
来实现这一目标。正如其他人指出的,处理执行上下文在传递退出代码时有一些微妙之处。此外,ERRORLEVEL 和退出代码机制是独立的,这常常让人感到意外。
正如一些人所指出的,使用 call
来调用你的CMD脚本可以解决一些问题。然而,这是一个痛苦的妥协,因为通常情况下,你不想关心一个命令是可执行文件还是脚本。而且你不想在命令行上的每一项操作前都加上 call ...
。
还有一点非常重要的事情在上面没有提到:你的CMD脚本的最后一行是神奇的。请注意,我指的是_实际的_最后一行,而不是最后执行的行(比如 exit /b <code>
)。脚本的最后一行的退出代码将代理到脚本本身的退出代码。当你评论说 cmd /c "exit 1"
起作用时,那是因为它也恰好是脚本的最后一行。例如,这个失败:
echo Hey!
甚至更好,这个 失败:
为什么呢?因为 goto :eof
成功。因此,为了正确设置退出代码,你必须将 cmd /c "exit 1"
放在脚本的最后一行。各种错误点将需要 goto
到这个位置。
更加灵活的方法是同时设置退出代码和ERRORLEVEL(尤其因为很多人认为这两者是等价的)。幸运的是,cmd /c exit <code>
正好做到了这一点。基于此,这是我使用的魔法代码:
...
call someCommand || (cmd /c exit <code> & goto :exit)
call someOtherCommand || (cmd /c exit <someOtherCode> & goto :exit)
...
:exit
@REM 用ERRORLEVEL退出脚本。
@REM 以下行必须是该脚本的最后一行。
cmd /c exit %ERRORLEVEL%
这里没有提到,但以防有人能因此避免一场大麻烦:绝对不要直接设置ERRORLEVEL环境变量。这样做会损害它的“神奇性”并用愚蠢的环境变量遮盖它。
英文:
Jay, you've correctly identified the magic cmd /c exit <code>
as the way to accomplish this. And as others have pointed out, there's some nuance to handling executing contexts when it comes to handing off exit codes. Also, the ERRORLEVEL and exit code mechanisms are independent, which often surprises people.
As some have pointed out, using call
to invoke your CMD script can sort some of these things out. That's a painful concession, however, because normally you want to not care about whether a command is an executable or a script. And you don't want to call ...
everything you ever do on the command line.
There's also a very important aspect to all this that I haven't seen mentioned above: the last line of your CMD script is magical. Note that I mean the actual last line - not the last line to be executed (such as exit /b <code>
). The exit code of the last line of your script is proxied to the exit code of the script itself. When you comment that cmd /c "exit 1"
works, it's because that also happens to be the last line of the script. For example, this fails:
cmd /c "exit 1"
echo Hey!
Even better, this fails:
cmd /c "exit 1" & goto :eof
Why? Because goto :eof
succeeds. So to properly set the exit code, you have to have cmd /c "exit 1"
as the last line of your script. Various error points will need to goto
this location.
A bit more flexible approach is to set the exit code an ERRORLEVEL simultaneously (particularly because many people think of these things as being equivalent). Luckily, the cmd /c exit <code>
does just that. Given this, here's the magic sauce I use:
...
call someCommand || (cmd /c exit <code> & goto :exit)
call someOtherCommand || (cmd /c exit <someOtherCode> & goto :exit)
...
:exit
@REM Exit the script with ERRORLEVEL exit code.
@REM The following line MUST be the last line of this script.
cmd /c exit %ERRORLEVEL%
And it wasn't brought up here, but just in case it saves someone a big headache: Never directly set the ERRORLEVEL environment variable. Doing to clobbers its "magicness" and and masks it with a dumb environment variable.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论