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.