devops-collective-inc/big-book-of-powershell-error-handling

not strictly true: $? will be set to True if the $LASTEXITCODE variable equals zero

Closed this issue · 3 comments

As mentioned on

$? will be set to True if the $LASTEXITCODE variable equals zero

It seems that $? is rather following a fact, if the external program produced an error, and not the exit code it provided.

Some programs do write all, or some non erroneous output to error stream,
producing couple of entries in $Error variable. While still returns exit code 0

e.g.
REG IMPORT file.reg
produces output in error stream:

REG : The operation completed successfully.
At C:\my.ps1:23 char:13

  •         REG IMPORT $file.reg
    
  •         ~~~~~~~~~~~~~~~~~~~~~~
    
    • CategoryInfo : NotSpecified: (The operation completed successfully.:String) [], Remot
      eException
    • FullyQualifiedErrorId : NativeCommandError

$? is false
$LASTEXITCODE is 0
$Error variable contains two new errors

Chocolatey guys run across this issue too and investigated further why:
chocolatey/choco#249 (comment)

There are more programs, where I noticed this behavior in the past, but I didn't check the $? value there

You're more than welcome to submit changes as a pull request!

Unfortunately it seems that the story is even more complicated.
If the exit code is not zero then $? is set to $false; so far so good.
But other than that, it is very difficult to make any statements without being specific to the context / environment.

For example the code shown below will write some text to the error stream and exit with code 0 and 1 respectively:

$Error.Clear()

'Exit Code = 0 / Non-Redirected Error Stream'
cmd /c 'echo SomeErrorText 1>&2 && exit 0'
'$?:            ' + $?
'$LASTEXITCODE: ' + $LASTEXITCODE
'$Error.Count:  ' + $Error.Count
'-------------------------------------------'

$Error.Clear()

'Exit Code = 1 / Non-Redirected Error Stream'
cmd /c 'echo SomeErrorText 1>&2 && exit 1'
'$?:            ' + $?
'$LASTEXITCODE: ' + $LASTEXITCODE
'$Error.Count:  ' + $Error.Count
'-------------------------------------------'

Running it in the console (both PoSh 5.1 & 6.1) will result in the following output:

Exit Code = 0 / Non-Redirected Error Stream
SomeErrorText
$?:            True
$LASTEXITCODE: 0
$Error.Count:  0
-------------------------------------------
Exit Code = 1 / Non-Redirected Error Stream
SomeErrorText
$?:            False
$LASTEXITCODE: 1
$Error.Count:  0
-------------------------------------------

So the output on the error stream did not result in $? being set to $false.


This could change however when we run the same in another host like the ISE:

Exit Code = 0 / Non-Redirected Error Stream
cmd : SomeErrorText  
At C:\temp\QuestionMarkVarOnErrStream.ps1:4 char:1
+ cmd /c 'echo SomeErrorText 1>&2 && exit 0'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (SomeErrorText  :String) [], RemoteEx 
   ception
    + FullyQualifiedErrorId : NativeCommandError
 
$?:            False
$LASTEXITCODE: 0
$Error.Count:  1
-------------------------------------------
Exit Code = 1 / Non-Redirected Error Stream
cmd : SomeErrorText  
At C:\temp\QuestionMarkVarOnErrStream.ps1:13 char:1
+ cmd /c 'echo SomeErrorText 1>&2 && exit 1'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (SomeErrorText  :String) [], RemoteEx 
   ception
    + FullyQualifiedErrorId : NativeCommandError
 
$?:            False
$LASTEXITCODE: 1
$Error.Count:  1
-------------------------------------------

So this time the output on the error stream did result in $? being set to $false.


Finally if we start to redirect the error stream like so:

$Error.Clear()

'Exit Code = 0 / Redirected Error Stream'
cmd /c 'echo SomeErrorText 1>&2 && exit 0' 2> err.txt
'$?:            ' + $?
'$LASTEXITCODE: ' + $LASTEXITCODE
'$Error.Count:  ' + $Error.Count
'-------------------------------------------'

$Error.Clear()

'Exit Code = 1 / Redirected Error Stream'
cmd /c 'echo SomeErrorText 1>&2 && exit 1' 2> err.txt
'$?:            ' + $?
'$LASTEXITCODE: ' + $LASTEXITCODE
'$Error.Count:  ' + $Error.Count
'-------------------------------------------'

Then running it in the console (again both PoSh 5.1 and 6.1) will start creating error objects for output on this stream and it did result in $? being set to $false:

Exit Code = 0 / Redirected Error Stream
$?:            False
$LASTEXITCODE: 0
$Error.Count:  1
-------------------------------------------
Exit Code = 1 / Redirected Error Stream
$?:            False
$LASTEXITCODE: 1
$Error.Count:  1
-------------------------------------------

(Personal) Conclusion: Relying on $? being set to $false when $LASTEXITCODE is not zero seems to be safe. The other way around is not always true and depends on other factors like the PoSh host and whether an error stream redirection are in place. For further details there is an issue with lots of details about PoSh error handling in general.


IMHO the best we can currently do is to add some hint about the fact that relying on $? being set to $false when $LASTEXITCODE is not zero should be OK, but the other way around is not really reliable and should be tested within the given environment. Maybe I will come up with a small PR.

What do you think?