Search code examples
powershellerror-handlingio-redirectiontee

Redirect powershell output and errors to console (in real-time) and to a variable


I would like to redirect the output of a command in PowerShell, following these rules:

  • The command is stored to a variable

  • Output must be written to the console in real-time (i.e. "ping" results), including errors

  • Output must be stored to a variable, including errors (real-time is not mandatory here)

Here are my tests, assuming:

$command = "echo:"

to test errors redirection, and:

$command = "ping 127.0.0.1"

to test real-time output.

  1. Output is written in real-time, errors are not redirected at all

    Invoke-Expression $command 2>&1 | Tee-Object -Variable out_content
    
  2. Output is written in real-time, errors are only redirected to the console

    Invoke-Expression ($command 2>&1) | Tee-Object -Variable out_content
    Invoke-Expression $command | Tee-Object -Variable out_content 2>&1
    
  3. Output is not written in real-time, errors are correctly redirected to both

    (Invoke-Expression $command) 2>&1 | Tee-Object -Variable out_content
    

Is it possible to get those rules working together?


Solution

  • Some general recommendations up front:

    • Invoke-Expression should generally be avoided, because it can be a security risk and introduces quoting headaches; there are usually better and safer solutions available; best to form a habit of avoiding Invoke-Expression, unless there is no other solution.

    • There is never a reason to use Invoke-Expression to simply execute an external program with arguments, such as ping 127.0.0.1; just invoke it directly - support for such direct invocations is a core feature of any shell, and PowerShell is no exception.

    • If you do need to store a command in a variable or pass it as an argument for later invocation, use script blocks ({ ... }); e.g., instead of $command = 'ping 127.0.0.1', use $command = { ping 127.0.0.1 }, and invoke that script block on demand with either &, the call operator, or ., the dot-sourcing operator.
      When calling external programs, the two operators exhibit the same behavior; when calling PowerShell-native commands, & executes the code in a child scope, whereas . (typically) executes in the caller's current scope.


    That Invoke-Expression $command 2>&1 doesn't work as expected looks like a bug (as of PowerShell (Core) 7.4.x) and has been reported in GitHub issue #10476.

    As for a workaround for your problem:

    PetSerAl, as countless times before, has provided a solution in a comment on the question:

    & { Invoke-Expression $command } 2>&1 | Tee-Object -Variable out_content
    

    { ... } is a script-block literal that contains the Invoke-Expression call, and it is invoked with &, the call operator, which enables applying stream-redirection expression 2>&1 to the & call, which bypasses the bug.

    If $command contained a PowerShell-native command that you wanted to execute directly in the current scope, such as a function definition, you'd use . instead of &.

    Alternative:

    Include the 2>&1 redirection in the string to evaluate:

    Invoke-Expression "$command 2>&1" | Tee-Object -Variable out_content