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

162

当访问PowerShell的Start-Process命令的StandardErrorStandardOutput属性时,是否存在错误?

如果我运行以下代码,则不会输出任何内容:

$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

7
在这种情况下,你真的需要使用Start-process吗?...$process= ping localhost # 会将输出保存在进程变量中。 - mjsr
1
真的。我正在寻找一种更清晰的处理返回和参数的方法。最终我按照你展示的方式编写了脚本。 - jzbruno
@mjsr 有没有不使用Start-process的方法来获取输出和ExitCode?我需要知道命令是否成功,但是能够传递输出以便于错误消息。 - scuba88
11个回答

168

这就是为什么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

7
我接受了你的回答。我希望他们不会创建未被使用的属性,那样会非常令人困惑。 - jzbruno
7
如果您在使用此方法时遇到问题,请参阅此处的被接受的答案:https://dev59.com/Tmgu5IYBdhLWcg3wK0Cr,其中稍微修改了WaitForExit和StandardOutput.ReadToEnd。 - Ralph Willgoss
5
当您使用动词“runAs”时,它不允许使用“-NoNewWindow”或“Redirection Options”。 - Maverick
26
由于同时读取了StdErr和StdOut直到结束,某些情况下该代码将会死锁。详情请参阅http://msdn.microsoft.com/en-us/library/system.diagnostics.processstartinfo.redirectstandardoutput.aspx - codepoke
10
@codepoke - 情况比这稍微糟糕一些 - 因为它先调用了WaitForExit函数,即使只重定向了其中一个流,如果流缓冲区被填满,它也可能出现死锁(因为它直到进程退出才尝试从中读取)。 - James Manning
显示剩余13条评论

33

在问题中提供的代码中,我认为读取初始化变量的 ExitCode 属性应该有效。

$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait
$process.ExitCode
请注意 (就像您的例子一样) ,您需要添加-PassThru-Wait参数(这让我困惑了一段时间)。

如果参数列表包含一个变量,会怎么样?它似乎不会被展开。 - O.O
2
你会把参数列表放在引号中。那样行吗? $process = Start-Process -FilePath ping -ArgumentList "-t localhost -n 1" -NoNewWindow -PassThru -Wait - JJones
如何在PowerShell窗口中显示输出并将其记录到日志文件中?这是可能的吗? - Murali Dhar Darshan
5
无法在 -Verb runAs 命令中使用 -NoNewWindow 参数。 - Dragas

20

重要提示:

我们一直在使用上述由LPG提供的函数(链接)

然而,这个函数存在一个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.ReadToEnd之前调用p.WaitForExit,并且子进程写入足够多的文本以填充重定向流,则可能会导致死锁条件。父进程将无限期地等待子进程退出。子进程将无限期地等待父进程从完整的StandardError流中读取。


8
由于对 ReadToEnd() 的同步调用,这段代码仍然会发生死锁,你在 MSDN 链接中提到了这一点。 - bergmeister
1
现在看来,这解决了我的问题。我必须承认我不完全明白为什么它会挂起,但似乎是由于空的stderr阻止了进程的完成。这件事很奇怪,因为它在很长一段时间内都能正常工作,但就在圣诞节前突然开始出问题,导致很多Java进程挂起。 - rhellem
有没有示例展示这个函数的使用方法? - Jonesome Reinstate Monica

15

我也遇到了这个问题,最终使用Andy的代码来创建一个函数,在需要运行多个命令时清理事物。

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

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"

好主意,但是似乎语法对我不起作用。参数列表不应该使用param([type]$ArgumentName)语法吗?你能给这个函数添加一个调用示例吗? - Lockszmith
1
关于“One thing to note: the function won't accept .\ in the path; full paths must be used.”:您可以使用以下代码:
$pinfo.FileName = Resolve-Path $commandPath
- Lupuz

13

我真的在那些示例中遇到了麻烦(来自Andy Arismendi)(来自LPG)。你应该始终使用:

$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

你在哪里看到过这样的话:“在$p.WaitForExit()之前,应始终使用$p.StandardOutput.ReadToEnd()”? 如果缓冲区中有输出被耗尽,并且稍后还会有更多输出,如果执行行在WaitForExit上并且进程尚未完成(随后输出更多的stderr或stdout),那么将会错过这些输出... - CJBS
关于我之前的评论,后来我看到了有关死锁和缓冲区溢出的接受答案的评论,但是除此以外,我预期即使缓冲区被完全读取完毕,也不能说明进程已经完成,因此可能会有更多未被捕获的输出。我漏掉了什么吗? - CJBS
2
@CJBS:“仅仅因为缓冲区读到了末尾,并不意味着进程已经完成”--实际上,它确实意味着如此。事实上,这就是为什么会出现死锁的原因。读取“到末尾”并不意味着“立即读取所有内容”。它的意思是开始读取,并且在流关闭时不停止,这与进程终止相同。 - Peter Duniho

5
这里有一种不太优雅的方法可以获取另一个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

2
!!! 很好!!谢谢 - Sergio Cabral
1
每当我添加-NoNewWindow时,就会出现“CategoryInfo:InvalidArgument:(:) [Start-Process],ParameterBindingException FullyQualifiedErrorId:AmbiguousParameterSet,Microsoft.PowerShell.Commands.StartProcessCommand”错误。如果没有它,底层的PowerShell脚本可以正常工作。 - ashrasmun
@ashrasmun,这是因为你不能在同一命令中使用-Verb-WindowStyle参数和-NoNewWindow - Mavaddat Javid

3

根据其他人在此线程上发布的示例,我做了一些东西。这个版本将隐藏控制台窗口,并提供输出显示选项。

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


很棒的函数。可以通过为DisplayLevel设置默认值(=>"Full")来改进它。 - fred727

2

改进的答案 - 只要您使用Start-Job而不是Start-Process没有问题

结果发现,当脚本运行时,STDOUT和STDERR会累积在字符串数组$job.ChildJobs[0].Output$job.ChildJobs[0].Error中。因此,您可以定期轮询这些值并将它们写出来。可能有点hack,但它有效。

它不是一个流,所以您必须手动跟踪数组中的起始索引。

这段代码比我的原始答案更简单,最后您将在$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()不是一个选项。你可以做一些花哨的循环,但在我看来,最“干净”的方法是忽略流。而是挂钩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"
  • 所展示的代码是快乐路径(stderr为空)
  • 要测试超时路径,请将 -TimeoutSec 设置为 .5
  • 要测试失败路径(stderr有内容),请将 FileName 设置为 'cmd',并将 Arguments 设置为 /C asdf

1
这个脚本可以正常工作,但外部进程生成的输出可能以无序的方式显示。我编写了一个小型控制台应用程序,它只是按其命令行上出现的顺序回显传递给它的参数。当我通过这个脚本执行它时,生成的输出被无序地显示出来。 - STLDev
1
@STLDev - 你说得对。请看我的更新答案。在我们的应用程序中,我们只是用它来进行视觉监控,而不是捕获输出,所以它是可以容忍的。 - Paul Williams

1

你可能还想考虑使用 & 运算符与 --% 结合使用,而不是使用 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

获取标准错误和标准输出的单独变量

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

1

为了获取标准输出和标准错误输出,我使用以下代码:

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)

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