如何在powershell中异步捕获进程输出?

问题描述 投票:0回答:7

我想从在 Powershell 脚本中启动的进程中捕获 stdout 和 stderr 并将其异步显示到控制台。我通过 MSDN 和其他博客找到了一些有关执行此操作的文档。

创建并运行下面的示例后,我似乎无法异步显示任何输出。所有输出仅在进程终止时显示。

$ps = new-object System.Diagnostics.Process
$ps.StartInfo.Filename = "cmd.exe"
$ps.StartInfo.UseShellExecute = $false
$ps.StartInfo.RedirectStandardOutput = $true
$ps.StartInfo.Arguments = "/c echo `"hi`" `& timeout 5"

$action = { Write-Host $EventArgs.Data  }
Register-ObjectEvent -InputObject $ps -EventName OutputDataReceived -Action $action | Out-Null

$ps.start() | Out-Null
$ps.BeginOutputReadLine()
$ps.WaitForExit()

在这个例子中,我期望在程序执行结束之前在命令行上看到“hi”的输出,因为 OutputDataReceived 事件应该已经被触发。

我已经尝试过使用其他可执行文件 - java.exe、git.exe 等。它们都具有相同的效果,所以我认为有一些简单的东西我不理解或错过了。异步读取 stdout 还需要做什么?

events powershell asynchronous
7个回答
46
投票

不幸的是,如果你想正确地进行异步读取,那么异步读取并不那么容易。如果您在没有超时的情况下调用 WaitForExit(),您可以使用我编写的类似函数(基于 C# 代码):

function Invoke-Executable {
    # Runs the specified executable and captures its exit code, stdout
    # and stderr.
    # Returns: custom object.
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String]$sExeFile,
        [Parameter(Mandatory=$false)]
        [String[]]$cArgs,
        [Parameter(Mandatory=$false)]
        [String]$sVerb
    )

    # Setting process invocation parameters.
    $oPsi = New-Object -TypeName System.Diagnostics.ProcessStartInfo
    $oPsi.CreateNoWindow = $true
    $oPsi.UseShellExecute = $false
    $oPsi.RedirectStandardOutput = $true
    $oPsi.RedirectStandardError = $true
    $oPsi.FileName = $sExeFile
    if (! [String]::IsNullOrEmpty($cArgs)) {
        $oPsi.Arguments = $cArgs
    }
    if (! [String]::IsNullOrEmpty($sVerb)) {
        $oPsi.Verb = $sVerb
    }

    # Creating process object.
    $oProcess = New-Object -TypeName System.Diagnostics.Process
    $oProcess.StartInfo = $oPsi

    # Creating string builders to store stdout and stderr.
    $oStdOutBuilder = New-Object -TypeName System.Text.StringBuilder
    $oStdErrBuilder = New-Object -TypeName System.Text.StringBuilder

    # Adding event handers for stdout and stderr.
    $sScripBlock = {
        if (! [String]::IsNullOrEmpty($EventArgs.Data)) {
            $Event.MessageData.AppendLine($EventArgs.Data)
        }
    }
    $oStdOutEvent = Register-ObjectEvent -InputObject $oProcess `
        -Action $sScripBlock -EventName 'OutputDataReceived' `
        -MessageData $oStdOutBuilder
    $oStdErrEvent = Register-ObjectEvent -InputObject $oProcess `
        -Action $sScripBlock -EventName 'ErrorDataReceived' `
        -MessageData $oStdErrBuilder

    # Starting process.
    [Void]$oProcess.Start()
    $oProcess.BeginOutputReadLine()
    $oProcess.BeginErrorReadLine()
    [Void]$oProcess.WaitForExit()

    # Unregistering events to retrieve process output.
    Unregister-Event -SourceIdentifier $oStdOutEvent.Name
    Unregister-Event -SourceIdentifier $oStdErrEvent.Name

    $oResult = New-Object -TypeName PSObject -Property ([Ordered]@{
        "ExeFile"  = $sExeFile;
        "Args"     = $cArgs -join " ";
        "ExitCode" = $oProcess.ExitCode;
        "StdOut"   = $oStdOutBuilder.ToString().Trim();
        "StdErr"   = $oStdErrBuilder.ToString().Trim()
    })

    return $oResult
}

