55

Is it possible to redirect stdout from an external program to a variable and stderr from external programs to another variable in one run?

For example:

$global:ERRORS = @();
$global:PROGERR = @();

function test() {
    # Can we redirect errors to $PROGERR here, leaving stdout for $OUTPUT?
    $OUTPUT = (& myprogram.exe 'argv[0]', 'argv[1]');

    if ( $OUTPUT | select-string -Pattern "foo" ) {
        # do stuff
    } else {
        $global:ERRORS += "test(): oh noes! 'foo' missing!";
    }
}

test;
if ( @($global:ERRORS).length -gt 0 ) {
    Write-Host "Script specific error occurred";
    foreach ( $err in $global:ERRORS ) {
        $host.ui.WriteErrorLine("err: $err");
    }
} else {
    Write-Host "Script ran fine!";
}

if ( @($global:PROGERR).length -gt 0 ) {
    # do stuff
} else {
    Write-Host "External program ran fine!";
}

A dull example however I am wondering if that is possible?

Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
dusz
  • 913
  • 1
  • 9
  • 15
  • You could use Start-Process to run myprogram.exe [as described here](http://stackoverflow.com/questions/8761888/powershell-capturing-standard-out-and-error-with-start-process). It captures STDOUT and STDERR separately. – Alexander Obersht Jun 14 '14 at 19:25

7 Answers7

61

One option is to combine the output of stdout and stderr into a single stream, then filter.

Data from stdout will be strings, while stderr produces System.Management.Automation.ErrorRecord objects.

$allOutput = & myprogram.exe 2>&1
$stderr = $allOutput | ?{ $_ -is [System.Management.Automation.ErrorRecord] }
$stdout = $allOutput | ?{ $_ -isnot [System.Management.Automation.ErrorRecord] }
Aaron Schultz
  • 1,188
  • 1
  • 8
  • 9
  • 18
    Or even better, replace the first line with `& myprogram.exe 2>&1 | tee -Variable allOutput`. That way you get the output printed for free, even keeping the order when stdout and stderr are interleaved (none of the other answers give that). This also doesn't go through any files which is a win in terms of performance and minimization of things that can fail. – Ohad Schneider Mar 22 '16 at 17:22
  • 4
    Combining @OhadSchneider's approach with capturing the output in a variable without outputting it: `[Void] (& myprog.exe 2>&1 | tee -Variable allOutput)` and then `$stdout = $allOutput | ?{ $_ -isnot [System.Management.Automation.ErrorRecord] }`. – ComFreek Aug 20 '17 at 11:07
22

The easiest way to do this is to use a file for the stderr output, e.g.:

$output = & myprogram.exe 'argv[0]', 'argv[1]' 2>stderr.txt
$err = get-content stderr.txt
if ($LastExitCode -ne 0) { ... handle error ... }

I would also use $LastExitCode to check for errors from native console EXE files.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Keith Hill
  • 194,368
  • 42
  • 353
  • 369
  • 3
    It is important to point out (at least for stderr), the output is not what the command produces, but rather it is the hosts report of what the command-produced, which would come as a shock to those coming from other backgrounds. – Cameron Kerr Nov 13 '16 at 10:10
  • 2
    Since `stderr.txt` is probably used only once, [`New-TemporaryFile`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/new-temporaryfile) comes in handy – Franklin Yu May 01 '18 at 20:28
9

You should be using Start-Process with -RedirectStandardError -RedirectStandardOutput options. This other post has a great example of how to do this (sampled from that post below):

$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "ping.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "localhost"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "exit code: " + $p.ExitCode
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
AckSynFool
  • 101
  • 5
5

This is also an alternative that I have used to redirect stdout and stderr of a command line while still showing the output during PowerShell execution:

$command = "myexecutable.exe my command line params"

Invoke-Expression $command -OutVariable output -ErrorVariable errors
Write-Host "STDOUT"
Write-Host $output
Write-Host "STDERR"
Write-Host $errors

It is just another possibility to supplement what was already given.

Keep in mind this may not always work depending upon how the script is invoked. I have had problems with -OutVariable and -ErrorVariable when invoked from a standard command line rather than a PowerShell command line like this:

PowerShell -File ".\FileName.ps1"

An alternative that seems to work under most circumstances is this:

$stdOutAndError = Invoke-Expression "$command 2>&1"

Unfortunately, you will lose output to the command line during execution of the script and would have to Write-Host $stdOutAndError after the command returns to make it "a part of the record" (like a part of a Jenkins batch file run). And unfortunately it doesn't separate stdout and stderr.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
James Eby
  • 1,784
  • 20
  • 25
1

Copied from my answer on how to capture both output and verbose information in different variables.


Using Where-Object(The alias is symbol ?) is an obvious method, but it's a bit too cumbersome. It needs a lot of code.

In this way, it will not only take longer time, but also increase the probability of error.

In fact, there is a more concise method that separate different streams to different variable in PowerShell(it came to me by accident).

# First, declare a method that outputs both streams at the same time.
function thisFunc {
    [cmdletbinding()]
    param()
    Write-Output 'Output'
    Write-Verbose 'Verbose'
}
# The separation is done in a single statement.Our goal has been achieved.
$VerboseStream = (thisFunc -Verbose | Tee-Object -Variable 'String' | Out-Null) 4>&1

Then we verify the contents of these two variables

$VerboseStream.getType().FullName
$String.getType().FullName

The following information should appear on the console:

PS> System.Management.Automation.VerboseRecord
System.String

'4>&1' means to redirect the verboseStream to the success stream, which can then be saved to a variable, of course you can change this number to any number between 2 and 5.

powershell output streams

KyleMit
  • 30,350
  • 66
  • 462
  • 664
Andy
  • 1,077
  • 1
  • 8
  • 20
1

In case you want to get any from a PowerShell script and to pass a function name followed by any arguments you can use dot sourcing to call the function name and its parameters.

Then using part of James answer to get the $output or the $errors.

The .ps1 file is called W:\Path With Spaces\Get-Something.ps1 with a function inside named Get-It and a parameter FilePath.

Both the paths are wrapped in quotes to prevent spaces in the paths breaking the command.

$command = '. "C:\Path Spaces\Get-Something.ps1"; Get-It -FilePath "W:\Apps\settings.json"'

Invoke-Expression $command -OutVariable output -ErrorVariable errors | Out-Null

# This will get its output.
$output

# This will output the errors.
$errors
Ste
  • 1,729
  • 1
  • 17
  • 27
0

Separately, preserving formatting

cls
function GetAnsVal {
    param([Parameter(Mandatory=$true, ValueFromPipeline=$true)][System.Object[]][AllowEmptyString()]$Output,
          [Parameter(Mandatory=$false, ValueFromPipeline=$true)][System.String]$firstEncNew="UTF-8",
          [Parameter(Mandatory=$false, ValueFromPipeline=$true)][System.String]$secondEncNew="CP866"
    )
    function ConvertTo-Encoding ([string]$From, [string]$To){#"UTF-8" "CP866" "ASCII" "windows-1251"
        Begin{
            $encFrom = [System.Text.Encoding]::GetEncoding($from)
            $encTo = [System.Text.Encoding]::GetEncoding($to)
        }
        Process{
            $Text=($_).ToString()
            $bytes = $encTo.GetBytes($Text)
            $bytes = [System.Text.Encoding]::Convert($encFrom, $encTo, $bytes)
            $encTo.GetString($bytes)
        }
    }
    $all = New-Object System.Collections.Generic.List[System.Object];
    $exception = New-Object System.Collections.Generic.List[System.Object];
    $stderr = New-Object System.Collections.Generic.List[System.Object];
    $stdout = New-Object System.Collections.Generic.List[System.Object]
    $i = 0;$Output | % {
        if ($_ -ne $null){
            if ($_.GetType().FullName -ne 'System.Management.Automation.ErrorRecord'){
                if ($_.Exception.message -ne $null){$Temp=$_.Exception.message | ConvertTo-Encoding $firstEncNew $secondEncNew;$all.Add($Temp);$exception.Add($Temp)}
                elseif ($_ -ne $null){$Temp=$_ | ConvertTo-Encoding $firstEncNew $secondEncNew;$all.Add($Temp);$stdout.Add($Temp)}
            } else {
                #if (MyNonTerminatingError.Exception is AccessDeniedException)
                $Temp=$_.Exception.message | ConvertTo-Encoding $firstEncNew $secondEncNew;
                $all.Add($Temp);$stderr.Add($Temp)
            }   
         }
    $i++
    }
    [hashtable]$return = @{}
    $return.Meta0=$all;$return.Meta1=$exception;$return.Meta2=$stderr;$return.Meta3=$stdout;
    return $return
}
Add-Type -AssemblyName System.Windows.Forms;
& C:\Windows\System32\curl.exe 'api.ipify.org/?format=plain' 2>&1 | set-variable Output;
$r = & GetAnsVal $Output
$Meta2=""
foreach ($el in $r.Meta2){
    $Meta2+=$el
}
$Meta2=($Meta2 -split "[`r`n]") -join "`n"
$Meta2=($Meta2 -split "[`n]{2,}") -join "`n"
[Console]::Write("stderr:`n");
[Console]::Write($Meta2);
[Console]::Write("`n");
$Meta3=""
foreach ($el in $r.Meta3){
    $Meta3+=$el
}
$Meta3=($Meta3 -split "[`r`n]") -join "`n"
$Meta3=($Meta3 -split "[`n]{2,}") -join "`n"
[Console]::Write("stdout:`n");
[Console]::Write($Meta3);
[Console]::Write("`n");
Garric
  • 591
  • 3
  • 10