Execution of external commands in PowerShell done right

Part 2 Part 3

Hi folks

Execution external (native) commands in PowerShell is not an easy thing. It looks like it is simple but it has a lot of downsides.

We’ll consider the following command

cmd /c "echo STDOUT & echo STDERR 1>&2 & exit 345"

And use it like that

It writes a message to STDOUT, a message to STDERR and returns some exit code.

Capturing output

> $result = cmd /c "echo STDOUT & echo STDERR 1>&2 & exit 345"
STDERR
> $result
STDOUT

As you see, STDERR was not captured to the variable and written to the console.

Let’s try to redirect STDERR to STDOUT first

> $result = cmd /c "echo STDOUT & echo STDERR 1>&2 & exit 345" 2>&1
> $result
STDOUT
cmd.exe : STDERR
At line:1 char:14
+ $result = cmd <<<<  /c "echo STDOUT & echo STDERR 1>&2 & exit 345" 2>&1
    + CategoryInfo          : NotSpecified: (STDERR  :String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

So we see that error is captured to the variable but is thrown when we extract it.

The reason for that is the fact that STDERR was captured not as string but as ErrorRecord

> $result[1].GetType().FullName
System.Management.Automation.ErrorRecord

To extract it

> $result = cmd /c "echo STDOUT & echo STDERR 1>&2 & exit 345" 2>&1 | % { "$_" }
> $result
STDOUT
STDERR

Looks good.

But if we have

> $ErrorActionPreference = "Stop"
> # ... some code
> $result = cmd /c "echo STDOUT & echo STDERR 1>&2 & exit 345" 2>&1 | % { "$_" }
cmd.exe : STDERR
At line:1 char:14
+ $result = cmd <<<<  /c "echo STDOUT & echo STDERR 1>&2 & exit 345" 2>&1 | % { "$_" }
    + CategoryInfo          : NotSpecified: (STDERR  :String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

it fails again. The code above works only when $ErrorActionPreference = “Continue”

So the correct approach would be to change it to back and forward

> $ErrorActionPreference = "Stop"
> # ... some code
> $backupErrorActionPreference = $ErrorActionPreference
> $ErrorActionPreference = "Continue"
> $result = cmd /c "echo STDOUT & echo STDERR 1>&2 & exit 345" 2>&1 | % { "$_" }
> $ErrorActionPreference = $backupErrorActionPreference
> $result
STDOUT
STDERR

Capturing failures

We’ve already discussed $ErrorActionPreference = “Stop” before

this setting makes the whole script fail on the first error occurred.

But this does not respect external commands exit codes.

Normally exit code 0 considered as success and all others as failure. We’ll add a check manually

> $ErrorActionPreference = "Stop"
> # ... some code
> $backupErrorActionPreference = $ErrorActionPreference
> $ErrorActionPreference = "Continue"
> $result = cmd /c "echo STDOUT & echo STDERR 1>&2 & exit 345" 2>&1 | % { "$_" }
> $ErrorActionPreference = $backupErrorActionPreference
> $result
STDOUT
STDERR
> if ($LASTEXITCODE -ne 0)
>> {
>>     throw "Exit code $LASTEXITCODE"
>> }
>>
Exit code 345
At line:3 char:10
+     throw <<<<  "Exit code $LASTEXITCODE"
    + CategoryInfo          : OperationStopped: (Exited 345:String) [], RuntimeException
    + FullyQualifiedErrorId : Exit code 345

Final version

Let’s create a helper to consolidate all the complexity required in one place.
We’ll two more features: abilitity to prefix STDERR messages if necessary and whitelist of exit codes that we want to consider as success.

function exec
{
    param
    (
        [ScriptBlock] $ScriptBlock,
        [string] $StderrPrefix = "",
        [int[]] $AllowedExitCodes = @(0)
    )

    $backupErrorActionPreference = $script:ErrorActionPreference

    $script:ErrorActionPreference = "Continue"
    try
    {
        & $ScriptBlock 2>&1 | ForEach-Object -Process `
            {
                if ($_ -is [System.Management.Automation.ErrorRecord])
                {
                    "$StderrPrefix$_"
                }
                else
                {
                    "$_"
                }
            }
        if ($AllowedExitCodes -notcontains $LASTEXITCODE)
        {
            throw "Execution failed with exit code $LASTEXITCODE"
        }
    }
    finally
    {
        $script:ErrorActionPreference = $backupErrorActionPreference
    }
}

And now

> $result = exec { cmd /c "echo STDOUT & echo STDERR 1>&2 & exit 345" }
Execution failed with exit code 345
At line:26 char:18
+             throw <<<<  "Execution failed with exit code $LASTEXITCODE"
    + CategoryInfo          : OperationStopped: (Execution failed with exit code 345:String) [], RuntimeException
    + FullyQualifiedErrorId : Execution failed with exit code 345
> $result = exec { cmd /c "echo STDOUT & echo STDERR 1>&2 & exit 345" } -StderrPrefix "STDERR: " -AllowedExitCodes @(0, 345)
> $result
STDOUT
STDERR: STDERR

Not that easy, heh?

Stay tuned!

Advertisements

About mnaoumov

Senior .NET Developer in Readify
This entry was posted in Uncategorized and tagged . Bookmark the permalink.

3 Responses to Execution of external commands in PowerShell done right

  1. Pingback: Execution of external commands (native applications) in PowerShell done right – Part 2 | mnaoumov.NET

  2. Pingback: Execution of external commands (native applications) in PowerShell done right – Part 3 | mnaoumov.NET

  3. Nybbler says:

    Thanks for taking the time to write this up — extremely helpful!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s