2

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?

Jay Bazuzi
  • 45,157
  • 15
  • 111
  • 168

3 Answers3

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.

SS64
  • 334
  • 5
  • 12
  • 26
  • 1
    PowerShell finally added &&/|| in PowerShell 7, yay! Reference my answer from 14 years ago: https://stackoverflow.com/a/564092/5314 – Jay Bazuzi Mar 20 '23 at 21:59
2

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 calls are executed before regular commands.

SomethingDark
  • 13,229
  • 5
  • 50
  • 55
  • Yes, I agree with what you say, but I don't think it's quite relevant to what I'm doing. I edited my question to clarify that I am running these instructions interactively at the command line, not inside of a script. – Jay Bazuzi Mar 20 '23 at 19:05
  • Well batch scripts are just a list of cmd commands to run and the order to run them in, so I think my point still stands, although for the sake of clarity and completion, I should change "parent script" to "parent environment" since the behavior is the same. – SomethingDark Mar 20 '23 at 19:25
  • Is your advice that every use of `||`/`&&` should be paired with `call`? – Jay Bazuzi Mar 20 '23 at 19:54
  • I'd recommend using `call` for scripts, but if the first thing in your line is a regular command rather than a script or other executable, you should be able to leave the `call` off. – SomethingDark Mar 20 '23 at 20:03
  • If I instead use `>echo @cmd /c "exit 1" > foo.cmd` the `||`/`&&` seems to work correctly. How does the parsing explination you gave apply there? – Jay Bazuzi Mar 20 '23 at 20:29
  • It definitely has something to do with the fact that there's an extra layer of calls there since `cmd /c` spawns an additional instance of the interpreter, but I'm afraid I don't know specifics. Jeb might know more. – SomethingDark Mar 20 '23 at 21:00
2

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.

Steve Hollasch
  • 2,011
  • 1
  • 20
  • 18