ChatGPT解决这个技术问题 Extra ChatGPT

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.

First and foremost: Do not use Start-Process to execute (by definition external) console applications synchronously - just invoke them directly, as in any shell; to wit: netdom /verify $pc /domain:hosp.uhhg.org. Doing so keeps the application connected to the calling console's standard streams, allowing its output to be captured by simple assignment $output = netdom .... Most answers given below implicitly forgo Start-Process in favor of direct execution.
@mklement0 except maybe if one wants to use the -Credential parameter
@CJBS Yes, in order to run with a different user identity, use of Start-Process is a must - but only then (and if you want to run a command in a separate window). And one should be aware of the unavoidable limitations in that case: No ability to capture output, except as - non-interleaved - text in files, via -RedirectStandardOutput and -RedirectStandardError.

m
mklement0

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: 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.

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 = () -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 '2>&1' # *array* of strings (typically) $cmdOutput = (cmd /c '2>&1') -join "`r`n" # single string cmd /c invokes cmd.exe with command and exits after 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 = 2>&1 | % { $_.ToString() }; in PS v3+ you can simplify to: $cmdOutput = 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):

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 '2>&1' # *array* of strings (typically) $cmdOutput = (cmd /c '2>&1') -join "`r`n" # single string cmd /c invokes cmd.exe with command and exits after 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.

cmd /c invokes cmd.exe with command and exits after 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 = 2>&1 | % { $_.ToString() }; in PS v3+ you can simplify to: $cmdOutput = 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):

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 = 2>&1 | % { $_.ToString() }; in PS v3+ you can simplify to: $cmdOutput = 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):

To work around this problem, apply the .ToString() method to each output object instead of piping to Out-String: $cmdOutput = 2>&1 | % { $_.ToString() }; in PS v3+ you can simplify to: $cmdOutput = 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.


This eventually worked for me after I took my executable path AND my arguments for it, tossed them in a string and treated that as my .
@Dan: When PowerShell itself interprets <command>, you mustn't combine the executable and its arguments in a single string; with invocation via cmd /c you may do so, and it depends on the situation whether it makes sense or not. Which scenario are you referring to, and can you give a minimal example?
Works: $command = "c:\mycommand.exe " + $Args ..... $output = cmd /c $command '2>&1'
@Dan: Yes, that works, though you don't need the intermediate variable and explicit construction of the string with the + operator; the following works too: cmd /c c:\mycommand.exe $Args '2>&1' - PowerShell takes care of passing the elements of $Args as a space-separated string in that case, a feature called splatting.
Finally a proper answer that works in PS6.1+. The secret in the sauce is indeed the '2>&1' part, and not enclosing in () as many scripts tend to do.
J
JNK

Have you tried:

$OutputVariable = (Shell command) | Out-String


I tried to assign it to a variable using "=" but I didn't try to pipe output to Out-String first. I'll give that a try.
I don't understand what is happening here and cannot get it to work. Is "Shell" a powershell keyword? So we don't actually use the Start-Process cmdlet? Can you please give a concrete example please (i.e. replace "Shell" and/or "command" with a real example).
@deadlydog Replace Shell Command with whatever you want to run. It's that simple.
@stej, you're right. I was mainly clarifying that the code in your comment had different functionality to the code in the answer. Beginners like me can can thrown off by subtle differences in behaviour like these!
@Atique I ran into the same issue. Turns out that ffmpeg will sometimes write to stderr instead of stdout if, for example, you use the -i option without specifying an output file. Redirecting the output using 2>&1 as described in some of the other answers is the solution.
P
Peter Mortensen

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

What does 2>&1 mean? 'run command called 2 and put its output into run command called 1'?
It means "redirect the standard error output (file descriptor 2) to the same place where the standard output (file descriptor 1) is going". Basically, redirects normal and error messages to the same place (in this case the console, if stdout is not redirected somewhere else -- like a file).
F
Finzzownt

Or try this. It will capture output into variable $scriptOutput:

& "netdom.exe" $params | Tee-Object -Variable scriptOutput | Out-Null

$scriptOutput

-1, unnecessarily complex. $scriptOutput = & "netdom.exe" $params
Removing the out-null and this is great for piping to both the shell and a variable at the same time.
P
Peter Mortensen

Another real-life example:

$result = & "$env:cust_tls_store\Tools\WDK\x64\devcon.exe" enable $strHwid 2>&1 | Out-String

Notice that this example includes a path (which begins with an environment variable). Notice that the quotes must surround the path and the EXE file, but not the parameters!

Note: Don't forget the & character in front of the command, but outside of the quotes.

The error output is also collected.

It took me a while to get this combination working, so I thought that I would share it.


P
Peter Mortensen

I tried the answers, but in my case I did not get the raw output. Instead it was converted to a PowerShell exception.

The raw result I got with:

$rawOutput = (cmd /c <command> 2`>`&1)

F
Fidel

I use the following:

Function GetProgramOutput([string]$exe, [string]$arguments)
{
    $process = New-Object -TypeName System.Diagnostics.Process
    $process.StartInfo.FileName = $exe
    $process.StartInfo.Arguments = $arguments
    
    $process.StartInfo.UseShellExecute = $false
    $process.StartInfo.RedirectStandardOutput = $true
    $process.StartInfo.RedirectStandardError = $true
    $process.Start()
    
    $output = $process.StandardOutput.ReadToEnd()   
    $err = $process.StandardError.ReadToEnd()
    
    $process.WaitForExit()
    
    $output
    $err
}
    
$exe = "C:\Program Files\7-Zip\7z.exe"
$arguments = "i"
    
$runResult = (GetProgramOutput $exe $arguments)
$stdout = $runResult[-2]
$stderr = $runResult[-1]
    
[System.Console]::WriteLine("Standard out: " + $stdout)
[System.Console]::WriteLine("Standard error: " + $stderr)

A
Alex R.

This thing worked for me:

$scriptOutput = (cmd /s /c $FilePath $ArgumentList)

F
Faye Smelter

I got the following to work:

$Command1="C:\\ProgramData\Amazon\Tools\ebsnvme-id.exe"
$result = & invoke-Expression $Command1 | Out-String

$result gives you the needful


P
Peter Mortensen

If all you are trying to do is capture the output from a command, then this will work well.

I use it for changing system time, as [timezoneinfo]::local always produces the same information, even after you have made changes to the system. This is the only way I can validate and log the change in time zone:

$NewTime = (powershell.exe -command [timezoneinfo]::local)
$NewTime | Tee-Object -FilePath $strLFpath\$strLFName -Append

Meaning that I have to open a new PowerShell session to reload the system variables.


c
ccoutinho

What did the trick for me, and would work when using external commands and also when both standard error and standard output streams could be the result of running the command (or a mix of them), was the following:

$output = (command 2>&1)