Jeff
Jeff

Reputation: 36563

PowerShell Invoke Command Line

PowerShell's process invocation is shaky and bug riddled at best, so when you need something that doesn't sporadically hang like Start-Process, or captures output to the pipeline, while still preserving $lastexitcode, most people seem to use Process/ProcessStartInfo. Some processes write a lot to the output or may be long running, so we don't want to wait until they finish to see the output stream (not necessarily the host...might be a log file). So I made this function

function Invoke-Cmd {
<# 
.SYNOPSIS 
Executes a command using cmd /c, throws on errors and captures all output. Writes error and info output to pipeline (so uses .NET process API).
#>
    [CmdletBinding()]
    param(
        [Parameter(Position=0,Mandatory=1)][string]$Cmd,
        [Parameter(Position=1,Mandatory=0)][ScriptBlock]$ErrorMessage = ({"Error executing command: $Cmd - Exit Code $($p.ExitCode)"}),
        [Parameter()][int[]]$ValidExitCodes = @(0)
    )
    begin {
        $p = New-Object System.Diagnostics.Process    
        $pi = New-Object System.Diagnostics.ProcessStartInfo
        $pi.FileName = "cmd.exe"
        $pi.Arguments = "/c $Cmd 2>&1"
        $pi.RedirectStandardError = $true
        $pi.RedirectStandardOutput = $true
        $pi.UseShellExecute = $false
        $pi.CreateNoWindow = $true        
        $p.StartInfo = $pi

        $outputHandler = {  
           if ($EventArgs.Data -ne $null) { Write-Output $EventArgs.Data } 
        }

        Write-Output "Executing..."

        $stdOutEvent = Register-ObjectEvent -InputObject $p `
            -Action $outputHandler -EventName 'OutputDataReceived'
        $stdErrEvent = Register-ObjectEvent -InputObject $p `
            -Action $outputHandler -EventName 'ErrorDataReceived'
    }
    process {
        $p.Start() | Out-Null

        $p.BeginOutputReadLine()
        $p.BeginErrorReadLine()

        $p.WaitForExit() | Out-Null

    }
    end {    
        Unregister-Event -SourceIdentifier $stdOutEvent.Name
        Unregister-Event -SourceIdentifier $stdErrEvent.Name

        if (!($ValidExitCodes -contains $p.ExitCode)) {
            throw (& $ErrorMessage)
        }
    }
}

The problem is that Write-Output in my event handler doesn't work in the same execution context as Invoke-Cmd itself... How can I get my event handler to Write-Output to the parent functions output stream?

Thank you

Upvotes: 2

Views: 265

Answers (1)

mklement0
mklement0

Reputation: 437090

This should do what you want:

cmd /c ... '2>&1' [ | ... ]

To capture the output in a variable:

$captured = cmd /c ... '2>&1' [ | ... ]

This idiom:

  • executes the external command passed to cmd /c synchronously,
  • passes both stdout and stderr through the pipeline, as output becomes available,
  • if specified, captures the combined output in variable $captured,
  • preserves the external command's exit code in $LASTEXITCODE.

$LASTEXITCODE is an automatic variable that is set automatically whenever an external program finishes running, and is set to that program's exit code. It's a global singleton variable you can access without scope specifier from any scope (as if it had been declared with -Scope Global -Option AllScope, even though Get-Variable LASTEXITCODE | Format-List doesn't reflect that). This implies that only the most recently run program's exit code is reflected in $LASTEXITCODE, irrespective of what scope it was run in.
Note, however, that the variable isn't created until the first external command launched in a session finishes running.

Caveat: Since it is cmd that is handling the 2&1 redirection (due to the quoting around it), stdout and stderr are merged at the source, so you won't be able to tell which output lines came from which stream; if you do need to know, use PowerShell's 2&1 redirection (omit the quoting).
For a discussion of how the two approaches differ, see this answer of mine.

If you want to output to the console in addition to capturing in a variable (assumes that the last pipeline segment calls a cmdlet:

cmd /c ... '2>&1' [ | ... -OutVariable captured ]

Example:

> cmd /c ver '&&' dir nosuch '2>&1' > out.txt
> $LASTEXITCODE
1
> Get-Content out.txt

Microsoft Windows [Version 10.0.10586]
 Volume in drive C has no label.
 Volume Serial Number is 7851-9F0B

 Directory of C:\

File Not Found

The example demonstrates that the exit code is correctly reflected in $LASTEXITCODE, and that both stdout and stderr were sent to the output stream and captured by PowerShell in file out.txt.

Upvotes: 1

Related Questions