Reputation: 164291
I need to capture the output of a external process into a variable (string), so I can do some processing on that.
The answer from here works nicely as long as the process writes to stdout. However, if the process fails, it writes to stderr. I'd like to capture this string too, and I can't figure out how to do it.
Example:
$cmdOutput = (svn info) | out-string
This works, unless SVN has an error. If an error occured, SVN writes to stderr, and $cmdOutput
is empty.
How do I capture the text written to stderr in a variable in PowerShell ?
Upvotes: 7
Views: 13857
Reputation: 437288
To complement manojlds' helpful answer with an overview:
To briefly explain redirection expression 2>&1
:
2>
redirects (>
) PowerShell's error output stream, whose number is 2
(and which maps onto stderr) into (&
) PowerShell's success output stream, whose number is 1
(and maps onto stdout).
Run Get-Help about_Redirection
to learn more.
Character-encoding caveat, which applies to all solutions below:
Capturing output from external programs or piping it to another command invariably subjects it to decoding into .NET strings, which can reveal a mismatch between the actual character encoding a given program uses and the one PowerShell uses for decoding, as reflected in [Console]::OutputEncoding
, which itself defaults to the console's active code page (as reported by chcp
).
Thus, you may have to (temporarily) set [Console]::OutputEncoding
to the actual encoding to ensure correct interpretation of non-ASCII characters in the output - see this answer for details.
As a collection of output lines as strings, without the ability to tell which line came from what stream:
Using the platform-native shell to perform the merging at the source makes PowerShell only see stdout output, which, as usual, it collects in an array of strings.
In the following examples, the commands each create both stdout and stderr output.
All commands use PSv3+ syntax for convenience and brevity.
Windows example:
# Collect combined output across both streams as an array of
# strings, where each string represents and output line.
# Note the selective quoting around & and 2>&1 to make sure they
# are passed through to cmd.exe rather than PowerShell itself interpreting them.
$allOutput = cmd /c ver '&' dir \nosuch '2>&1'
Unix example (PowerShell Core):
# sh, the Unix default shell, expects the entire command as a *single* argument.
$allOutput = sh -c '{ date; ls /nosuch; } 2>&1'
Note:
You do need to call the platform-native shell explicitly (cmd /c
on Windows, sh -c
on Unix), which makes this approach less portable.
You will not be able to tell from the resulting array of lines which line came from what stream.
See below for how to make this distinction.
As a mix of string lines (from stdout) and [System.Management.Automation.ErrorRecord]
"lines" (from stderr):
By using PowerShell's 2>&1
redirection, you also get a single stream of lines representing the merged stdout and stderr streams, but the stderr lines aren't captured as strings, but rather as [System.Management.Automation.ErrorRecord]
instances.
Caveat: A bug in Windows PowerShell v5.1 (since fixed in PowerShell (Core) 7+) results in unexpected behavior when you redirect PowerShell's error stream with 2>
while $ErrorActionPreference = 'Stop'
is in effect - see this GitHub issue.
This gives you the flexibility to distinguish between stdout and stderr lines by examining the data type of each output object.
On the flip side, you may have to convert the stderr lines to strings.
In PowerShell (Core) 7+, the different data types are not obvious when you simply display the captured output, but you can tell by reflection:
# Let PowerShell merge the streams with 2>&1, which captures
# stdout lines as strings and stderr lines as [System.Management.Automation.ErrorRecord]
# instances.
$allOutput = cmd /c ver '&' dir \nosuch 2>&1
Inspect the result (%
is a built-in alias for the ForEach-Object
cmdlet):
PS> $allOutput | % GetType | % Name
String
String
String
String
String
String
ErrorRecord
As you can see, the last array element is an error record, which represent the single File Not Found
stderr output line produced by the dir \nosuch
command.
Note:
[System.Management.Automation.ErrorRecord]
instances actually rendered in the same format as PowerShell errors, perhaps making it appear as if an error had occurred then.To convert all captured output to strings (for brevity, %
, the built-in alias of ForEach-Object
is used; in effect, the .ToString()
method is called on each output object):
$allOutput = cmd /c ver '&' dir \nosuch 2>&1 | % ToString
To filter out the stderr lines (and converting them to strings in the process; for brevity, ?
, the built-in alias of Where-Object
is used):
$allOutput = cmd /c ver '&' dir \nosuch 2>&1
$stderrOnly =
$allOutput |
? { $_ -is [System.Management.Automation.ErrorRecord] } |
% ToString
Using a temporary file:
As of PSv5.1, the only direct way to capture stderr output in isolation is to use redirection 2>
with a filename target; i.e., to capture stderr output - as text - in a file:
$stderrFile = New-TemporaryFile # PSv5+; PSv4-: use [io.path]::GetTempFileName()
$stdoutOutput = cmd /c ver '&' dir \nosuch 2>$stderrFile
$stderrOutput = Get-Content $stdErrFile
Remove-Item $stderrFile
Clearly, this is cumbersome and also slower than in-memory operations.
Using the PSv4+ intrinsic .Where()
array method:
The PSv4+ intrinsic .Where()
array method allows you to split a collection in two, based on whether the elements pass a Boolean test or not:
# Merge the streams first, so that stderr too goes to the success stream,
# then separate the objects in the merged stream by type.
$stdoutOutput, [string[]] $stderrOutput =
(cmd /c ver '&' dir \nosuch 2>&1).Where({ $_ -is [string] }, 'Split')
Note:
The [string[]]
type constraint converts the [System.Management.Automation.ErrorRecord]
instances that make up the stderr output to an array of strings.
The two output objects emitted by .Where()
are always collections (array-like, of type [System.Collections.ObjectModel.Collection[psobject]]
), even if they contain only one element.
While this approach is convenient and concise, its downside is that stdout output too is collected in full in memory first, and then has to be output in order to print to the console (if needed), which negates the benefits of PowerShell's usual streaming of output: relaying output lines as they're being received; in addition to potentially wasting memory if stdout needn't be collected in memory as a whole, this can be undesired in long-running external programs where ongoing visual feedback is important; see the next approach for a solution.
Combining in-memory capturing of stderr output with streaming stdout:
This approach avoids the .Where()
solution's collect-all-stdout-first behavior in favor of streaming stdout line as they're being received:
$stderr = [Collections.Generic.List[string]]::new()
# Merge the streams first, so that stderr too goes to the success stream,
# then decide based on the type whether to pass the line through (stdout)
# or to collect them in list $stderr.
cmd /c ver '&' dir \nosuch 2>&1 |
% { if ($_ -is [string]) { $_ } else { $stderr.Add($_) } }
Potential future improvement:
An experimental feature named PSRedirectToVariable
, available since preview 4 of v7.5, now allows variables to be redirection targets, using variable:<varname>
syntax.
# !! PowerShell *7.5 preview 4 or above only*, with
# !! experimental feature PSRedirectToVariable enabled.
# Collect stderr lines in variable $stderr
$stdout = cmd /c ver '&' dir \nosuch 2>variable:stderr
Caveat: As with any experimental feature,
it must be enabled, with Enable-ExperimentalFeature
(though in preview versions of PowerShell all experimental features are enabled by default).
it isn't guaranteed to become a stable (official) feature; feedback from the community and presumably usage information based on telemetry factor into the decision.
Upvotes: 9