58

Why does PowerShell show the surprising behaviour in the second example below?

First, an example of sane behaviour:

PS C:\> & cmd /c "echo Hello from standard error 1>&2"; echo "`$LastExitCode=$LastExitCode and `$?=$?"
Hello from standard error
$LastExitCode=0 and $?=True

No surprises. I print a message to standard error (using cmd's echo). I inspect the variables $? and $LastExitCode. They equal to True and 0 respectively, as expected.

However, if I ask PowerShell to redirect standard error to standard output over the first command, I get a NativeCommandError:

PS C:\> & cmd /c "echo Hello from standard error 1>&2" 2>&1; echo "`$LastExitCode=$LastExitCode and `$?=$?"
cmd.exe : Hello from standard error
At line:1 char:4
+ cmd <<<<  /c "echo Hello from standard error 1>&2" 2>&1; echo "`$LastExitCode=$LastExitCode and `$?=$?"
    + CategoryInfo          : NotSpecified: (Hello from standard error :String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

$LastExitCode=0 and $?=False

My first question, why the NativeCommandError?

Secondly, why is $? False when cmd ran successfully and $LastExitCode is 0? PowerShell's documentation about automatic variables doesn't explicitly define $?. I always supposed it is True if and only if $LastExitCode is 0, but my example contradicts that.


Here's how I came across this behaviour in the real-world (simplified). It really is FUBAR. I was calling one PowerShell script from another. The inner script:

cmd /c "echo Hello from standard error 1>&2"
if (! $?)
{
    echo "Job failed. Sending email.."
    exit 1
}
# Do something else

Running this simply as .\job.ps1, it works fine, and no email is sent. However, I was calling it from another PowerShell script, logging to a file .\job.ps1 2>&1 > log.txt. In this case, an email is sent! What you do outside the script with the error stream affects the internal behaviour of the script. Observing a phenomenon changes the outcome. This feels like quantum physics rather than scripting!

[Interestingly: .\job.ps1 2>&1 may or not blow up depending on where you run it]

Machavity
  • 30,841
  • 27
  • 92
  • 100
Colonel Panic
  • 132,665
  • 89
  • 401
  • 465
  • possible duplicate of [Powershell difference between $? and $LastExitCode](http://stackoverflow.com/questions/10666035/powershell-difference-between-and-lastexitcode) – Raymond Chen May 19 '12 at 14:57
  • 1
    Raymond, related maybe, but not really a duplicate. It'd be nice if Jeffrey Snover chimed in here with [Word of God](http://tvtropes.org/pmwiki/pmwiki.php/Main/WordOfGod), though :-) – Joey May 19 '12 at 15:05
  • I asked the other question to make sure I understood the expected behaviour before exhibiting the unexpected. – Colonel Panic May 19 '12 at 15:11
  • I'd file »stderr output« under causes for `!$?` too with the caveat that it apparently doesn't work like expected. – Joey May 19 '12 at 15:13
  • It looks like there's a bug report for this already: https://connect.microsoft.com/PowerShell/feedback/details/185231/output-of-stderr-should-be-consistent-there-should-be-a-way-to-redirect-stderr-from-text-based-programs-that-use-it-without-powershell-thinking-that-each-line-of-text-is-an-error – Andy Arismendi May 20 '12 at 01:10
  • 2
    Looks like a workaround is to escape the redirection operator: `& cmd /c "echo Hello from standard error 1>&2" 2\`>\`&1` – Andy Arismendi May 20 '12 at 01:15
  • Thanks Andy. Don't understand why, but `2\`>\`&1` indeed works – Colonel Panic May 20 '12 at 08:54
  • I added a link to this question in the connect bug report. – Andy Arismendi May 20 '12 at 16:22
  • The backtick workaround is unreliable, some commands consume it as an argument, eg. `nslookup microsoft.com 2\`>\`&1` gives `Can't find server address for '2>&1'` – Colonel Panic May 22 '12 at 10:27
  • I would suggest using the exit code for error detection if you can. A work around for nslookup would be: `& cmd.exe /c nslookup "microsoft.com" 2\`>\`&1` – Andy Arismendi May 23 '12 at 02:57
  • 2
    Matt: Of course they should consume it as an argument. In the cited case it was an argument to the *shell* `cmd`. If you want redirection you need a shell that understands it. `nslookup` is just a command that does its thing but it's no shell. – Joey May 30 '12 at 07:21

5 Answers5

93

(I am using PowerShell v2.)

The '$?' variable is documented in about_Automatic_Variables:

$?
  Contains the execution status of the last operation

This is referring to the most recent PowerShell operation, as opposed to the last external command, which is what you get in $LastExitCode.

In your example, $LastExitCode is 0, because the last external command was cmd, which was successful in echoing some text. But the 2>&1 causes messages to stderr to be converted to error records in the output stream, which tells PowerShell that there was an error during the last operation, causing $? to be False.

To illustrate this a bit more, consider this:

> java -jar foo; $?; $LastExitCode
Unable to access jarfile foo
False
1

$LastExitCode is 1, because that was the exit code of java.exe. $? is False, because the very last thing the shell did failed.

But if all I do is switch them around:

> java -jar foo; $LastExitCode; $?
Unable to access jarfile foo
1
True

... then $? is True, because the last thing the shell did was print $LastExitCode to the host, which was successful.

Finally:

> &{ java -jar foo }; $?; $LastExitCode
Unable to access jarfile foo
True
1

...which seems a bit counter-intuitive, but $? is True now, because the execution of the script block was successful, even if the command run inside of it was not.


Returning to the 2>&1 redirect.... that causes an error record to go in the output stream, which is what gives that long-winded blob about the NativeCommandError. The shell is dumping the whole error record.

This can be especially annoying when all you want to do is pipe stderr and stdout together so they can be combined in a log file or something. Who wants PowerShell butting in to their log file??? If I do ant build 2>&1 >build.log, then any errors that go to stderr have PowerShell's nosey $0.02 tacked on, instead of getting clean error messages in my log file.

But, the output stream is not a text stream! Redirects are just another syntax for the object pipeline. The error records are objects, so all you have to do is convert the objects on that stream to strings before redirecting:

From:

> cmd /c "echo Hello from standard error 1>&2" 2>&1
cmd.exe : Hello from standard error
At line:1 char:4
+ cmd &2" 2>&1
    + CategoryInfo          : NotSpecified: (Hello from standard error :String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

To:

> cmd /c "echo Hello from standard error 1>&2" 2>&1 | %{ "$_" }
Hello from standard error

...and with a redirect to a file:

> cmd /c "echo Hello from standard error 1>&2" 2>&1 | %{ "$_" } | tee out.txt
Hello from standard error

...or just:

> cmd /c "echo Hello from standard error 1>&2" 2>&1 | %{ "$_" } >out.txt
Martin Liversage
  • 104,481
  • 22
  • 209
  • 256
Droj
  • 3,541
  • 3
  • 26
  • 19
  • 5
    Hey, love that `2>&1 | %{ "$_" }` workaround, I'll be using that, thanks. – Colonel Panic Oct 12 '12 at 20:42
  • 2
    That's right. Putting the stream object in quotes ("$_") converts to string. – Droj Dec 20 '13 at 15:41
  • 3
    in V2.0 `2>&1 | %{ "$_" }` does not work if ErrorActionPref is set to 'stop'. Is there a way to run the command and override the global EA value? – cmcginty Jun 06 '14 at 00:25
  • 1
    I combined this with $LASTEXITCODE check, otherwise command is considered failed: `command 2>&1 | %{ "$_" }; if ($LASTEXITCODE -ne 0) { throw "Command returned exit code $LASTEXITCODE" } else { Write-Host "Command finished successfully" }` – Vojta Apr 07 '15 at 10:33
  • 1
    @cmcginty makes a very important point. If you use $ErrorActionPreference="Stop", this doesn't solve the problem sadly. But as pointed out in the comment by @DKroot in the Start-NativeExecutable answer, wrapping the command that outputs to stderr in a `cmd /c "command 2>&1"` works well. – aggieNick02 Oct 18 '19 at 16:13
  • 1
    In PS7, `someCommand 2>&1 | %{ "$_" }` may output "System.Management.Automation.RemoteException" if the error stream contains an empty line. **Repro**: `git ls-remote invalidarg 2>&1 | %{ "$_" }`. **FIX**: `someCommand 2>&1 | Out-String -Stream`. Parameter `-Stream` allows us to continuously receive the output of the process, while it is written to the stream. – zett42 Nov 27 '20 at 16:38
25

This bug is an unforeseen consequence of PowerShell's prescriptive design for error handling, so most likely it will never be fixed. If your script plays only with other PowerShell scripts, you're safe. However if your script interacts with applications from the big wide world, this bug may bite.

PS> nslookup microsoft.com 2>&1 ; echo $?

False

Gotcha! Still, after some painful scratching, you'll never forget the lesson.

Use ($LastExitCode -eq 0) instead of $?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Colonel Panic
  • 132,665
  • 89
  • 401
  • 465
  • 4
    The converse applies of course - you can get apps (console scripts especially) that write to stdErr but *dont* set the exit code, and in that case you *must* detect those errors using $?. So the answer is ... it depends. – piers7 Nov 18 '15 at 00:29
  • 1
    @piers7 or you can capture stderr yourself to a variable (http://stackoverflow.com/questions/24222088/powershell-capture-program-stdout-and-stderr-to-seperate-variables) and then test both `$LastExitCode` and `$stderr`. – Ohad Schneider Mar 22 '16 at 18:04
  • 9
    Also, the advice should be *Use ($LastExitCode -eq 0) instead of $? **for native commands***. When invoking PowerShell scripts / Cmdlets, `$?` is the way to go - `$LastExitCode` won't even be set if the script doesn't contain an explicit `Exit-PSSession` call. – Ohad Schneider Apr 16 '16 at 22:31
  • I almost down-voted this because this is not a bug. What @OhadSchneider recommends is a better answer: use $LastExitCode instead of $? for native commands *only*. – chris Jul 12 '19 at 22:58
14

Update: The problems have been fixed in v7.2 - see this answer.


A summary of the problems as of v7.1:

The PowerShell engine still has bugs with respect to 2> redirections applied to external-program calls:

The root cause is that using 2> causes the stderr (standard error) output to be routed via PowerShell's error stream (see about_Redirection), which has the following undesired consequences:

  • If $ErrorActionPreference = 'Stop' happens to be in effect, using 2> unexpectedly triggers a script-terminating error, i.e. aborts the script (even in the form 2>$null, where the intent is clearly to ignore stderr lines). See GitHub issue #4002.

    • Workaround: (Temporarily) set $ErrorActionPreference = 'Continue'
  • Since 2> currently touches the error stream, $?, the automatic success-status variable is invariably set to $False if at least one stderr line was emitted, and then no longer reflects the true success status of the command. See this GitHub issue.

    • Workaround, as recommended in your answer: only ever use $LASTEXITCODE -eq 0 to test for success after calls to external programs.
  • With 2>, stderr lines are unexpectedly recorded in the automatic $Error variable (the variable that keeps a log of all errors that occurred in the session) - even if you use 2>$null. See this GitHub issue.

    • Workaround: Short of keeping track how many error records were added and removing them with $Error.RemoveAt() one by one, there is none.

Generally, unfortunately, some PowerShell hosts by default route stderr output from external programs via PowerShell's error stream, i.e. treat it as error output, which is inappropriate, because many external programs use stderr also for status information, or more generally, for anything that is not data (git being a prime example): Not every stderr line can be assumed to represent an error, and the presence of stderr output does not imply failure.

Affected hosts:

Hosts that DO behave as expected in non-remoting, non-background invocations (they pass stderr lines through to the display and print them normally):

This inconsistency across hosts is discussed in this GitHub issue.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • You might want to add that PS 7.1 has experimental feature to enable "sane" behavior: `Enable-ExperimentalFeature PSNotApplyErrorActionToStderr`. I'm going to delete my answer, as yours covers it more completely otherwise. – zett42 Nov 16 '22 at 20:31
  • 1
    @zett42, v7.2 has made this feature official, which the note at the top implies, i.e. the problem no longer exists in v7.2+, so I don't think the experimental v7.1 feature needs to be part of the answer anymore, but thanks for pointing it out in your comment. – mklement0 Nov 16 '22 at 22:18
11

(Note: This is mostly speculation; I rarely use many native commands in PowerShell and others probably know more about PowerShell internals than me)

I guess you found a discrepancy in the PowerShell console host.

  1. If PowerShell picks up stuff on the standard error stream it will assume an error and throw a NativeCommandError.
  2. PowerShell can only pick this up if it monitors the standard error stream.
  3. PowerShell ISE has to monitor it, because it is no console application and thus a native console application has no console to write to. This is why in the PowerShell ISE this fails regardless of the 2>&1 redirection operator.
  4. The console host will monitor the standard error stream if you use the 2>&1 redirection operator because output on the standard error stream has to be redirected and thus read.

My guess here is that the console PowerShell host is lazy and just hands native console commands the console if it doesn't need to do any processing on their output.

I would really believe this to be a bug, because PowerShell behaves differently depending on the host application.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Joey
  • 344,408
  • 85
  • 689
  • 683
  • I agree with Joey's assessment, and I think PowerShell.exe's behavior should be improved. – JasonMArcher May 19 '12 at 15:48
  • You're telling me in Powershell _ISE_, both my commands blow up. Now I'm more confused! – Colonel Panic May 19 '12 at 21:00
  • Are there any Powershell docs concerning point 1? IMHO this is a bad design decision, many programs print debugging information to standard error, this doesn't mean they failed. – Colonel Panic May 19 '12 at 21:15
  • Just searched the specification for that. Nothing that would explain the behaviour you see. Also a little fuzzy on what exactly constitutes an error for `$?`. – Joey May 19 '12 at 22:08
  • 1
    I made a quick python script that just writes a message to stderr of the console and executed it with `powershell.exe` and `$?` was true. The python script was just an import of sys and then `sys.stderr.write("Hi There\n")`. So it seems `$?` is only false if the native command exit code is non-zero. – Andy Arismendi May 20 '12 at 00:26
  • Andy: Indeed; I see the same with a small C# program: `class Test{static void Main(){System.Console.Error.WriteLine("foo"); System.Environment.Exit(0);}}`. In that case `$LastExitCode` seems to behave strange with the sample code Matt has given in his question. This gets really weird. – Joey May 20 '12 at 07:49
  • I found a bug related to this and put a link in the question comments section above. The work around is to escape `>` and `&` using PowerShell's escape character which to me suggests PowerShell parser is not tokenizing `2>&1` properly when it's an argument to a native command. – Andy Arismendi May 20 '12 at 16:35
  • That's a great explanation of why the ISE behaves differently here - I'd never thought of that. Thanks. – piers7 Nov 18 '15 at 00:29
1

For me it was an issue with ErrorActionPreference. When running from ISE I've set $ErrorActionPreference = "Stop" in the first lines and that was intercepting everything event with *>&1 added as parameters to the call.

So first I had this line:

& $exe $parameters *>&1

Which like I've said didn't work because I had $ErrorActionPreference = "Stop" earlier in file (or it can be set globally in profile for user launching the script).

So I've tried to wrap it in Invoke-Expression to force ErrorAction:

Invoke-Expression -Command "& `"$exe`" $parameters *>&1" -ErrorAction Continue

And this doesn't work either.

So I had to fallback to hack with temporary overriding ErrorActionPreference:

$old_error_action_preference = $ErrorActionPreference

try
{
    $ErrorActionPreference = "Continue"
    & $exe $parameters *>&1
}
finally
{
    $ErrorActionPreference = $old_error_action_preference
}

Which is working for me.

And I've wrapped that into a function:

<#
    .SYNOPSIS

    Executes native executable in specified directory (if specified)
    and optionally overriding global $ErrorActionPreference.
#>
function Start-NativeExecutable
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    Param
    (
        [Parameter (Mandatory = $true, Position = 0, ValueFromPipelinebyPropertyName=$True)]
        [ValidateNotNullOrEmpty()]
        [string] $Path,

        [Parameter (Mandatory = $false, Position = 1, ValueFromPipelinebyPropertyName=$True)]
        [string] $Parameters,

        [Parameter (Mandatory = $false, Position = 2, ValueFromPipelinebyPropertyName=$True)]
        [string] $WorkingDirectory,

        [Parameter (Mandatory = $false, Position = 3, ValueFromPipelinebyPropertyName=$True)]
        [string] $GlobalErrorActionPreference,

        [Parameter (Mandatory = $false, Position = 4, ValueFromPipelinebyPropertyName=$True)]
        [switch] $RedirectAllOutput
    )

    if ($WorkingDirectory)
    {
        $old_work_dir = Resolve-Path .
        cd $WorkingDirectory
    }

    if ($GlobalErrorActionPreference)
    {
        $old_error_action_preference = $ErrorActionPreference
        $ErrorActionPreference = $GlobalErrorActionPreference
    }

    try
    {
        Write-Verbose "& $Path $Parameters"

        if ($RedirectAllOutput)
            { & $Path $Parameters *>&1 }
        else
            { & $Path $Parameters }
    }
    finally
    {
        if ($WorkingDirectory)
            { cd $old_work_dir }

        if ($GlobalErrorActionPreference)
            { $ErrorActionPreference = $old_error_action_preference }
    }
}
Michael Logutov
  • 2,551
  • 4
  • 28
  • 32
  • There is an active bug opened about this behavior with Microsoft since 2011: https://connect.microsoft.com/PowerShell/feedback/details/645954. A better solution though to redirect stderr and prevent errors is to use cmd /c "command 2>&1" – Dima Korobskiy Dec 08 '15 at 15:57