它捕获标准输出、标准错误和退出代码。用法示例:

$oResult = Invoke-Executable -sExeFile 'ping.exe' -cArgs @('8.8.8.8', '-a')
$oResult | Format-List -Force 

有关更多信息和替代实现(C# 语言),请通过 archive.orgarchive.is 阅读此博客文章。


14
投票

基于 Alexander Obersht 的回答 我创建了一个使用超时和异步任务类而不是事件处理程序的函数。 根据迈克阿德尔森

不幸的是,这个方法(事件处理程序)无法提供知道 当最后一位数据被接收时。因为一切都是 异步,事件有可能(并且我已经观察到这一点) WaitForExit() 返回后触发。

function Invoke-Executable {
# from https://stackoverflow.com/a/24371479/52277
    # Runs the specified executable and captures its exit code, stdout
    # and stderr.
    # Returns: custom object.
# from http://www.codeducky.org/process-handling-net/ added timeout, using tasks
param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String]$sExeFile,
        [Parameter(Mandatory=$false)]
        [String[]]$cArgs,
        [Parameter(Mandatory=$false)]
        [String]$sVerb,
        [Parameter(Mandatory=$false)]
        [Int]$TimeoutMilliseconds=1800000 #30min
    )
    Write-Host $sExeFile $cArgs

    # Setting process invocation parameters.
    $oPsi = New-Object -TypeName System.Diagnostics.ProcessStartInfo
    $oPsi.CreateNoWindow = $true
    $oPsi.UseShellExecute = $false
    $oPsi.RedirectStandardOutput = $true
    $oPsi.RedirectStandardError = $true
    $oPsi.FileName = $sExeFile
    if (! [String]::IsNullOrEmpty($cArgs)) {
        $oPsi.Arguments = $cArgs
    }
    if (! [String]::IsNullOrEmpty($sVerb)) {
        $oPsi.Verb = $sVerb
    }

    # Creating process object.
    $oProcess = New-Object -TypeName System.Diagnostics.Process
    $oProcess.StartInfo = $oPsi


    # Starting process.
    [Void]$oProcess.Start()
# Tasks used based on http://www.codeducky.org/process-handling-net/    
 $outTask = $oProcess.StandardOutput.ReadToEndAsync();
 $errTask = $oProcess.StandardError.ReadToEndAsync();
 $bRet=$oProcess.WaitForExit($TimeoutMilliseconds)
    if (-Not $bRet)
    {
     $oProcess.Kill();
    #  throw [System.TimeoutException] ($sExeFile + " was killed due to timeout after " + ($TimeoutMilliseconds/1000) + " sec ") 
    }
    $outText = $outTask.Result;
    $errText = $errTask.Result;
    if (-Not $bRet)
    {
        $errText =$errText + ($sExeFile + " was killed due to timeout after " + ($TimeoutMilliseconds/1000) + " sec ") 
    }
    $oResult = New-Object -TypeName PSObject -Property ([Ordered]@{
        "ExeFile"  = $sExeFile;
        "Args"     = $cArgs -join " ";
        "ExitCode" = $oProcess.ExitCode;
        "StdOut"   = $outText;
        "StdErr"   = $errText
    })

    return $oResult
}

4
投票

我无法让这些示例与 PS 4.0 一起使用。

我想从 Octopus Deploy 包运行

puppet apply
(通过
Deploy.ps1
)并“实时”查看输出,而不是等待该过程完成(一个小时后),所以我想出了以下方法:

# Deploy.ps1

$procTools = @"

using System;
using System.Diagnostics;

