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

28

我想从在Powershell脚本中启动的进程中捕获标准输出和错误输出,并异步地将其显示到控制台。我已经找到了一些有关通过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?

start-process -nonewwindow cmd '/c timeout 5 & echo hi' 这个怎么样? - js2010
给其他人的提示:下面的一些解决方案对我有用,但是进程输出并没有异步流式传输;相反,在进程完成后一次性打印出来。解决方案是不使用 $process.WaitForExit()。我不得不将这行代码替换为 while ( -Not $process.HasExited ) { sleep 1 },以获得相同的效果同时启用流式传输。 - Blaisem
7个回答

47
很遗憾,如果你想要正确地进行异步读取,那并不是一件容易的事情。如果你在调用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

1
很遗憾,运行此代码后我没有得到任何标准输出或标准错误。 - Ci3
我得到了返回的对象,其中StdOut、StdErr的值为null。退出代码是“0”。我期望ping.exe的输出有回复、字节、时间等信息。这样对吗?我按照你在这里提供的方式运行它。我正在运行Powershell 4。啊,我刚在Powershell 2上运行它,它按预期工作了! - Ci3
编辑了我的答案 - 添加了代码以删除和注销事件。现在应该适用于PS3.0 / PS4.0。 - Alexander Obersht
这在我的PS4上运行良好。在读取StringBuilder对象之前,您必须取消注册事件。 - Todd
我基于你的答案创建了另一个答案,但是使用任务而不是事件处理程序更加安全,参考 http://www.codeducky.org/process-handling-net/。 - Michael Freidgeim
显示剩余4条评论

14
根据Alexander Obersht的答案,我创建了一个函数,它使用超时和异步任务类来代替事件处理程序。根据Mike Adelson的说法

不幸的是,这种方法(事件处理程序)没有提供任何方式来知道何时收到了最后一点数据。由于所有内容都是异步的,事件有可能在WaitForExit()返回后触发(我已经观察到过这种情况)。

function Invoke-Executable {
# from https://dev59.com/JGAf5IYBdhLWcg3wskeQ#24371479
    # 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
}

3
谢谢分享!在PowerShell脚本中使用毫秒作为超时时间可能过于复杂。我无法想象有哪个脚本需要如此精确的计时,即使我能想象出来,我也不确定PS是否能够胜任。除此之外,这确实是一种更好的方法。在我深入了解异步操作在.NET中的工作方式之前,我编写了该函数,现在是时候进行审查并提高它的水平了。 - Alexander Obersht
1
你知道如何分割流吗?我想允许写入和捕获。这样进度可以写入控制台,以便用户可以实时查看正在发生的事情,并且输出可以被捕获,以便管道中的其他停止点可以处理它。 - Lucas
非常感谢,它帮助了我很多,现在我的PowerShell脚本和公司遇到的挂起进程问题得到了解决! - R13mus

5

我无法使用PS 4.0使这两个示例中的任何一个正常工作。

我想要从Octopus Deploy包(通过Deploy.ps1)运行puppet apply,并实时查看输出,而不是等待进程完成(一个小时后),因此我想出了以下解决方案:

# 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的贡献。


谢谢!唯一真正异步工作的示例。 为使其工作得更好,我还添加了以下内容: 对于kubectl和其他二进制文件中的非英文字母,使用非BOM输出process.StartInfo.StandardOutputEncoding = System.Text.Encoding.GetEncoding("UTF-8")。 在pwsh退出时杀死进程 - AppDomain.CurrentDomain.ProcessExit += (a, b) => process.Kill(); 在ctrl+c上杀死进程 - Console.CancelKeyPress += (a, b) => process.Kill(); - Anton Smolkov
@AntonSmolkov 我猜这个工作是异步的,因为 WaitForExit 方法在 dll 代码中。你可以通过将 WaitForExit 替换为 !$process.HasExited 和 sleep 的 while 循环,在纯 PS 中完成它。 - Blaisem

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

只有在本地调用进程时才能使用此方法。更可定制的调用方式是通过 Start-Process / [System.Diagnostics.Process],它会创建一个对象,当通过管道传输时不会返回进程输出。 - Blaisem

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'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接