PowerShell: Capture the output from external proce

2020-02-11 07:09发布

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 ?

2条回答
乱世女痞
2楼-- · 2020-02-11 07:39

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.

Capturing stdout and stderr output combined, as a merged stream:


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 collection of lines representing the merged stdout and stderr streams, but the stderr lines aren't captured as strings, but as [System.Management.Automation.ErrorRecord] instances.

Caveat: A bug as of Windows PowerShell v5.1 / PowerShell Core v6.0.0-beta.4 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 array element.
On the flip side, you may have to convert the stderr lines to strings.

In PSv6, the different data types are not obvious when you simply output 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:

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: Up to PSv5.1, when you output the captured output to the console, the [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.
In PSv6, these error records print just their message, i.e., the contents of the original stderr line, and visually you can't tell that an error record rather than a string is being printed.

To convert all captured output to strings:

$allOutput = cmd /c ver '&' dir \nosuch 2>&1 | % ToString

To filter out the stderr lines (and converting them to strings in the process):

$allOutput = cmd /c ver '&' dir \nosuch 2>&1
$stderrOnly = $allOutput | ? { $_ -is [System.Management.Automation.ErrorRecord] } | 
  % ToString

Capturing stderr output separately:


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+ .Where() collection operator:

The (little-known) PSv4+ .Where() collection operator allows you to split a collection in two, based on whether the elements pass a Boolean test or not:

 # Merge the streams first, so they go to the success stream, then
 # split the objects in the merged stream by type.
 $stdoutOutput, $stderrOutput = (cmd /c ver '&' dir \nosuch 2>&1).Where({
   $_ -isnot [System.Management.Automation.ErrorRecord]
 }, 'Split')

 # Convert the collected [System.Management.Automation.ErrorRecord]
 # instances to strings.
 $stderrOutput = $stderrOutput | % ToString

Potential future alternative:

This GitHub issue proposes a new redirection syntax that would allow collecting stderr lines in a variable much more simply:

 # As of v5.1: WISHFUL THINKING
 $stdout = cmd /c ver '&' dir \nosuch 2>&stderr # collect stderr lines in var. $stderr
查看更多
beautiful°
3楼-- · 2020-02-11 07:58

Try this:

$cmdOutput = svn info 2>&1
查看更多
登录 后发表回答