How do I capture the output into a variable from an external process in PowerShell?
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.
Solution 1:
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):
# IMPORTANT:
# <command> is a *placeholder* for any valid command; e.g.:
# $cmdOutput = Get-Date
# $cmdOutput = attrib.exe +R readonly.txt
$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 (GitHub issue #14444 discusses this problematic behavior):
# 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.
-
-
Character-encoding issues can therefore come into play:
-
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 toOut-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 withsh
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
invokescmd.exe
with command<command>
and exits after<command>
has finished. -
Note the single quotes around
2>&1
, which ensures that the redirection is passed tocmd.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 forcmd.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 by2>&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 toOut-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 {
$_
}
}
An aside re argument-passing, as of PowerShell 7.1:
-
Passing arguments to external programs is broken with respect to empty-string arguments and arguments that contain embedded
"
characters. -
Additionally, the (nonstandard) quoting needs of executables such as
msiexec.exe
and batch files aren't accommodated.
For the former problem only, a fix may be coming (though the fix would be complete on Unix-like platforms), as discussed in this answer, which also details all the current problems and workarounds.
If installing a third-party module is an option, the ie
function from the Native
module (Install-Module Native
) offers a comprehensive solution.
[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.
Solution 2:
Have you tried:
$OutputVariable = (Shell command) | Out-String
Solution 3:
If you want to redirect the error output as well, you have to do:
$cmdOutput = command 2>&1
Or, if the program name has spaces in it:
$cmdOutput = & "command with spaces" 2>&1
Solution 4:
Or try this. It will capture output into variable $scriptOutput:
& "netdom.exe" $params | Tee-Object -Variable scriptOutput | Out-Null
$scriptOutput