namespace Proc.Tools
{
  public static class exec
  {
    public static int runCommand(string executable, string args = "", string cwd = "", string verb = "runas") {

      //* Create your Process
      Process process = new Process();
      process.StartInfo.FileName = executable;
      process.StartInfo.UseShellExecute = false;
      process.StartInfo.CreateNoWindow = true;
      process.StartInfo.RedirectStandardOutput = true;
      process.StartInfo.RedirectStandardError = true;

      //* Optional process configuration
      if (!String.IsNullOrEmpty(args)) { process.StartInfo.Arguments = args; }
      if (!String.IsNullOrEmpty(cwd)) { process.StartInfo.WorkingDirectory = cwd; }
      if (!String.IsNullOrEmpty(verb)) { process.StartInfo.Verb = verb; }

      //* Set your output and error (asynchronous) handlers
      process.OutputDataReceived += new DataReceivedEventHandler(OutputHandler);
      process.ErrorDataReceived += new DataReceivedEventHandler(OutputHandler);

      //* Start process and handlers
      process.Start();
      process.BeginOutputReadLine();
      process.BeginErrorReadLine();
      process.WaitForExit();

      //* Return the commands exit code
      return process.ExitCode;
    }
    public static void OutputHandler(object sendingProcess, DataReceivedEventArgs outLine) {
      //* Do your stuff with the output (write to console/log/StringBuilder)
      Console.WriteLine(outLine.Data);
    }
  }
}
"@

Add-Type -TypeDefinition $procTools -Language CSharp

$puppetApplyRc = [Proc.Tools.exec]::runCommand("ruby", "-S -- puppet apply --test --color false ./manifests/site.pp", "C:\ProgramData\PuppetLabs\code\environments\production");

if ( $puppetApplyRc -eq 0 ) {
  Write-Host "The run succeeded with no changes or failures; the system was already in the desired state."
} elseif ( $puppetApplyRc -eq 1 ) {
  throw "The run failed; halt"
} elseif ( $puppetApplyRc -eq 2) {
  Write-Host "The run succeeded, and some resources were changed."
} elseif ( $puppetApplyRc -eq 4 ) {
  Write-Warning "WARNING: The run succeeded, and some resources failed."
} elseif ( $puppetApplyRc -eq 6 ) {
  Write-Warning "WARNING: The run succeeded, and included both changes and failures."
} else {
  throw "Un-recognised return code RC: $puppetApplyRc"
}

归功于 T30Stefan Goßner


2
投票

这里的示例都很有用,但并不完全适合我的用例。我不想调用命令并退出。我想打开命令提示符,发送输入,读取输出,然后重复。这是我的解决方案。

创建Utils.CmdManager.cs

using System;
using System.Diagnostics;
using System.Text;
using System.Threading;

namespace Utils
{
    public class CmdManager : IDisposable
    {
        const int DEFAULT_WAIT_CHECK_TIME = 100;
        const int DEFAULT_COMMAND_TIMEOUT = 3000;

        public int WaitTime { get; set; }
        public int CommandTimeout { get; set; }

        Process _process;
        StringBuilder output;

        public CmdManager() : this("cmd.exe", null, null) { }
        public CmdManager(string filename) : this(filename, null, null) { }
        public CmdManager(string filename, string arguments) : this(filename, arguments, null) { }

        public CmdManager(string filename, string arguments, string verb)
        {
            WaitTime = DEFAULT_WAIT_CHECK_TIME;
            CommandTimeout = DEFAULT_COMMAND_TIMEOUT;

            output = new StringBuilder();

            _process = new Process();
            _process.StartInfo.FileName = filename;
            _process.StartInfo.RedirectStandardInput = true;
            _process.StartInfo.RedirectStandardOutput = true;
            _process.StartInfo.RedirectStandardError = true;
            _process.StartInfo.CreateNoWindow = true;
            _process.StartInfo.UseShellExecute = false;
            _process.StartInfo.ErrorDialog = false;
            _process.StartInfo.Arguments = arguments != null ? arguments : null;
            _process.StartInfo.Verb = verb != null ? verb : null;

            _process.EnableRaisingEvents = true;
            _process.OutputDataReceived += (s, e) =>
            {
                lock (output)
                {
                    output.AppendLine(e.Data);
                };
            };
            _process.ErrorDataReceived += (s, e) =>
            {
                lock (output)
                {
                    output.AppendLine(e.Data);
                };
            };

            _process.Start();
            _process.BeginOutputReadLine();
            _process.BeginErrorReadLine();
            _process.StandardInput.AutoFlush = true;
        }

