How do I capture the output into a variable from an external process in PowerShell?

Adam Bertram picture Adam Bertram · Nov 11, 2011 · Viewed 302.4k times · Source

I'd like to run an external process and capture it's command output to a variable in PowerShell. I'm currently using this:

$params = "/verify $pc /domain:hosp.uhhg.org"
start-process "netdom.exe" $params -WindowStyle Hidden -Wait

I've confirmed the command is executing but I need to capture the output into a variable. This means I can't use the -RedirectOutput because this only redirects to a file.

Answer

mklement0 picture mklement0 · Mar 14, 2016

Note: The command in the question uses Start-Process, which prevents direct capturing of the target program's output. Generally, do not use Start-Process to execute console applications synchronously - just invoke them directly, as in any shell. Doing so keeps the application connected to the calling console's standard streams, allowing its output to be captured by simple assignment $output = netdom ..., as detailed below.

Fundamentally, capturing output from external programs works the same as with PowerShell-native commands (you may want a refresher on how to execute external programs; <command> is a placeholder for any valid command below):

$cmdOutput = <command>   # captures the command's success stream / stdout output

Note that $cmdOutput receives an array of objects if <command> produces more than 1 output object, which in the case of an external program means a string[1] array containing the program's output lines.

If you want to make sure that the result is always an array - even if only one object is output, type-constrain the variable as an array, or wrap the command in @(), the array-subexpression operator):

[array] $cmdOutput = <command> # or: $cmdOutput = @(<command>)

By contrast, if you want $cmdOutput to always receive a single - potentially multi-line - string, use Out-String, though note that a trailing newline is invariably added:

# Note: Adds a trailing newline.
$cmdOutput = <command> | Out-String

With calls to external programs - which by definition only ever return strings in PowerShell[1] - you can avoid that by using the -join operator instead:

# NO trailing newline.
$cmdOutput = (<command>) -join "`n"

Note: For simplicity, the above uses "`n" to create Unix-style LF-only newlines, which PowerShell happily accepts on all platforms; if you need platform-appropriate newlines (CRLF on Windows, LF on Unix), use [Environment]::NewLine instead.


To capture output in a variable and print to the screen:

<command> | Tee-Object -Variable cmdOutput # Note how the var name is NOT $-prefixed

Or, if <command> is a cmdlet or advanced function, you can use common parameter
-OutVariable / -ov
:

<command> -OutVariable cmdOutput   # cmdlets and advanced functions only

Note that with -OutVariable, unlike in the other scenarios, $cmdOutput is always a collection, even if only one object is output. Specifically, an instance of the array-like [System.Collections.ArrayList] type is returned.
See this GitHub issue for a discussion of this discrepancy.


To capture the output from multiple commands, use either a subexpression ($(...)) or call a script block ({ ... }) with & or .:

$cmdOutput = $(<command>; ...)  # subexpression

$cmdOutput = & {<command>; ...} # script block with & - creates child scope for vars.

$cmdOutput = . {<command>; ...} # script block with . - no child scope

Note that the general need to prefix with & (the call operator) an individual command whose name/path is quoted - e.g., $cmdOutput = & 'netdom.exe' ... - is not related to external programs per se (it equally applies to PowerShell scripts), but is a syntax requirement: PowerShell parses a statement that starts with a quoted string in expression mode by default, whereas argument mode is needed to invoke commands (cmdlets, external programs, functions, aliases), which is what & ensures.

The key difference between $(...) and & { ... } / . { ... } is that the former collects all input in memory before returning it as a whole, whereas the latter stream the output, suitable for one-by-one pipeline processing.


Redirections also work the same, fundamentally (but see caveats below):

$cmdOutput = <command> 2>&1 # redirect error stream (2) to success stream (1)

However, for external commands the following is more likely to work as expected:

$cmdOutput = cmd /c <command> '2>&1' # Let cmd.exe handle redirection - see below.

