Reputation: 46536
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?
Upvotes: 2
Views: 923
Reputation: 2131
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.
Upvotes: 2
Reputation: 363
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.
Upvotes: 4
Reputation: 14325
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 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.
Upvotes: 2