1

As I didn't find a solution by searching the forum and spent some time for finding out how to do it properly, I'm placing here the issue along with the working solution.

Scenario: in Powershell, need to remotely execute a script block stored in a variable and capture its output for further processing. No output should appear on the screen unless the script generates it on purpose. The script block can contain Write-Warning commands.

0

2 Answers 2

2

Note that the behaviors of interest apply generally to PowerShell commands, not just in the context of Invoke-Command and the - generally to be avoided - Invoke-Expression; in your case, it is only needed to work around a bug.[1]


Your own answer shows how to redirect a single, specific output streams to the success output stream; e.g, 3>&1 redirects (>&) the warning stream (3) to the success (output) stream (1).

The & indicates that the redirection target is a stream, as opposed to a file; for more information about PowerShell's output stream, see about_Redirection.

If you want to redirect all output streams to the success output stream, use redirection *>&1

By redirecting all streams to the output stream, their combined output can be captured in a variable, redirected to a file, or sent through the pipeline, whereas by default only the success output stream (1) is captured.

Separately, you can use the common parameters named -*Variable parameters to capture individual stream output in variables for some streams, namely:

  • Stream 1 (success): -OutVariable
  • Stream 2 (error): -ErrorVariable
  • Stream 3 (warning): -WarningVariable
  • Stream 6 (information): -InformationVariable

Be sure to specify the target variable by name only, without the $ prefix; e.g., to capture warnings in variable $warnings, use
-WarningVariable warnings, such as in the following example:
Write-Warning hi -WarningVariable warnings; "warnings: $warnings"

Note that with -*Variable, the stream output is collected in the variable whether or not you silence or even ignore that stream otherwise, with the notable exception of -ErrorAction Ignore, in which case an -ErrorVariable variable is not populated (and the error is also not recorded in the automatic $Error variable that otherwise records all errors that occur in the session).

Generally, -{StreamName}Action SilentlyIgnore seems to be equivalent to {StreamNumber}>$null.


Note the absence of the verbose (4) and the debug (5) streams above; you can only capture them indirectly, via 4>&1 and 5>&1 (or *>&1), which then requires you to extract the output of interest from the combined stream, via filtering by output-object type:

Important:

  • The verbose (4) and debug (5) streams are the only two streams that are silent at the source by default; that is, unless these streams are explicitly turned on via -Verbose / -Debug or their preference-variable equivalents, $VerbosePreference = 'Continue' / $DebugPreference = 'Continue', nothing is emitted and nothing can be captured.

  • The information stream (5) is silent only on output by default; that is, writing to the information stream (with Write-Information) always writes objects to the stream, but they're not displayed by default (they're only displayed with -InformationAction Continue / $InformationPreference = 'Continue')

    • Since v5, Write-Host now too writes to the information stream, though its output does print by default, but can be suppressed with 6>$null or -InformationAction Ignore (but not -InformationAction SilentlyContinue).
# Sample function that produces success and verbose output.
# Note that -Verbose is required for the message to actually be emitted.
function foo { Write-Output 1; Write-Verbose -Verbose 4 }

# Get combined output, via 4>&1
$combinedOut = foo 4>&1

# Extract the verbose-stream output records (objects).
# For the debug output stream (5), the object type is
# [System.Management.Automation.DebugRecord]
$verboseOut = $combinedOut.Where({ $_ -is [System.Management.Automation.VerboseRecord] })

[1] Stream-capturing bug, as of PowerShell v7.0:

In a nutshell: In the context of remoting (such as Invoke-Command -Session here), background jobs, and so-called minishells (passing a script block to the PowerShell CLI to execute commands in a child process), only the success (1) and error (2) streams can be captured as expected; all other are unexpectedly passed through to the host (display) - see GitHub issue #9585.

Your command should - but currently doesn't - work as follows, which would obviate the need for Invoke-Expression:

# !! 3>&1 redirection is BROKEN as of PowerShell 7.0, if *remoting* is involved
# !! (parameters -Session or -ComputerName).
$RemoteOutput = 
  Invoke-Command -Session $Session $Commands 3>&1 -ErrorVariable RemoteError 2>$null

That is, in principle you should be able to pass a $Commands variable that contains a script block directly as the (implied) -ScriptBlock argument to Invoke-Command.

Sign up to request clarification or add additional context in comments.

4 Comments

thank you for the detailed explanation but, as you mentioned itself, my post is related to resolving only this specific situation where remote output of Invoke-Expression can't be captured as expected. Regarding your comment "generally to be avoided - Invoke-Expression". I'm aware about that but it seems to be an overstatement. I know the reason behind that recommendation but I don't consider it to be too important. In the end, correctness of your code is a matter of testing in any case.
In case you're interested in the global task, it's about mass-executing a code block from an external file. The content of the file is not known in advance. Executing the commands directly by using the parameter of Invoke-Command doesn't allow to capture remote output properly and sometimes simply doesn't work throwing remote execution errors.
Thanks, @Evgeny. Yes, the direct capturing of streams from a remote invocation is buggy, as the footnote hopefully makes clear; I'm not aware of other errors, so I can't speak to your statement about throwing remote execution errors.
As for warning against Invoke-Expression use being an overstatement: It is the sensible recommendation by a core member of the PowerShell team and there is an ongoing discussion on how to best frame this recommendation in the official documentation. Note the word generally: the point is that while there may be legitimate uses (certainly, your workaround for the stream-capturing bug is one), there are typically better and safer solutions
0

Script block is contained in $Commands variable. $Session is an already established Powershell remoting session.

The task is resolved by the below command:

$RemoteOutput = 
  Invoke-Command -Session $Session {
    Invoke-Expression $Using:Commands 3>&1
  } -ErrorVariable RemoteError 2>$null

After the command is executed all output of the script block is contained in $RemoteOutput. Errors generated during remote code execution are placed in $RemoteError.

Additional clarifications. Write-Warning in Invoke-Expression code block generates its own output stream that is not captured by Invoke-Command. The only way to capture it in a variable is to redirect that stream to the standard stream of Invoke-Expression by using 3>&1. Commands in the code block writing to other output streams (verbose, debug) seems not to be captured even by adding 4>&1 and 5>&1 parameters to Invoke-Expression. However, stream #2 (errors) is properly captured by Invoke-Command in the way shown above.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.