Follow-up from #11461.
When using the PowerShell CLI with -c
/ -Command
, PowerShell by design sets its exit code based on the _success status_ of the _last_ statement executed in the specified command string.
If the last statement is a _PowerShell_ command, success can only be derived from the automatic success-status variable, $?
. As a _Boolean_, the only sensibly way to map that to an exit code is to map $true
to 0
, and $false
to 1
- this works as expected.
However, if the last statement is a call to an _external program_, a process exit code is _directly_ available from the child process in which the external program executed, as reflected in automatic variable $LASTEXITCODE
. The same potentially applies to a call to an (in-process) call to a *.ps1
script that uses exit <n>
.
Given that $LASTEXITCODE
can contain more specific information than just abstract success vs. failure, it makes sense to preserve this specific information in the _caller's_ $LASTEXITCODE
value instead of mapping it in a lossy manner to either 0
($LASTEXITCODE
being 0
) or 1
(_any nonzero_ $LASTEXITCODE
value) - which is what currently happens.
While this would technically be a breaking change, it seems to me that it falls into Bucket 3: Unlikely Grey Area:
In the typical case, code checks for abstract _success vs. failure_ of child processes, which means that _any nonzero_ exit code indicates failure, without regard to the _specific_ nonzero value.
The proposed change wouldn't interfere with that, while at the same time providing more specific information to code that _does_ care about the _specific_ nonzero exit code reported.
On Unix:
& { pwsh -noprofile -c 'sh -c ''exit 5'''; $LASTEXITCODE } | Should -Be 5
The test should pass.
The test fails, because the process exit code 5
was unexpectedly mapped to 1
in the caller's scope:
Expected 5, but got 1.
PowerShell Core 7.1.0-preview.6
Just my thoughts: I do not believe that it's PowerShell's task to handle the errorcode of another executable that was started inside a script and to use that as it's own exitcode.
If you want PowerShell to return a specific exitcode you have to use "exit $MyExitCode".
Especially if the exitcode would be ambiguous. E.g. Pwsh returns an exitcode of 1 if the script was interrupted (CTRL+C).
If your process also returns a 1 on failure you would be unable to distinguish between these two cases.
I'm using "exit $MyExitCode" in some scripts to inform the starting script about the result:
pwsh -noprofil -c 'dir c: ; if ($?) {Exit 200} else {exit 13}'
$LASTEXITCODE
A $LASTEXITCODE of 1 indicates that the script was interrupted.
@Northman-de
I do not believe that it's PowerShell's task to handle the errorcode of another executable
It is PowerShell's job - as a _shell_ - to play as nicely with the outside world as it can - an aspiration that it has historically often fallen short of.
With -c
/ -Command
, PowerShell's CLI sets an exit code deliberately - which is good - but it throws away information in the process - which is bad.
Clearly, the design intent was to translate the success status of the (last) command executed in the command string into an appropriate process exit code; for PowerShell-native commands mapping $?
to 0
and 1
is the best that can be done, but for external executables and *.ps1
scripts an appropriate exit code is _directly available_ and should be used as such.
Note that calling a *.ps1
script with -File
_does_ respect an exit <n>
statement and reports <n>
as the exit code:
PS> 'exit 5' > ./t.ps1; pwsh -noprofile -file ./t.ps1; $LASTEXITCODE
5
There are situations where you need to call .ps1
scripts _via -c
/ -Command
_ instead, such as when you need to pass _array_ arguments.
PS> 'exit 5' > ./t.ps1; pwsh -noprofile -c ./t.ps1 -param foo, bar; $LASTEXITCODE
1 # !! Specific exit code was lost, solely due to switching from -File to -Command.
Does this discrepancy make sense to you? I think it amounts to a pitfall that is easily avoided.
Especially if the exitcode would be ambiguous.
This is really a separate issue, worth tackling in its own right.
It is an issue that (a) already exists and (b) one that, if anything, could be _helped_ by resolving the issue at hand, making _workarounds_ easier.
It is unfortunate that PowerShell chose to use the nondescript exit code 1
for termination via Ctrl-C and for unhandled script-terminating errors (created with throw
, for instance).
POSIX-like shells more sensibly reserve a range of exit codes to indicate termination _by signal_ (which Ctrl-C constitutes, via SIGINT
): 128 + <signal-number
; thus, since SIGINT
has a numeric value of 2
, exit code 130
is reported.
@mklement0
I do not believe that it's PowerShell's task to handle the errorcode of another executable
It is PowerShell's job - as a _shell_ - to play as nicely with the outside world as it can - an aspiration that it has historically often fallen short of.
Come on. You let out the and-part "and to use that as it's own exitcode" :-)
Does this discrepancy make sense to you?
Indeed, it does :-).
From the pwsh help for -command
:
Executes the specified commands (and any parameters) as though they were typed at the PowerShell command prompt, and then exits, unless the NoExit parameter is specified.
The parameter is for executing commands, not processes or scripts.
As you wrote: PowerShell sets it's own exit code based on the execution result of the whole command.
In your example the execution "fails" as the exitcode of t.ps1 is not 0.
[..], but for external executables and *.ps1 scripts an appropriate exit code is directly available and should be used as such.
You are not executing a script (as you would with using -file
), you are executing a command that starts a script and that fails and results in an exit code of 1.
The exit code of t.ps1 remains inside the command. Your command is just one script. What exit code should be returned if the command contains more? E.g.:
./t.ps1 ; dir $PWD
Actually this returns 0 as the last instruction succeded.
The -file
parameter is for scripts and therefore the exitcode is the one of the script (my personal(!) conclusion).
Even with -command
you can specify the exit code as the one from the script:
[PS]> set-content -Path t2.ps1 -Value 'param ([int[]]$param); $param.Gettype(); $param; exit 5'
[PS]> $array = 1..5
[PS]> $cmd = './t2.ps1 -param {0}; Write-Host "ExitCode: $LASTEXITCODE"; Exit $LASTEXITCODE' -f ($array -join ",")
[PS]> pwsh -noprofile -c $cmd ; "PWSH ExitCode: $LASTEXITCODE"
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32[] System.Array
1
2
3
4
5
ExitCode: 5
PWSH ExitCode: 5
It is unfortunate that PowerShell chose to use the nondescript exit code
1
for termination via Ctrl-C and for unhandled script-terminating errors (created withthrow
, for instance).
I guess that's because PowerShell has it's roots in Windows...
POSIX-like shells more sensibly reserve a range of exit codes to indicate termination _by signal_ (which Ctrl-C constitutes, via
SIGINT
):128 + <signal-number
; thus, sinceSIGINT
has a numeric value of2
, exit code130
is reported.
I wasn't aware of that. That's an interesting solution.
@Northman-de
Come on. You let out the and-part "and to use that as it's own exitcode" :-)
That omission wasn't intentional (I simply pasted what I thought was enough to provide context for my response), and it has no bearing on the points that I made.
I understand how it currently works (the -File
vs. -Command
behavior), but my point is that the current -Command
behavior _in itself_ falls short; that it exhibits an inconsistency with the -File
behavior (why report the _abstract success status_ in one case, and the specific exit code in the other?) makes matters worse.
As you wrote: PowerShell sets its own exit code based on the execution result of the whole command.
No: It sets its exit code based on the _last_ statement executed, which is also what POSIX-compatible shells do (except they also do so when exiting an _interactive_ session, unlike PowerShell).
To recap the issue: Given that PowerShell intentionally derives the exit code from the statement executed _last_ in a -Command
call, it makes sense to use _that last statement's exit code as-is_ - if it has one; this is always the case with calls to external programs and *.ps1
scripts (see note below).
In other words: There is no reason to throw away the _specific_ exit code in favor of an abstract one; PowerShell is only forced to _choose_ an exit code for statement forms that _don't have exit codes_ (PowerShell-native commands and expressions), where the mapping of $?
to 0
($true
) and 1
($false
) makes sense.
I guess that's because PowerShell has its roots in Windows...
That is no excuse, because process exit codes and forced termination with Ctrl-C have always existed on
Windows as well.
As an aside: The inconsistency of *.ps1
exit code reporting:
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:
-File
, unlike with -Command
, the exit code is categorically set to 0
in the absence of an exit
statement (barring abnormal termination).$LASTEXITCODE
) is _not set at all_ for the script _as a whole_ in the absence of an exit
statement.See #11712 for details.
So right now an exit code of 1
means something very specific for folks already manually using exit codes: an unexpected error occurred, something that I as the writer didn't plan for.
Lets say I have a script that SCCM kicks off as part of an application install. In that script I exit with 0x200
to indicate that a reboot is needed. At the end of that script I fire off a native application that I expect always exits with 0
but happens to exit with 0x200
every now and then. What would have been flagged as an obvious problem with my script in certain environments is now flagged as a successful install that just needs to reboot.
Also if you know an executable only returns 0
or 1
it's not unreasonable to say code == 1
means there was an error. afaik the only ways you get something other than those two at the moment are if PowerShell straight up crashes or an exit code is explicitly specified. This change would definitely be at least bucket 2 and I think an argument could be made for bucket 1.
@SeeminglyScience - fair point re what bucket the change falls into: I was thinking of &&
/ ||
use and tools that check for any _nonzero_ exit code to infer failure - irrespective of its specific value.
I don't know know how common testing for 1
_specifically_ is in the real world.
I'll leave it to others to make the bucket call.
If it turns out to be too breaking, we have another candidate for #6745 on our hands (more on the why below).
means something very specific for folks already manually using exit codes: an unexpected error occurred, something that I as the writer didn't plan for.
Of course, the only specificity is in the _abstraction_ that _something_ went wrong - not _what_, which only a _specific_ exit code can communicate.
There's no reason for PowerShell not to enable reporting a specific exit code (if available) as the default experience, which requires addressing the following issues:
-Command
/ -c
CLI call should report the last (executed) statement's _specific_ exit code (if available), as -File
already does.throw
) should use a reserved range of exit codes to allow distinguishing these cases from regular termination with deliberately set exit codes.I don't know know how common testing for
1
specifically is in the real world.
Unfortunately it's hard to tell any specifics. That said, orchestration software often lets you assign meaning to specific exit codes, or set which error codes specifically mean fail. It'll break stuff for sure, though it's more or less impossible to know how much.
Of course, the only specificity is in the _abstraction_ that _something_ went wrong - not _what_, which only a _specific_ exit code can communicate.
Well yes and no. You would actually lose a little bit of information in changing this. Right now if my script exits with a non-zero exit code that isn't 1
then I know for sure that it comes from one of my exit
statements specifically. If it's 1
then it means I messed up and didn't handle something. It's sort of like catching specific exceptions vs an untyped catch block.
There's no reason for PowerShell not to enable reporting a specific exit code (if available) as the default experience, which requires addressing the following issues:
I disagree that it should be the default for compatibility reasons, but I'd love to see a switch for it.
it's not unreasonable to say code == 1
I saw this in some code while on a call this week. I suspect it would be as common as the old $WindowsVersion.StartsWith('9')
.
I'm not a huge fan of the current exit code, but it's established behaviour that users depend on and is easy enough to override.
but I'd love to see a switch for it.
Yeah that's the kind of thing I'd like to see, but I doubt will happen simply due to time and testing requirements.
it's established behaviour
Unfortunately, it's not documented, though, so let's start there:
about_Pwsh
: https://github.com/MicrosoftDocs/PowerShell-Docs/issues/6548about_Scripts
: https://github.com/MicrosoftDocs/PowerShell-Docs/issues/6559In the process I discovered:
Ctrl-C with -File
sets the exit code to 0
(!) - see #13523
$LASTEXITCODE
to the POSIX-compliant 130
, but only on Unix; on Windows, $LASTEXITCODE
isn't set at all in that case (same as not exiting with exit
from the script - see #11712)A non-numeric exit
argument or a numeric value outside the platform-supported range ([int]
on Windows, [byte]
on Unix) quietly results in 0
(!).
easy enough to override.
I we can't change the current behavior for backward-compatibility reasons, then I personally think that is enough (advising people to use exit $LASTEXITCODE
with -Command
in the docs), so I'm closing this - thanks for the discussion.
Most helpful comment
Unfortunately, it's not documented, though, so let's start there:
about_Pwsh
: https://github.com/MicrosoftDocs/PowerShell-Docs/issues/6548about_Scripts
: https://github.com/MicrosoftDocs/PowerShell-Docs/issues/6559In the process I discovered:
Ctrl-C with
-File
sets the exit code to0
(!) - see #13523$LASTEXITCODE
to the POSIX-compliant130
, but only on Unix; on Windows,$LASTEXITCODE
isn't set at all in that case (same as not exiting withexit
from the script - see #11712)A non-numeric
exit
argument or a numeric value outside the platform-supported range ([int]
on Windows,[byte]
on Unix) quietly results in0
(!).