用 Start-Process 捕获标准输出和错误

PowerShell 的 Start-Process命令在访问 StandardErrorStandardOutput属性时是否存在 bug?

如果我运行以下命令,将不会得到任何输出:

$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait
$process.StandardOutput
$process.StandardError

但是如果我将输出重定向到一个文件,就会得到预期的结果:

$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait -RedirectStandardOutput stdout.txt -RedirectStandardError stderr.txt
254220 次浏览

这就是为什么 Start-Process被设计出来的原因。这里有一个不需要发送到文件就可以得到它的方法:

$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "ping.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "localhost"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "exit code: " + $p.ExitCode

在问题中给出的代码中,我认为应该读取启动变量的 ExitCode 属性。

$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait
$process.ExitCode

请注意(就像在您的示例中一样)您需要添加 -PassThru-Wait参数(这让我有一段时间不知所措)。

我也遇到过这个问题,最后使用 安迪的密码创建了一个函数,在需要运行多个命令时清理这些东西。

它将返回 stderr、 stdout 和作为对象的退出代码。需要注意的一点是: 函数不接受路径中的 .\; 必须使用完整路径。

Function Execute-Command ($commandTitle, $commandPath, $commandArguments)
{
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $commandPath
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = $commandArguments
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
[pscustomobject]@{
commandTitle = $commandTitle
stdout = $p.StandardOutput.ReadToEnd()
stderr = $p.StandardError.ReadToEnd()
ExitCode = $p.ExitCode
}
}

以下是使用方法:

$DisableACMonitorTimeOut = Execute-Command -commandTitle "Disable Monitor Timeout" -commandPath "C:\Windows\System32\powercfg.exe" -commandArguments " -x monitor-timeout-ac 0"

我对那些例子 来自安迪・阿里斯门迪从液化石油气确实有些困难。你应该经常使用:

$stdout = $p.StandardOutput.ReadToEnd()

在打电话之前

$p.WaitForExit()

一个完整的例子是:

$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "ping.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "localhost"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
$p.WaitForExit()
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "exit code: " + $p.ExitCode

重要提示:

我们一直在使用上面提供的函数 石油气

但是,当您启动一个生成大量输出的进程时,可能会遇到一个 bug。由于这个原因,在使用这个函数时可能会出现死锁。相反,请使用下面的改编版本:

Function Execute-Command ($commandTitle, $commandPath, $commandArguments)
{
Try {
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $commandPath
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = $commandArguments
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
[pscustomobject]@{
commandTitle = $commandTitle
stdout = $p.StandardOutput.ReadToEnd()
stderr = $p.StandardError.ReadToEnd()
ExitCode = $p.ExitCode
}
$p.WaitForExit()
}
Catch {
exit
}
}

关于这一问题的进一步资料可参阅 在 MSDN:

如果父进程在 p.StandardError 之前调用 p.WaitForExit,则可能导致死锁条件。ReadToEnd 和子进程写入足够的文本以填充重定向流。父进程将无限期地等待子进程退出。子进程将无限期地等待父进程从完整的 StandardError 流中读取。

下面是我的函数版本,它返回标准的 System.Diagnostics.Process,带有3个新属性

Function Execute-Command ($commandTitle, $commandPath, $commandArguments)
{
Try {
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $commandPath
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.WindowStyle = 'Hidden'
$pinfo.CreateNoWindow = $True
$pinfo.Arguments = $commandArguments
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
$p.WaitForExit()
$p | Add-Member "commandTitle" $commandTitle
$p | Add-Member "stdout" $stdout
$p | Add-Member "stderr" $stderr
}
Catch {
}
$p
}

下面是从另一个 powershell 进程(序列化)获得输出的一种复杂方法:

start-process -wait -nonewwindow powershell 'ps | Export-Clixml out.xml'
import-clixml out.xml

让我强调一下 -nonewwindow来获得标准输出和标准错误,至少在本地屏幕上是这样的:

start-process -wait cmd '/c dir' -nonewwindow


Volume in drive C is Windows
Volume Serial Number is 2AC6-626F


Directory of C:\users\me\foo


11/24/2022  11:40 AM    <DIR>          .
11/24/2022  11:40 AM    <DIR>          ..
11/24/2022  11:40 AM               330 file.json
1 File(s)            330 bytes
2 Dir(s)  25,042,915,328 bytes free
start-process -wait cmd '/c dir foo' -nonewwindow


Volume in drive C is Windows
Volume Serial Number is 2AC6-626F


Directory of C:\users\me\foo


File Not Found

要同时获取 stdout 和 stderr,我使用:

Function GetProgramOutput([string]$exe, [string]$arguments)
{
$process = New-Object -TypeName System.Diagnostics.Process
$process.StartInfo.FileName = $exe
$process.StartInfo.Arguments = $arguments


$process.StartInfo.UseShellExecute = $false
$process.StartInfo.RedirectStandardOutput = $true
$process.StartInfo.RedirectStandardError = $true
$process.Start()


$output = $process.StandardOutput.ReadToEnd()
$err = $process.StandardError.ReadToEnd()


$process.WaitForExit()


$output
$err
}


$exe = "cmd"
$arguments = '/c echo hello 1>&2'   #this writes 'hello' to stderr


$runResult = (GetProgramOutput $exe $arguments)
$stdout = $runResult[-2]
$stderr = $runResult[-1]


[System.Console]::WriteLine("Standard out: " + $stdout)
[System.Console]::WriteLine("Standard error: " + $stderr)

下面是我根据其他人在这个帖子上发布的例子编写的。此版本将隐藏控制台窗口并提供输出显示选项。