Considerations specific to external programs:

  • External programs, because they operate outside PowerShell's type system, only ever return strings via their success stream (stdout); similarly, PowerShell only ever sends strings to external programs via the pipeline.[1]

    • Character-encoding issues can therefore come into play:
      • On sending data via the pipeline to external programs, PowerShell uses the encoding stored in the $OutVariable preference variable; which in Windows PowerShell defaults to ASCII(!) and in PowerShell [Core] to UTF-8.

      • On receiving data from an external program, PowerShell uses the encoding stored in [Console]::OutputEncoding to decode the data, which in both PowerShell editions defaults to the system's active OEM code page.

      • See this answer for more information; this answer discusses the still-in-beta (as of this writing) Windows 10 feature that allows you to set UTF-8 as both the ANSI and the OEM code page system-wide.

  • If the output contains more than 1 line, PowerShell by default splits it into an array of strings. More accurately, the output lines are stored in an array of type [System.Object[]] whose elements are strings ([System.String]).

  • If you want the output to be a single, potentially multi-line string, use the -join operator (you can alternatively pipe to Out-String, but that invariably adds a trailing newline):
    $cmdOutput = (<command>) -join [Environment]::NewLine

  • Merging stderr into stdout with 2>&1, so as to also capture it as part of the success stream, comes with caveats:

    • To do this at the source, let cmd.exe handle the redirection, using the following idioms (works analogously with sh on Unix-like platforms):
      $cmdOutput = cmd /c <command> '2>&1' # *array* of strings (typically)
      $cmdOutput = (cmd /c <command> '2>&1') -join "`r`n" # single string

      • cmd /c invokes cmd.exe with command <command> and exits after <command> has finished.

      • Note the single quotes around 2>&1, which ensures that the redirection is passed to cmd.exe rather than being interpreted by PowerShell.

      • Note that involving cmd.exe means that its rules for escaping characters and expanding environment variables come into play, by default in addition to PowerShell's own requirements; in PS v3+ you can use special parameter --% (the so-called stop-parsing symbol) to turn off interpretation of the remaining parameters by PowerShell, except for cmd.exe-style environment-variable references such as %PATH%.

      • Note that since you're merging stdout and stderr at the source with this approach, you won't be able to distinguish between stdout-originated and stderr-originated lines in PowerShell; if you do need this distinction, use PowerShell's own 2>&1 redirection - see below.

    • Use PowerShell's 2>&1 redirection to know which lines came from what stream:

      • Stderr output is captured as error records ([System.Management.Automation.ErrorRecord]), not strings, so the output array may contain a mix of strings (each string representing a stdout line) and error records (each record representing a stderr line). Note that, as requested by 2>&1, both the strings and the error records are received through PowerShell's success output stream).

      • Note: The following only applies to Windows PowerShell - these problems have been corrected in PowerShell [Core] v6+, though the filtering technique by object type shown below ($_ -is [System.Management.Automation.ErrorRecord]) can also be useful there.

      • In the console, the error records print in red, and the 1st one by default produces multi-line display, in the same format that a cmdlet's non-terminating error would display; subsequent error records print in red as well, but only print their error message, on a single line.

      • When outputting to the console, the strings typically come first in the output array, followed by the error records (at least among a batch of stdout/stderr lines output "at the same time"), but, fortunately, when you capture the output, it is properly interleaved, using the same output order you would get without 2>&1; in other words: when outputting to the console, the captured output does NOT reflect the order in which stdout and stderr lines were generated by the external command.

      • If you capture the entire output in a single string with Out-String, PowerShell will add extra lines, because the string representation of an error record contains extra information such as location (At line:...) and category (+ CategoryInfo ...); curiously, this only applies to the first error record.

        • To work around this problem, apply the .ToString() method to each output object instead of piping to Out-String:
          $cmdOutput = <command> 2>&1 | % { $_.ToString() };
          in PS v3+ you can simplify to:
          $cmdOutput = <command> 2>&1 | % ToString
          (As a bonus, if the output isn't captured, this produces properly interleaved output even when printing to the console.)

        • Alternatively, filter the error records out and send them to PowerShell's error stream with Write-Error (as a bonus, if the output isn't captured, this produces properly interleaved output even when printing to the console):

$cmdOutput = <command> 2>&1 | ForEach-Object {
  if ($_ -is [System.Management.Automation.ErrorRecord]) {
    Write-Error $_
  } else {
    $_
  }
}

[1] As of PowerShell 7.1, PowerShell knows only strings when communicating with external programs. There is generally no concept of raw byte data in a PowerShell pipeline. If you want raw byte data returned from an external program, you must shell out to cmd.exe /c (Windows) or sh -c (Unix), save to a file there, then read that file in PowerShell. See this answer for more information.