        public void RunCommand(string command)
        {
            _process.StandardInput.WriteLine(command);
        }

        public string GetOutput()
        {
            return GetOutput(null, CommandTimeout, WaitTime);
        }

        public string GetOutput(string endingOutput)
        {
            return GetOutput(endingOutput, CommandTimeout, WaitTime);
        }

        public string GetOutput(string endingOutput, int commandTimeout)
        {
            return GetOutput(endingOutput, commandTimeout, WaitTime);
        }

        public string GetOutput(string endingOutput, int commandTimeout, int waitTime)
        {
            string tempOutput = "";
            int tempOutputLength = 0;
            int amountOfTimeSlept = 0;

            // Loop until
            //  a) command timeout is reached
            //  b) some output is seen
            while (output.ToString() == "")
            {
                if (amountOfTimeSlept >= commandTimeout)
                {
                    break;
                }

                Thread.Sleep(waitTime);
                amountOfTimeSlept += waitTime;
            }

            // Loop until:
            //  a) command timeout is reached
            //  b) endingOutput is found
            //  c) OR endingOutput is null and there is no new output for at least waitTime
            while (amountOfTimeSlept < commandTimeout)
            {
                if (endingOutput != null && output.ToString().Contains(endingOutput))
                {
                    break;
                }
                else if(endingOutput == null && tempOutputLength == output.ToString().Length)
                {
                    break;
                }

                tempOutputLength = output.ToString().Length;

                Thread.Sleep(waitTime);
                amountOfTimeSlept += waitTime;
            }

            // Return the output and clear the buffer
            lock (output)
            {
                tempOutput = output.ToString();
                output.Clear();
                return tempOutput.TrimEnd();
            }
        }

        public void Dispose()
        {
            _process.Kill();
        }
    }
}

然后从 PowerShell 添加该类并使用它。

Add-Type -Path ".\Utils.CmdManager.cs"

$cmd = new-object Utils.CmdManager
$cmd.GetOutput() | Out-Null

$cmd.RunCommand("whoami")
$cmd.GetOutput()

$cmd.RunCommand("cd")
$cmd.GetOutput()

$cmd.RunCommand("dir")
$cmd.GetOutput()

$cmd.RunCommand("cd Desktop")
$cmd.GetOutput()

$cmd.RunCommand("cd")
$cmd.GetOutput()

$cmd.RunCommand("dir")
$cmd.GetOutput()

$cmd.Dispose()

不要忘记最后调用

Dispose()
函数来清理后台运行的进程。或者,您可以通过运行类似
$cmd.RunCommand("exit")

的命令来关闭该进程

1
投票

我来到这里寻找一种解决方案来创建一个记录进程并将其输出到屏幕的包装器。这些都不适合我。我编写了这段代码,看起来效果很好。

PSDataCollection 允许您继续执行脚本,而无需等待进程完成。

Using namespace System.Diagnostics;
Using namespace System.Management.Automation;

$Global:Dir = Convert-Path "."
$Global:LogPath = "$global:Dir\logs\mylog.log"
[Process]$Process = [Process]::New();
[ProcessStartInfo]$info = [ProcessStartInfo]::New();
$info.UseShellExecute = $false
$info.Verb = "runas"
$info.WorkingDirectory = "$Global:Dir\process.exe"
$info.FileName = "$Global:Dir\folder\process.exe"
$info.Arguments = "-myarg yes -another_arg no"
$info.RedirectStandardOutput = $true
$info.RedirectStandardError  = $true
$Process.StartInfo = $info;
$Process.EnableRaisingEvents = $true
$Global:DataStream = [PSDataCollection[string]]::New()
$Global:DataStream.add_DataAdded(
    {
        $line = $this[0];
        [IO.File]::AppendAllLines($LogPath, [string[]]$line);
        [Console]::WriteLine($line)
        $this.Remove($line);
    }
)
$script = {
    param([Object]$sender, [DataReceivedEventArgs]$e) 
    $global:Datastream.Add($e.Data)
}
Register-ObjectEvent -InputObject $Process -Action $script -EventName 'OutputDataReceived' | Out-Null
Register-ObjectEvent -InputObject $Process -Action $script -EventName 'ErrorDataReceived' | Out-Null
$Process.Start()
$Process.BeginOutputReadLine()
$Process.BeginErrorReadLine()