function Invoke-Process {
[CmdletBinding(SupportsShouldProcess)]
param
(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$FilePath,


[Parameter()]
[ValidateNotNullOrEmpty()]
[string]$ArgumentList,


[ValidateSet("Full","StdOut","StdErr","ExitCode","None")]
[string]$DisplayLevel
)


$ErrorActionPreference = 'Stop'


try {
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $FilePath
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.WindowStyle = 'Hidden'
$pinfo.CreateNoWindow = $true
$pinfo.Arguments = $ArgumentList
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$result = [pscustomobject]@{
Title = ($MyInvocation.MyCommand).Name
Command = $FilePath
Arguments = $ArgumentList
StdOut = $p.StandardOutput.ReadToEnd()
StdErr = $p.StandardError.ReadToEnd()
ExitCode = $p.ExitCode
}
$p.WaitForExit()


if (-not([string]::IsNullOrEmpty($DisplayLevel))) {
switch($DisplayLevel) {
"Full" { return $result; break }
"StdOut" { return $result.StdOut; break }
"StdErr" { return $result.StdErr; break }
"ExitCode" { return $result.ExitCode; break }
}
}
}
catch {
exit
}
}

例子: Invoke-Process -FilePath "FQPN" -ArgumentList "ARGS" -DisplayLevel Full

改进的答案 -只要你同意使用 Start-Job而不是 Start-Process

结果表明,当脚本运行时,STDOUT 和 STDERR 在字符串数组 $job.ChildJobs[0].Output$job.ChildJobs[0].Error中积累。因此您可以轮询这些值并定期写出它们。也许算是黑客技术,但很管用。

但它不是一个流,因此必须手动跟踪数组中的起始索引。

这段代码比我最初的答案要简单,最后在 $job.ChildJobs[0].Output中有整个 STDOUT。作为这个演示的小奖励,调用脚本是 PS7,后台工作是 PS5。

$scriptBlock = {
Param ([int]$param1, [int]$param2)
$PSVersionTable
Start-Sleep -Seconds 1
$param1 + $param2
}


$parameters = @{
ScriptBlock = $scriptBlock
ArgumentList = 1, 2
PSVersion = 5.1 # <-- remove this line for PS7
}


$timeoutSec = 5
$job = Start-Job @parameters
$job.ChildJobs[0].Output
$index = $job.ChildJobs[0].Output.Count


while ($job.JobStateInfo.State -eq [System.Management.Automation.JobState]::Running) {
Start-Sleep -Milliseconds 200
$job.ChildJobs[0].Output[$index]
$index = $job.ChildJobs[0].Output.Count
if (([DateTime]::Now - $job.PSBeginTime).TotalSeconds -gt $timeoutSec) {
throw "Job timed out."
}
}

正如所指出的,我的原始答案可以交错输出。这是 PowerShell 中事件处理的一个限制。这不是一个可以解决的问题。

原来的答案,不要使用 -只是把它留在这里感兴趣

如果有超时,则 ReadToEnd()不是一个选项。您可以执行一些奇特的循环,但是 IMO 执行此操作的“最干净”的方法是忽略流。改为钩住 OutputDataReceived/ErrorDataReceived事件,收集输出。这种方法还避免了其他人提到的线程问题。

这在 c # 中很简单,但是在 Powershell 却很复杂和冗长。特别是,由于某些原因,add_OutputDataReceived不可用。(不确定这是一个 bug 还是一个特性,至少在 PowerShell 5.1中似乎是这种情况。)为了解决这个问题,你可以使用 Register-ObjectEvent

$stdout = New-Object System.Text.StringBuilder
$stderr = New-Object System.Text.StringBuilder


$proc = [System.Diagnostics.Process]@{
StartInfo = @{
FileName = 'ping.exe'
Arguments = 'google.com'
RedirectStandardOutput = $true
RedirectStandardError = $true
UseShellExecute = $false
WorkingDirectory = $PSScriptRoot
}
}


$stdoutEvent = Register-ObjectEvent $proc -EventName OutputDataReceived -MessageData $stdout -Action {
$Event.MessageData.AppendLine($Event.SourceEventArgs.Data)
}


$stderrEvent = Register-ObjectEvent $proc -EventName ErrorDataReceived -MessageData $stderr -Action {
$Event.MessageData.AppendLine($Event.SourceEventArgs.Data)
}


$proc.Start() | Out-Null
$proc.BeginOutputReadLine()
$proc.BeginErrorReadLine()
Wait-Process -Id $proc.Id -TimeoutSec 5


if ($proc.HasExited) {
$exitCode = $proc.ExitCode
}
else {
Stop-Process -Force -Id $proc.Id
$exitCode = -1
}


# Be sure to unregister.  You have been warned.
Unregister-Event $stdoutEvent.Id
Unregister-Event $stderrEvent.Id
Write-Output $stdout.ToString()
Write-Output $stderr.ToString()
Write-Output "Exit code: $exitCode"
  • 显示的代码是快乐路径(stderr 为空)
  • 要测试超时路径,请将 -TimeoutSec设置为 .5
  • 要测试可悲的路径(stderr 有内容) ,请将 FileName设置为 'cmd',将 Arguments设置为 /C asdf

您可能还需要考虑使用 & 操作符与 --%结合使用,而不是 start-process-这样可以方便地管道和处理命令和/或错误输出。

  • 将转义参数放入变量中
  • 将参数放入变量中
$deploy= "C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe"
$esc = '--%'
$arguments ="-source:package='c:\temp\pkg.zip' -verb:sync"
$output = & $deploy $esc $arguments

它可以不受干扰地将参数传递给可执行文件,并且让我可以绕过 start-process 的问题。

将 Stderr 和 Stdout 合并为一个变量:

$output = & $deploy $esc $arguments 2>&1

获取 Stderr 和 Stdout 的单独变量

$err = $( $output = & $deploy $esc $arguments) 2>&1