![](/img/trans.png)
[英]Powershell: Capturing standard out and error with Process object
[英]Capturing standard out and error with Start-Process
访问StandardError
和StandardOutput
属性时,PowerShell 的Start-Process
命令是否存在错误?
如果我运行以下命令,我不会得到 output:
$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait
$process.StandardOutput
$process.StandardError
但是,如果我将 output 重定向到一个文件,我会得到预期的结果:
$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait -RedirectStandardOutput stdout.txt -RedirectStandardError stderr.txt
出于某种原因,这就是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
参数(这让我很困惑)。
重要的:
我们一直在使用LPG提供的上述功能。
但是,这包含您在启动生成大量输出的进程时可能会遇到的错误。 因此,您在使用此功能时可能会遇到死锁。 而是使用下面的改编版本:
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.ReadToEnd 之前调用 p.WaitForExit 并且子进程写入足够的文本来填充重定向的流,则可能导致死锁情况。 父进程将无限期地等待子进程退出。 子进程将无限期地等待父进程从完整的 StandardError 流中读取。
我也遇到了这个问题,最终使用Andy 的代码创建了一个函数,以便在需要运行多个命令时进行清理。
它将标准错误、标准输出和退出代码作为对象返回。 需要注意的一件事:该函数不会接受路径中的.\
; 必须使用完整路径。
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
这是从另一个powershell进程(序列化)获取输出的一种笨拙方法:
start-process -wait -nonewwindow powershell 'ps | Export-Clixml out.xml'; import-clixml out.xml
要同时获得标准输出和标准错误,我使用:
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
.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
由于某种原因不可用。 (不确定这是错误还是功能,至少在 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"
-TimeoutSec
设置为.5
FileName
设置为'cmd'
并将Arguments
设置为/C asdf
这是我的函数版本,它返回具有 3 个新属性的标准 System.Diagnostics.Process
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
}
您可能还想考虑将 & 运算符与--%
结合使用,而不是 start-process - 这使您可以轻松地 pipe 并处理命令和/或错误 output。
$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
这将参数无干扰地传递给可执行文件,让我解决了启动过程的问题。
将 Stderr 和 Stdout 组合成一个变量:
$output = & $deploy $esc $arguments 2>&1
为 Stderr 和 Stdout 获取单独的变量
$err = $( $output = & $deploy $esc $arguments) 2>&1
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.