0
投票

如果您只想将其动态转储到 PowerShell 控制台,请执行以下操作:

my.exe | Out-Default

我不能声称已经弄清楚了。

请参阅此技术网帖子的底部:https://social.technet.microsoft.com/Forums/windowsserver/en-US/b6691fba-0e92-4e9d-aec2-47f3d5a17419/start-process-and-redirect-output- to-powershell-window?forum=winserverpowershell

这也指的是 this stackoverflow post

$LASTEXITCODE 还填充了我的 exe 的退出代码,这也是我所需要的。


0
投票

我遇到了这个帖子,并想为将来可能需要这个的人分享我的解决方案。这适用于 PowerShell Core 7.3.4。

<#
.Synopsis
    This function will run a provided command and arguments.
.DESCRIPTION
    This function was created due to the inconsistencies of running Start-Process in Linux. This function provides a 
    consistent way of running non-PowerShell commands that require many parameters/arguments to run (e.g., docker).
    
    PowerShell commands or aliases will NOT work with this function. For example commands such as: echo, history, or cp
    will NOT work. Use the build-in PowerShell commands for those.
.PARAMETER Name
    The path or name of the command to be ran.
.PARAMETER Arguments
    The optional parameters/arguments to be added with your command.
.PARAMETER WorkingDirectory
    The current WorkingDirectory to run said Command. If you are not using the full path to files, you should probably
    use this parameter. 
.PARAMETER LoadUserProfile
    Gets or sets a value that indicates whether the Windows user profile is to be loaded from the registry.

    This will NOT work on Unix/Linux.
.PARAMETER Timer
    Provide a timer (in ms) for how long you want to wait for the process to exit/end.
.PARAMETER Verb
    Specifies a verb to use when this cmdlet starts the process. The verbs that are available are determined by the filename extension of the file that runs in the process.

    The following table shows the verbs for some common process file types.

    File type   Verbs
    .cmd    Edit, Open, Print, RunAs, RunAsUser
    .exe    Open, RunAs, RunAsUser
    .txt    Open, Print, PrintTo
    .wav    Open, Play
    To find the verbs that can be used with the file that runs in a process, use the New-Object cmdlet to create a System.Diagnostics.ProcessStartInfo object for the file. The available verbs are in the Verbs property of the ProcessStartInfo object. For details, see the examples.

    This will NOT work on Unix/Linux.
.PARAMETER Passthru
    Pass the object into the pipeline. Using -Passthru will ignore error-handling.
.NOTES
    Author - Zack Flowers
.LINK
    GitHub: https://github.com/zackshomelab
.EXAMPLE
    Start-Command -Name 'docker' -CommandArguments "container ls --all"
    
    Example #1:
    This example executes command 'docker' and passes arguments 'container ls --all' to display the offline/online containers.
.EXAMPLE
    Start-Command -Name 'docker' -CommandArguments "container", "ls", "--all"

    Example #2:
    This example is simular to Example #1, except it accepts comma-separated arguments.
.EXAMPLE
    $whoami = Start-Command -Name 'whoami' -Passthru

    $whoami

    Title        : whoami
    OutputStream : System.Management.Automation.PSEventJob
    OutputData   : zac
    ErrorStream  : 
    ErrorData    : 
    ExitCode     : 0

    Example #3:
    This example utilizes the -Passthru feature of this script.
.INPUTS
    None
.OUTPUTS
    System.String
    System.Management.Automation.PSCustomObject
