273

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.

wonea
  • 4,783
  • 17
  • 86
  • 139
Adam Bertram
  • 3,858
  • 4
  • 22
  • 28
  • 5
    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 Sep 12 '18 at 02:19
  • 1
    @mklement0 except maybe if one wants to use the `-Credential` parameter – CJBS Jun 11 '20 at 21:58
  • @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`. – mklement0 Jun 11 '20 at 22:28

11 Answers11

355

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's output streams connected to PowerShell's streams, allowing their output to be captured by simple assignment $output = netdom ... (and with 2> for stderr output), 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 ([object[]]), or enclose the command in @(...), the array-subexpression operator:[2]

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

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.

  • If the output contains more than 1 line, PowerShell by default splits it into an array of strings. More accurately, the output lines are streamed one by one, and, when captured, 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 {
    $_
  }
}

An aside re argument-passing, as of PowerShell 7.2.x:

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

[2] There are subtle differences between the two approaches (which you may combine), though they usually won't matter: If the command has no output, the [array] type-constraint approach results in $null getting stored in the target variable, whereas it is an empty ([object[]) array in the case of @(...). Additionally, the [array] type constraint means that future (non-empty) assignments to the same variable are coerced to an array too.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 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 Jun 07 '18 at 14:55
  • 2
    @Dan: When PowerShell itself interprets ``, 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? – mklement0 Jun 07 '18 at 15:23
  • Works: $command = "c:\mycommand.exe " + $Args ..... $output = cmd /c $command '2>&1' – Dan Jun 07 '18 at 15:39
  • 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_](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_splatting). – mklement0 Jun 07 '18 at 15:42
  • 1
    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. – not2qubit Apr 10 '19 at 16:08
  • I tried `$a = command` and `$a = command | Out-String` but both variants didn't capture lines that started with "#..." it appears that the lines are tried to be converted to objects and that powershell treats "#" as comments and outputs them directlry.. – Johannes Schaub - litb Jun 06 '23 at 23:12
  • @JohannesSchaub-litb, no output sent to _stdout_ / PowerShell's success [output stream](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Output_Streams) receives special treatment. Perhaps your `#`-prefixed lines are being sent to a _different_ output stream. I encourage you to post a new question if you have follow-up questions. – mklement0 Jun 06 '23 at 23:21
  • Thanks for your help! The #-es were sent to stderr. That's confusing, especially since they appear to be in-line with stdout (output of ssh-keyscan). – Johannes Schaub - litb Jun 07 '23 at 00:25
  • @JohannesSchaub-litb, yes, with direct-to-terminal output, stdout and stderr streams are _interleaved_, so that that you cannot tell which line came from what stream. To capture this interleaved output _programmatically_, you need to merge stderr into stdout (`2>&1`), with stdout mapping onto PowerShell's _success_ output stream (the generalization of this merging that also works for all _PowerShell_ output streams is `*>&1`). Output from streams _other_ than stdout/success is wrapped in stream-specific _objects_,as discussed in the answer with respect to stderr/ PowerShell's error stream. – mklement0 Jun 07 '23 at 01:23
190

Have you tried:

$OutputVariable = (Shell command) | Out-String

JNK
  • 63,321
  • 15
  • 122
  • 138
  • 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. – Adam Bertram Nov 11 '11 at 21:15
  • 13
    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 Feb 22 '13 at 23:45
  • 3
    @deadlydog Replace `Shell Command` with whatever you want to run. It's that simple. – JNK Feb 25 '13 at 14:26
  • @JNK This wasn't working for me because the shell command I was using didn't produce any output. I thought my variable was empty because there was a problem with this code, but it was actually specific to what I was doing. Thanks. – deadlydog Feb 25 '13 at 17:40
  • Does not work for ffmpeg command. ffmpeg output still goes to standard output instead of variable. – Atiq Rahman Aug 24 '13 at 05:16
  • @stej, it seems to return an array instead of a string in that case. – Sam Sep 02 '13 at 04:06
  • @Sam, the question was how to capture the output. If it should be only a string, then (shell_commnand) -join "`r`n" might help. – stej Sep 09 '13 at 13:04
  • 1
    @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! – Sam Sep 09 '13 at 22:04
  • 1
    @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. – jmbpiano Feb 26 '17 at 03:57
  • `Out-String` is not only generally not needed, it changes how PowerShell captures an external program's output by default. By default, multi-line output is captured in a string _array_; piping to `Out-String` always results in a _single_, potentially multi-line string. While either behavior may be desired in a given situation, it's important to know the difference. – mklement0 Sep 12 '18 at 02:12
  • don't know why but worked for me only was `$env:OutputVariable=(Shell command) | Out-String` – Nick Dec 31 '19 at 13:50
  • ahh, PS supports both nice – Nick Dec 31 '19 at 14:19
  • Could not get rid of powershell exception message, but suspect this is only best alternative at the moment – TarmoPikaro Mar 30 '21 at 19:46
37

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
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
manojlds
  • 290,304
  • 63
  • 469
  • 417
  • 6
    What does 2>&1 mean? 'run command called 2 and put its output into run command called 1'? – Richard Jan 24 '14 at 07:50
  • 11
    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). – gtirloni Jan 29 '14 at 19:09
12

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

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

$scriptOutput
Finzzownt
  • 380
  • 3
  • 6
11

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Davidiam
  • 119
  • 1
  • 2
11

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)
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Cwt
  • 8,206
  • 3
  • 32
  • 27
5

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)
Fidel
  • 7,027
  • 11
  • 57
  • 81
4

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

Faye Smelter
  • 131
  • 9
  • [`Invoke-Expression` should generally be avoided](https://stackoverflow.com/a/51252636/45375); definitely [don't use it to invoke an external program or PowerShell script](https://stackoverflow.com/a/57966347/45375). – mklement0 Aug 17 '20 at 02:26
2

This thing worked for me:

$scriptOutput = (cmd /s /c $FilePath $ArgumentList)
Alex R.
  • 467
  • 3
  • 14
1

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Kelly Davis
  • 354
  • 2
  • 6
1

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)
ccoutinho
  • 3,308
  • 5
  • 39
  • 47