PowerShell and process exit codes - powershell

This self-answered question tries to address two distinct aspects of dealing with process exit codes in PowerShell:
In PowerShell code, how can you query the exit code set by an external process (a call to an external program), and how do such exit codes integrate with PowerShell's error handling?
When someone else calls PowerShell via its CLI, pwsh (PowerShell Core) / powershell.exe (Windows PowerShell), what determines the PowerShell process' exit code that communicates success vs. failure to the calling process (which could be a build / CI / automation-server task, a scheduled task, or a different shell, for instance).

Current as of PowerShell [Core] 7.2.1
PowerShell-internal use of exit codes:
PowerShell-internally, where native PowerShell commands generally run in-process, exit codes from child processes that run external programs play a very limited role:
Native PowerShell commands generally don't set exit codes and don't act on them.
PowerShell has an abstract counterpart to exit codes: $?, the automatic, Boolean success-status variable:
It reflects whether the most recently executed command had any errors, but in practice it is rarely used, not least because - up to version 6.x - something as seemingly inconsequential as enclosing a command in (...) resets $? to $true - see GitHub issue #3359 - and because using Write-Error in user functions doesn't set $? to $false - see GitHub issue #3629; however, eventually providing the ability for user code to set $? explicitly has been green-lit for a future version.
While $? also reflects (immediately afterwards) whether an external program reported an exit code of 0 (signaling success, making $? report $true) or a nonzero exit code (typically signaling failure, making $? $false), it is the automatic $LASTEXICODE variable that contains the specific exit code as an integer, and that value is retained until another external program, if any, is called in the same session.
Caveat: Due to cmd.exe's quirks, a batch file's exit code isn't reliably reported, but you can work around that with cmd /c <batch-file> ... `& exit - see this answer; GitHub issue #15143 additionally suggests building this workaround into PowerShell itself.
Also, up to v7.1, $? can report false negatives if the external program reports exit code 0 while also producing stderr output and there is also a PowerShell redirection involving 2> or *> - see this answer and GitHub issue #3996; as of PowerShell PowerShell Core 7.2.0-preview.4; the corrected behavior is a available as experimental feature PSNotApplyErrorActionToStderr.
Finally, it's best to treat $LASTEXITCODE as read-only and let only PowerShell itself set it. (Technically, the variable is writeable and lives in the global scope, so if you do want to modify manually, after all, be sure to assign to $global:LASTEXITCODE, so as not accidentally create a transient local copy that has no effect.)
Unlike terminating errors or non-terminating errors reported by PowerShell-native commands, nonzero exit codes from external programs can not be automatically acted upon by the $ErrorActionPreference preference variable; that is, you cannot use that variable to silence stderr output from external programs nor can you, more importantly, choose to abort a script via value 'Stop' when an external program reports a nonzero exit code.
Better integration of external programs into PowerShell's error handling is being proposed in RFC #277.
How to control what PowerShell reports as its exit code when it is called from the outside:
Setting an exit code that at least communicates success (0) vs. failure (nonzero, typically) is an important mechanism for letting outside callers know whether your PowerShell code succeeded overall or not, such as when being called from a scheduled task or from an automation server such as Jenkins via the PowerShell CLI (command-line interface) - pwsh for PowerShell [Core] vs. powershell.exe for Windows PowerShell.
The CLI offers two ways to execute PowerShell code, and you can use exit <n> to set an exit code, where <n> is the desired exit code:
-File <script> [args...] expects the path of a script file (*.ps1) to execute, optionally followed by arguments.
Executing exit <n> directly inside such a script file (not inside another script that you call from that script) makes the PowerShell process report its exit code as <n>.
If a given script file exits implicitly or with just exit (without an exit-code argument), exit code 0 is reported.
-Command <powershell-code> expects a string containing one or more PowerShell commands.
To be safe, use exit <n> as a direct part of that command string - typically, as the last statement.
If your code is called from tools that check success by exit code, make sure that all code paths explicitly use exit <n> to terminate.
Caveat: If the PowerShell process terminates due to an unhandled script-terminating error - irrespective of whether the CLI was invoked with -File or -Command - the exit code is always 1.
A script-terminating (fatal) error is either generated from PowerShell code with the throw statement or by escalating a less a severe native PowerShell error with -ErrorAction Stop or $ErrorActionPreference = 'Stop', or by pressing Ctrl-C to forcefully terminate a script.
If exit code 1 isn't specific enough (it usually is, because typically only success vs. failure needs to be communicated), you can wrap your code in a try / catch statement and use exit <n> from the catch block.
The exact rules for how PowerShell sets its process exit code are complex; find a summary below.
How PowerShell sets its process exit code:
If an unhandled script-terminating error occurs, the exit code is always 1.
With -File, executing a script file (*.ps1):
If the script directly executes exit <n>, <n> becomes the exit code (such statements in nested calls are not effective).
Otherwise, it is 0, even if non-terminating or statement-terminating errors occurred during script execution.
With -Command, executing a command string containing one or more statements:
If an exit <n> statement is executed directly as one of the statements passed in the command string (typically, the last statement), <n> becomes the exit code.
Otherwise, it is the success status of the last statement executed, as implied by $?, that determines the exit code:
If $? is:
$true -> exit code 0
$false -> exit code 1 - even in the case where the last executed statement was an external program that reported a different nonzero exit code.
Given that the last statement in your command string may not be the one whose success vs. failure you want to signal, use exit <n> explicitly to reliably control the exit code, which also allows you to report specific nonzero exit codes.
For instance, to faithfully relay the exit code reported by an external program, append ; exit $LASTEXITCODE to the string you pass to -Command.
Inconsistencies and pitfalls as of PowerShell 7.0:
Arguably, -Command (-c) should report the specific exit code of the last statement - provided it has one - instead of the abstract 0 vs. 1. For instance, pwsh -c 'findstr'; $LASTEXITCODE should report 2, findstr.exe's specific exit code, instead of the abstract 1 - see GitHub issue #13501.
Exit-code reporting with *.ps1 files / the -File CLI parameter:
It is only an explicit exit <n> statement that meaningfully sets an exit code; instead, it should again be the last statement executed in the script that determines the exit code (which, of course, could be an exit statement), as is the case in POSIX-compatible shells and with -Command, albeit in the suboptimal manner discussed.
When you call a *.ps1 script via -File or as the last statement via -Command, PowerShell's exit code in the absence of the script exiting via an exit statement is always 0 (except in the exceptional Ctrl-C / throw cases, where it becomes 1).
By contrast, when called in-session, again in the absence of exit, $LASTEXICODE reflects the exit code of whatever external program (or other *.ps1 if it set an exit code) was executed last - whether executed inside the script or even before.
In other words:
With -File, unlike with -Command, the exit code is categorically set to 0 in the absence of an exit statement (barring abnormal termination).
In-session, the exit code (as reflected in $LASTEXITCODE) is not set at all for the script as a whole in the absence of an exit statement.
See GitHub issue #11712.

