Reputation: 53
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.
Upvotes: 1
Views: 3352
Reputation: 437278
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:
1
(success): -OutVariable
2
(error): -ErrorVariable
3
(warning): -WarningVariable
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'
)
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
.
Upvotes: 2
Reputation: 53
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.
Upvotes: 0