Skip to content
mnaoumov.dev
Go back

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: ability 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!


Share this post on:

Previous Post
IE vs Firefox and Chrome and jQuery val() vs attr('value')
Next Post
Get EXIF metadata with PowerShell