Related

How to fix 'script returned exit code 1' when running Powershell script through Jenkins?

I have a Powershell script which runs some Unity unit tests. On fail, this returns code 1, which Jenkins interprets as a fail and stops the current build. How do I avoid this behavior?
NOTE: This question is almost identical to this one, but that one uses bash, so I cannot apply it to my problem. In other words, how do I mimic the set +e behavior in Powershell? Alternatively, how do I tell Jenkins to ignore these script return codes and to continue the build anyway?
how do I mimic the set +e behavior in Powershell
You don't have to - it's the default behavior in that calls to external programs never[1] cause execution to stop prematurely, even if they report nonzero exit codes.
Alternatively, how do I tell Jenkins to ignore these script return codes and to continue the build anyway?
Ensuring that your script always terminates with exit 0 is indeed the correct way to ensure that Jenkins doesn't consider the script call failed (though you'll lose the information as to whether or not the tests failed).
[1] It can happen accidentally, due to flawed behavior up to PowerShell 7.1: If you use a 2> redirection and $ErrorActionPreference = 'Stop' happens to be effect, a script-terminating error occurs when stderr output is actually received. There is also pending RFC #277, which suggests introducing an opt-in mechanism for properly integrating external-program calls into PowerShell's error handling.

Run command in powershell and ignore exit code in one line

