Search code examples
powershellstdoutsubcommand

Command output printed instead of captured in Powershell


In general, I know how to capture the output of a command into a variable in Powershell:

> $output = python3 --version
> Write-Host $output
Python 3.7.0

Python 3 prints its version but it's captured into the variable $output and nothing is displayed on the console. As expected:

> $output = python3 --version
> Write-Host $output
Python 3.7.0

But in the case of Python 2, it doesn't work. Python 2 prints its version but its displayed on the console instead of captured and the variable $output is empty.

> $output = python2 --version
Python 2.7.15
> Write-Host $output

>

I have tried some alternative syntaxes but so far nothing helped:

> $output = (python2 --version)  # use a subcommand
Python 2.7.15
> $output = "$(python2 --version)"  # enclose the subcommand into a string
Python 2.7.15

If I use it in Write-Command, the output looks like the following:

> Write-Host "Python version: $(python2 --version)"
Python 2.7.15
Python version:
>

How is Python 2 printing its version that it's not captured into a variable and is it possible to capture it anyhow?


Solution

  • It's caused by Python 2 which prints version to stderr instead of stdout. Assignment of program call's output captures stdout only which is empty in this case. On the other side, stderr is printed to the console by default, that's why the $output variable is empty and the version is printed to the console. See below for details.

    tl; dr:

    # Redirect stderr to the success output stream and convert to a string.
    $output = (python --version 2>&1).ToString()
    # $? is now $True if python could be invoked, and $False otherwise
    

    For commands that may return multiple stderr lines, use:

    • PSv4+, using the .ForEach() method:

      $output = (python --version 2>&1).ForEach('ToString')
      
    • PSv3-, using the ForEach-Object cmdlet (whose built-in alias is %):

      # Note: The (...) around the python call are required for
      #       $? to have the expected value.
      $output = (python --version 2>&1) | % { $_.ToString() }
      
      # Shorter PSv3+ equivalent, using an operation statement
      $output = (python --version 2>&1) | % ToString
      

    • As Mathias R. Jessen notes in a comment on the question and you yourself clarified in terms of versions, python 2.x - surprisingly - outputs its version information to stderr rather than stdout, unlike 3.x.

    • The reason you weren't able to capture the output with $output = ... is that assigning an external program call's output to a variable only captures its stdout output by default.

      • With a PowerShell-native command, it captures the command's success output stream - see about_redirection.
    • The primary fix is to use redirection 2>&1 to merge PowerShell's error stream (PowerShell's analog to stderr, to which stderr output is mapped if redirected) into PowerShell's success output stream (the stdout analog), which is then captured in the variable.
      This has two side effects, however:

      • Since PowerShell's error stream is now involved due to the 2>&1 redirection (by default, stderr output is passed through to the console), $? is invariably set to $False, because writing anything to the error stream does that (even if the error stream is ultimately sent elsewhere, as in this case).

        • Note that $? is not set to $False without the redirection (except in the ISE, which is an unfortunate discrepancy), because stderr output then never "touches" PowerShell's error output stream.
          In the PowerShell console, when calling an external program without a 2> redirection, $? is set to $False only is if its exit code is nonzero ($LASTEXITCODE -ne 0).

        • Therefore, use of 2>&1 solves one problem (inability capture output) while introducing another ($? incorrectly set to $False) - see below for a remedy.

      • It isn't strings that get written to the success output stream, but [System.Management.Automation.ErrorRecord] instances, because PowerShell wraps every stderr output line in one.

      • If you only use these instances in a string context - e.g., "Version: $output", you may not notice or care, however.

    • .ToString() (for a single output line) and .ForEach('ToString') / (...) | % ToString (for potentially multiple output lines) undo both side effects:

      • They convert the [System.Management.Automation.ErrorRecord] instances back to strings.

      • They reset $? to $True, because the .ToString() / .ForEach() calls / the use of (...) around the command are expressions, which are themselves considered successful, even if they contain commands that themselves set $? to $False.

    Note:

    • If executable python cannot be located, PowerShell itself will throw a statement-terminating error, which means that the assignment to $output will be skipped (its value, if any existed, will not change), and by default you get noisy error output and $? will be set to $False.

    • If python can be invoked and it reports an error (which is unlikely with --version), as indicated via nonzero exit code, you can inspect that exit code via the automatic $LASTEXITCODE variable (whose value won't change until the next call to an external program).

    • That something as simple as putting (...) around a command makes $? invariably return $True (except if a statement- or script-terminating error occurs) is taken advantage of in the above approaches, but is generally obscure behavior that could be considered problematic - see this discussion on GitHub.