#>
function Start-Command {
    [cmdletbinding(DefaultParameterSetName="default")]
    param (
        [parameter(Mandatory,
            Position=0,
            ValueFromPipelineByPropertyName)]
            [ValidateNotNullOrEmpty()]
        [string]$Name,

        [parameter(Mandatory=$false,
            Position=1,
            ValueFromPipelineByPropertyName)]
            [ValidateNotNullOrEmpty()]
        [object]$Arguments,

        [parameter(Mandatory=$false,
            ValueFromPipelineByPropertyName)]
            [ValidateScript({Test-Path $_})]
        [string]$WorkingDirectory,

        [parameter(Mandatory=$false)]
            [ValidateScript({
                if ($PSVersionTable.Platform -eq "Unix") {
                    Throw "-LoadUserProfile cannot be used on Unix/Linux."
                }
            })]
        [switch]$LoadUserProfile,

        [parameter(Mandatory,
            ValueFromPipelineByPropertyName,
            ParameterSetName="timer")]
            [ValidateRange(1, 600000)]
        [int]$Timer,

        [parameter(Mandatory=$false,
            ValueFromPipelineByPropertyName)]
            [ValidateScript({
                if ($PSVersionTable.Platform -eq "Unix") {
                    Throw "-Verb cannot be used on Unix/Linux."
                }
            })]
        [string]$Verb,

        [parameter(Mandatory=$false)]
        [switch]$Passthru
    )

    begin {
        $FileName = (Get-Command -Name $Name -ErrorAction SilentlyContinue).Source

        # If we cannot find the provided FileName, this could be due to the user providing..
        # ..a command that is a PowerShell Alias (e.g., echo, history, cp)
        if ($null -eq $FileName -or $FileName -eq "") {
            
            # Source doesn't exist. Let's see if the provided command is a PowerShell command
            $getPSCommand = (Get-Command -Name $Name -ErrorAction SilentlyContinue)

            if ($null -eq $getPSCommand -or $getPSCommand -eq "") {
                Throw "Start-Command: Could not find command $Name nor could we find its PowerShell equivalent."
            }

            # Stop the script if the command was found but it returned an alias. 
            # Sometimes, a command may not return a source but WILL return an alias. This will cause issues with incompatibility with..
            # ..parameters for said commands.
            #
            # Example commands that will not work: echo, history, and cd
            if ($getPSCommand.CommandType -eq 'Alias') {
                Throw "Start-Command: This function does not support Aliases. Command $Name matches $($getPSCommand.ResolvedCommand.Name)."
            }

            # This function does not support Microsoft PowerShell commands.
            if ($getPSCommand.Source -like "Microsoft.PowerShell*") {
                Throw "Start-Command: This function should only be used for Non-PowerShell commands (e.g., wget, touch, mkdir, etc.)"
            }

            # Retrieve the version of PowerShell and its location and replace $FileName with it
            $FileName = $PSVersionTable.PSEdition -eq 'Core' ? (Get-Command -Name 'pwsh').Source : (Get-Command -Name 'powershell').Source
            
            # Reconfigure Arguments to execute PowerShell
            $Arguments = "-noprofile -Command `"& {$($getPSCommand.ReferencedCommand.Name) $Arguments}`""
        }

        # Data Object will store all streams of data from our command
        $dataObject = [pscustomobject]@{
            Title        = $Name
            OutputStream = ''
            OutputData   = ''
            ErrorData    = ''
            ExitCode     = 0
        }
    }
    process {

        $processStartInfoProps = @{
            Arguments               = $null -ne $Arguments ? $Arguments : $null
            CreateNoWindow          = $true
            ErrorDialog             = $false
            FileName                = $FileName
            RedirectStandardError   = $true
            RedirectStandardInput   = $true
            RedirectStandardOutput  = $true
            UseShellExecute         = $false
            WindowStyle             = [System.Diagnostics.ProcessWindowStyle]::Hidden
            WorkingDirectory        = $PSBoundParameters.ContainsKey('WorkingDirectory') ? $WorkingDirectory : $PSScriptRoot
            Verb                    = $PSBoundParameters.ContainsKey('Verb') ? $Verb : $null
        }

        # This will Error on Unix/Linux Systems if property LoadUserProfile is added regardless if it's null or false.
        if ($PSBoundParameters.ContainsKey('LoadUserProfile')) {
            $processStartInfoProps.Add('LoadUserProfile', $LoadUserProfile)
        }

        try {

            $process = New-Object System.Diagnostics.Process
            $process.EnableRaisingEvents = $true

            $processStartInfo = New-Object System.Diagnostics.ProcessStartInfo -Property $processStartInfoProps
            $process.StartInfo = $processStartInfo

            # Register Process OutputDataReceived:
            #   This will create a background job to capture output data
            #   Reference: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.standardoutput?redirectedfrom=MSDN&view=net-7.0#System_Diagnostics_Process_StandardOutput
            $outputEventParams = @{
                InputObject = $process
                SourceIdentifier = 'OnOutputDataReceived '
                EventName = 'OutputDataReceived'
                Action = {
                    param (
                        [System.Object]$sender,
                        [System.Diagnostics.DataReceivedEventArgs]$e
                    )

                    foreach ($data in $e.Data) { 
                        if ($null -ne $data -and $data -ne "") { 
                            $($data).Trim()
                        } 
                    }
                }
            }
            $dataObject.OutputStream = Register-ObjectEvent @outputEventParams

            # Start the process/command
            if ($process.Start()) {
                $process.BeginOutputReadLine()
                $dataObject.ErrorData = $process.StandardError.ReadToEnd()

                if ($PSCmdlet.ParameterSetName -eq 'timer') {
                    $process.WaitForExit($Timer) | Out-Null
                } else {
                    $process.WaitForExit()
                }
            }
            
            # Retrieve the exit code and the OutputStream Job
            $dataObject.ExitCode = $process.ExitCode
            $dataObject.OutputData = Receive-Job -id $($dataObject.OutputStream.id)

            [bool]$hasError = ($null -ne $($dataObject.ErrorData) -and $($dataObject.ErrorData) -ne "" -and $($dataObject.ExitCode) -ne 0) ? $true : $false
            [bool]$hasOutput = ($null -ne $($dataObject.OutputData) -and $($dataObject.OutputData) -ne "") ? $true : $false

            # Output the PSCustomObject if -Passthru is provided.
            if ($Passthru) {
                if ($hasError) {
                    $dataObject.ErrorData = $($dataObject.ErrorData.Trim())
                }
                $dataObject
            } else {

                if ($hasError) {
                    if ($($ErrorActionPreference) -ne 'Stop') {
                        Write-Error "Exit Code $($dataObject.ExitCode): $($dataObject.ErrorData.Trim())"
                    } else {
                        Throw "Exit Code $($dataObject.ExitCode): $($dataObject.ErrorData.Trim())"
                    }
                }

                if ($hasOutput) {
                    $($dataObject.OutputData)
                }
            }
        }
        finally {

            # Cleanup
            $process.Close()
            Unregister-Event -SourceIdentifier $($dataObject.OutputStream.Name) -Force | Out-Null
            Remove-Job -Id $($dataObject.OutputStream.Id) -Force
        }
    }
}

示例1:常规用法

Start-Command -Name 'docker' -Arguments 'container ls --all'

示例 2:逗号分隔的参数

Start-Command -Name 'docker' -Arguments 'container', 'ls', '--all'

示例 3:Passthru 用法

$whoami = Start-Command -Name 'whoami' -Passthru

$whoami

Title        : whoami
OutputStream : System.Management.Automation.PSEventJob
OutputData   : zac
ErrorStream  : 
ErrorData    : 
ExitCode     : 0

示例 4:错误示例

Start-Command -Name 'docker' -Arguments 'force' -ErrorAction Stop

Output: 
Line |
 245 |  …             Throw "Exit Code $($dataObject.ExitCode): $($dataObject.E …
     |                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Exit Code 1: docker: 'force' is not a docker command. See 'docker --help'
© www.soinside.com 2019 - 2024. All rights reserved.