I try to execute a command in powershell and ignore any non zeroexit code. Unfortunately I completely fail doing this :-(
Under Linux this is done with this trivial line:
command arg1 arg2 || echo "ignore failure"
The or clause is executed only in case of a failure and then the exit code of echo resets $?
I thought something like this would do the trick:
Invoke-Expression "command arg1 arg2" -ErrorAction Ignore
But $LASTEXITCODE is still set to a non zero value
PowerShell v7+'s pipeline-chain operators, && and ||, implicitly act on $LASTEXITCODE, but never reset it.
If you do want to reset it - which is generally not necessary - you can do the following:
command arg1 arg2 || & { "ignore failure"; $global:LASTEXITCODE = 0 }
Note that PowerShell scripts - unlike scripts for POSIX-compatible shells such as bash - do not implicitly exit with the exit code of the most recently executed command; instead, you must use exit $n explicitly, where $n is the desired exit code.
In the context of calling the PowerShell CLI from the outside, the above applies to using the -File parameter to call a script; for use with the -Command (-c) parameter, see the next section.
As for what you tried:
|| and && don't work in Windows PowerShell (versions up to v5.1) at all.
Invoke-Expression doesn't help here and should generally be avoided and used only as a last resort, due to its inherent security risks. In short: Avoid it, if possible, given that superior alternatives are usually available. If there truly is no alternative, only ever use it on input you either provided yourself or fully trust - see this answer.
If you're using the Windows PowerShell CLI with -Command (-c), and you need to make sure that the PowerShell process exits with exit code 0, do something like the following (... represents your command):
powershell.exe -noprofile -c "...; exit 0"
If you want to comment on the failure:
powershell.exe -noprofile -c "...; if ($LASTEXITCODE) { 'ignore failure' }; exit 0"
Note: In this case, ; exit 0 isn't strictly necessary, because the if statement alone, due to it succeeding, irrespective of the value of $LASTEXITCODE, is enough to make the exit code 0.
Also, note that PowerShell CLI sends all of PowerShell's output streams - including the error stream - to stdout by default, though you can selective redirect the error stream on demand with 2>.
This also applies to the PowerShell [Core] v7+ CLI, whose executable name is pwsh, and whose parameters are a superset of the Windows PowerShell CLI.
For more information on PowerShell with respect to process exit codes, see this answer.

How to return non zero exit code from a Powershell module function without closing the powershell console?

My powershell module has a function and I want it to return a non zero exit code. However, being a module function it is loaded into the context of the powershell console when I run Import-Module. So, when the function executes exit 1 - poof, goes the console window!
At least, this is how I explain it closing the powershell window when it exits.
So, how can a ps module function exit with a non zero exit code without killing the console where the module was imported?
P.S.
I did notice several questions on SO about this subject, but none seems to examine this particular case.
EDIT 1
I would like to provide some context. I have a PS module with a lot of functions. Some of them are used as is in Azure DevOps yaml build scripts. The latter knows to recognize non zero exit code and abort the pipeline, so it is not necessary to throw from a function to abort the flow.
However, if I want to call that function from the console, e.g. to test something quickly and it exits with non zero code, the whole console window is closed. This is extremely annoying.
Sometimes there is a workaround. So, instead of this code:
dotnet build ...
$ExitCode = $LastExitCode
DoSomethingInAnyCase()
if ($ExitCode)
{
exit $ExitCode
}
We can have the following version:
try
{
dotnet build ...
}
finally
{
DoSomethingInAnyCase()
}
Both versions would correctly return the right exit code, but because the second one does not have the explicit exit statement, it does not close the console.
You'll have to set $global:LASTEXITCODE in order to set the exit code, but note that PowerShell functions aren't really meant to set exit codes, only scripts, and the latter only for reporting exit codes to the outside world, via the PowerShell process' own exit code, when PowerShell is called via its (CLI powershell.exe for Windows PowerShell, pwsh for PowerShell Core) from a build tool, scheduled task, or another shell, for instance.
Also note that setting $global:LASTEXITCODE directly:
does not make $?, the automatic success-status variable, reflect $false in the caller's context, the way that exit <nonzero-value> does from a script and the way that calling an external program that reports a nonzero exit code does.
is not enough to make the PowerShell process as a whole report this exit code.
In short: All this gains you is that the caller can inspect $LASTEXITCODE after your function was called, as you would after calling an external program.
Generally, exit codes are an inter-process concept and do not fit well into PowerShell's in-process world.
For more information about exit codes in PowerShell, see this post.
PowerShell's analog to exit codes is $?, the automatic, Boolean success-status variable ($true or $false), which reflects a PowerShell command's success immediately afterwards.
(If that command is an external-program call, an exit code of 0 sets $? to $true, and any nonzero one sets it to $false).
As it turns out, setting that was what you really meant to ask.
As of PowerShell Core 7.0.0-preview.5, you cannot set $? directly.
For now, these are the only ways to cause $? to reflect $false in the caller's scope, and, conversely, you cannot always ensure that it is $true:
From a script: Exit the script with exit <nonzero-integer>
From a cmdlet or function (script as well):
Throw a script-terminating error (Throw) or a statement-terminating error ($PSCmdlet.ThrowTerminatingError()) - the latter being only available in advanced functions and scripts.
Write an error to PowerShell's error stream with $PSCmdlet.WriteError() - the latter being only available in advanced functions and scripts.
Note that this unexpectedly currently does not apply to the Write-Error cmdlet - see this GitHub issue
Note that both techniques invariably involve emitting an error.
Since it sounds like that's precisely what you're trying to avoid, you'll have to wait until the ability to set $? directly is implemented.
The decision to implement this ability has been made, but it's unclear when it will be implemented.
Workaround: Run cmd.exe and set whatever exit code you want before exiting the function. Example:
function Test-Function {
$cmd = Join-Path ([Environment]::GetFolderPath([Environment+SpecialFolder]::System)) "cmd.exe"
& $cmd /c exit 3
# $LASTEXITCODE will be set to 3
}

Equivalent of bash "set -o errexit" for windows cmd.exe batch file?

What's the Windows batch file equivalent of set -o errexit in a bash script?
I have a long batch file filled with different programs to run on Windows command line... basically its an unrolled make file with every compiler command that needs to be run to build an exe in a long sequence of commands.
The problem with this method is that I want it to exit the batch command on the first non-zero return code generate by a command in the script.
As far as I know, Windows batch files have a problem where they don't automatically exit on the first error without adding a lot of repetitive boilerplate code between each command to check for a non-zero return code and to exit the script.
What I'm wondering about, is there an option similar to bash's set -o errexit for Windows cmd.exe? or perhaps a technique that works to eliminate too much boilerplate error checking code... like you set it up once and then it automatically exits if a command returns a non-zero return code without adding a bunch of junk to your script to do this for you.
(I would accept PowerShell option as well instead of cmd.exe, except PowerShell isn't very nice with old-unix-style command flags like: -dontbreak -y ... breaking those commands without adding junk to your command line like quotes or escape characters... not really something I want to mess around with either...)
CMD/Batch
As Ken mentioned in the comments, CMD does not have an equivalent to the bash option -e (or the equivalent -o errexit). You'd have to check the exit status of each command, which is stored in the variable %errorlevel% (equivalent to $? in bash). Something like
if %errorlevel% neq 0 then exit /b %errorlevel%
PowerShell
PowerShell already automatically terminates script execution on errors in most cases. However, there are two error classes in PowerShell: terminating and non-terminating. The latter just displays an error without terminating script execution. The behavior can be controlled via the variable $ErrorActionPreference:
$ErrorActionPreference = 'Stop': terminate on all errors (terminating and non-terminating)
$ErrorActionPreference = 'Continue' (default): terminate on terminating errors, continue on non-terminating errors
$ErrorActionPreference = 'SilentlyContinue': don't terminate on any error
PowerShell also allows more fine-grained error handling via try/catch statements:
try {
# run command here
} catch [System.SomeException] {
# handle exception of a specific type
} catch [System.OtherException] {
# handle exception of a different type
} catch {
# handle all other exceptions
} finally {
# cleanup statements that are run regardless of whether or not
# an exception was thrown
}

Automatically stop powershell script on bat errors

Generally one can use $ErrorActionPreference to stop the execution of a PS script on error as in the example below that uses an invalid subversion command. The script exits after the first command.
$ErrorActionPreference='Stop'
svn foo
svn foo
Trying the same thing with the maven command (mvn.bat) executes both commands.
$ErrorActionPreference='Stop'
mvn foo
mvn foo
Initially I suspected that mvn.bat does not set an error code and PS just doesn't see the error. However, $? is set properly as demonstrated by the following, when PS exits after the first error:
mvn foo
if (-not $?) {exit 1}
mvn foo
So here's my question: Given that both svn.exe and mvn.bat set an error code on failure, why does the PS script not stop after the mvn error. I find it a lot more convenient to set "$ErrorActionPreference=Stop" globally rather than doing "if (-not $?) {exit 1}" after each command to terminate on error.
Not all command line programs provide errors in the same way. Some set an exit code. Some don't. Some use the error stream, some don't. I've seen some command line programs actually output everything to error, and always output non-zero return codes.
So there's not a real safe guess one could ever make as to it having run successfully, and therefore it's next to impossible to codify that behavior into PowerShell.
$errorActionPreference will actually stop a script whenever a .exe writes to the error stream, but many .exes will write regular output to the console and never populate the error stream.
But it will not reliably work. Neither, for that matter, will $?. If an exe returns 0 and doesn't write to error $? will be true.
While it might be a pain in the butt to deal with each of these individual .exes and their errors in PowerShell, it's a great example of why PowerShell's highly structured Output/Error/Verbose/Warning/Debug/Progress streams and consistent error behavior in Cmdlets beats out plain old .EXE tools.
Hope this Helps
$ErrorActionPreference controls PowerShell's behavior with regard to commandlets and PowerShell functions -- it is not sensitive to exit codes of "legacy" commands. I'm still looking for a work-around for this design decision myself -- for this issue, refer to this thread: How to stop a PowerShell script on the first error?. You'll probably conclude that this custom "exec" function is the best solution: http://jameskovacs.com/2010/02/25/the-exec-problem/
With regard to the stop-on-stderr behavior, I can't reproduce this behavior in PowerShell 3:
Invoke-Command
{
$ErrorActionPreference='Stop'
cmd /c "echo hi >&2" # Output to stderr (this does not cause termination)
.\DoesNotExist.exe # Try to run a program that doesn't exist (this throws)
echo bye # This never executes
}
Update: The stop-on-stderr behavior appears to take effect when using PS remoting.