【问题标题】:PowerShell - redirect executable's stderr to file or variable but still have stdout go to consolePowerShell - 将可执行文件的标准错误重定向到文件或变量,但仍有标准输出到控制台
【发布时间】:2013-05-04 06:32:12
【问题描述】:

我正在编写一个脚本来从 GitHub 下载几个存储库。这是下载存储库的命令:

git clone "$RepositoryUrl" "$localRepoDirectory"

当我运行此命令时,它会在我想要显示的控制台窗口中显示一些不错的进度信息。

问题是我还希望能够检测下载时是否发生了任何错误。我找到了this post that talks about redirecting the various streams,所以我尝试了:

(git clone "$RepositoryUrl" "$localRepoDirectory") 2> $errorLogFilePath

这会将任何错误从 stderr 传送到我的文件,但不再在控制台中显示漂亮的进度信息。

我可以像这样使用 Tee-Object:

(git clone "$RepositoryUrl" "$localRepoDirectory") | Tee-Object -FilePath $errorLogFilePath

我仍然得到了不错的进度输出,但是这会将 stdout 传递给文件,而不是 stderr;我只关心检测错误。

有没有一种方法可以存储文件或(最好是)变量中发生的任何错误,同时还能将进度信息通过管道传输到控制台窗口?我感觉答案可能在redirecting various streams into other streams as discusses in this post,但我不太确定。

======== 更新=======

我不确定 git.exe 是否与您的典型可执行文件不同,但我进行了更多测试,发现如下:

$output = (git clone "$RepositoryUrl" "$localRepoDirectory")

$output 始终包含文本“正在克隆到 '[localRepoDirectory]'...”,无论命令成功完成还是产生错误。此外,执行此操作时,进度信息仍会写入控制台。这让我认为进度信息不是通过 stdout 写入的,而是通过其他流写入的?

如果发生错误,则会将错误写入控制台,但使用通常的白色前景色,而不是典型的红色表示错误和黄色表示警告。当从 cmdlet 函数中调用它并且命令失败并出现错误时,错误不会通过函数的 -ErrorVariable(或 -WarningVariable)参数返回(但是,如果我自己执行 Write-Error 并通过 -ErrorVariable 返回)。这让我认为 git.exe 不会写入 stderr,但是当我们这样做时:

(git clone "$RepositoryUrl" "$localRepoDirectory") 2> $errorLogFilePath

错误消息被写入文件,所以我认为它确实写入了 stderr。所以现在我很困惑......

======== 更新 2 =======

因此,在拜伦的帮助下,我使用新流程尝试了更多解决方案,但仍然无法得到我想要的。使用新进程时,我从来没有将好的进度写入控制台。

我尝试过的三种新方法都使用了这段代码:

$process = New-Object System.Diagnostics.Process
$process.StartInfo.Arguments = "clone ""$RepositoryUrl"" ""$localRepoDirectory"""
$process.StartInfo.UseShellExecute = $false
$process.StartInfo.RedirectStandardOutput = $true
$process.StartInfo.RedirectStandardError = $true
$process.StartInfo.CreateNoWindow = $true
$process.StartInfo.WorkingDirectory = $WORKING_DIRECTORY
$process.StartInfo.FileName = "git"

方法 1 - 在新进程中运行,然后读取输出:

$process.Start()
$process.WaitForExit()
Write-Host Output - $process.StandardOutput.ReadToEnd()    
Write-Host Errors - $process.StandardError.ReadToEnd()

方法二——同步获取输出:

$process.Start()
while (!$process.HasExited)
{
    Write-Host Output - $process.StandardOutput.ReadToEnd()
    Write-Host Error Output - $process.StandardError.ReadToEnd()

    Start-Sleep -Seconds 1
}

尽管这看起来会在进程运行时写入输出,但在进程退出之前它不会写入任何内容。

方法 3 - 异步获取输出:

Register-ObjectEvent -InputObject $process -EventName "OutputDataReceived" -Action {Write-Host Output Data - $args[1].Data }
Register-ObjectEvent -InputObject $process -EventName "ErrorDataReceived" -Action { Write-Host Error Data - $args[1].Data }
$process.Start()
$process.BeginOutputReadLine()
$process.BeginErrorReadLine()
while (!$process.HasExited)
{
    Start-Sleep -Seconds 1
}

这确实在进程运行时输出数据,这很好,但它仍然没有显示好的进度信息:(

【问题讨论】:

    标签: redirect powershell stream stderr powershell-3.0


    【解决方案1】:

    我想我有你的答案。我正在使用 PowerShell 一段时间并创建了几个构建系统。对不起,如果脚本有点长,但它可以工作。

    $dir = <your dir>
    $global:log = <your log file which must be in the global scope> # Not global = won't work
    
    function Create-Process {
        $process = New-Object -TypeName System.Diagnostics.Process
        $process.StartInfo.CreateNoWindow = $false
        $process.StartInfo.RedirectStandardError = $true
        $process.StartInfo.UseShellExecute = $false
        return $process
    }
    
    function Terminate-Process {
        param([System.Diagnostics.Process]$process)
    
        $code = $process.ExitCode
        $process.Close()
        $process.Dispose()
        Remove-Variable process
        return $code
    }
    
    function Launch-Process {
        param([System.Diagnostics.Process]$process, [string]$log, [int]$timeout = 0)
    
        $errorjob = Register-ObjectEvent -InputObject $process -EventName ErrorDataReceived -SourceIdentifier Common.LaunchProcess.Error -action {
            if(-not [string]::IsNullOrEmpty($EventArgs.data)) {
                "ERROR - $($EventArgs.data)" | Out-File $log -Encoding ASCII -Append
                Write-Host "ERROR - $($EventArgs.data)"
            }
        }
        $outputjob = Register-ObjectEvent -InputObject $process -EventName OutputDataReceived -SourceIdentifier Common.LaunchProcess.Output -action {
            if(-not [string]::IsNullOrEmpty($EventArgs.data)) {
                "Out - $($EventArgs.data)" | Out-File $log -Encoding ASCII -Append
                Write-Host "Out - $($EventArgs.data)"
            }
        }
    
        if($errorjob -eq $null) {
            "ERROR - The error job is null" | Out-File $log -Encoding ASCII -Append
            Write-Host "ERROR - The error job is null"
        }
    
        if($outputjob -eq $null) {
            "ERROR - The output job is null" | Out-File $log -Encoding ASCII -Append
            Write-Host "ERROR - The output job is null"
        }
    
        $process.Start() 
        $process.BeginErrorReadLine()
    
        if($process.StartInfo.RedirectStandardOutput) {
            $process.BeginOutputReadLine() 
        }
    
        $ret = $null
        if($timeout -eq 0)
        {
            $process.WaitForExit()
            $ret = $true
        }
        else
        {
            if(-not($process.WaitForExit($timeout)))
            {
                Write-Host "ERROR - The process is not completed, after the specified timeout: $($timeout)"
                $ret = $false
            }
            else
            {
                $ret = $true
            }
        }
    
        # Cancel the event registrations
        Remove-Event * -ErrorAction SilentlyContinue
        Unregister-Event -SourceIdentifier Common.LaunchProcess.Error
        Unregister-Event -SourceIdentifier Common.LaunchProcess.Output
        Stop-Job $errorjob.Id
        Remove-Job $errorjob.Id
        Stop-Job $outputjob.Id
        Remove-Job $outputjob.Id
    
        $ret
    }
    
    $repo = <your repo>
    
    $process = Create-Process
    $process.StartInfo.RedirectStandardOutput = $true
    $process.StartInfo.FileName = "git.exe"
    $process.StartInfo.Arguments = "clone $($repo)"
    $process.StartInfo.WorkingDirectory = $dir
    Launch-Process $process $global:log
    Terminate-Process $process
    

    日志文件必须在全局范围内,因为运行事件处理的例程不在脚本范围内。

    我的日志文件示例:

    Out - 克隆到 ''... 错误 - 检出文件:22% (666/2971)
    错误 - 检出文件:23% (684/2971)
    错误 - 检出文件:24% (714/2971)

    【讨论】:

    • 您好 romu,感谢您的代码!这仍然不能给我我想要的东西。这确实将所有内容(包括进度)写入日志文件以及控制台,但不会在控制台中实时显示进度;它等待下载完成,然后立即显示所有内容。因此,如果下载 repo 需要 10 分钟,我只会在控制台中看到闪烁的光标 10 分钟,然后当它完成时,它会显示所有进度信息:(
    • 好的,很抱歉不能提供更多帮助。
    【解决方案2】:

    如果你使用System.Diagnostics.Process启动Git,你可以重定向所有的错误和输出。

    我只需要为 Inkscape 解决这个问题:

    $si = New-Object System.Diagnostics.ProcessStartInfo
    $si.Arguments = YOUR PROCESS ARGS
    $si.UseShellExecute = $false
    $si.RedirectStandardOutput = $true
    $si.RedirectStandardError = $true
    $si.WorkingDirectory = $workingDir
    $si.FileName = EXECUTABLE LOCATION
    $process = [Diagnostics.Process]::Start($si)
    while (!($process.HasExited))
    {
        // Do what you want with strerr and stdout
        Start-Sleep -s 1  // Sleep for 1 second
    }
    

    当然,你可以用适当的参数将它包装在一个函数中......

    【讨论】:

    • 看起来这也不起作用;除非我做错了什么。在 $process.WaitForExit() 之后,我可以通过 $process.StandardOutput.ReadToEnd() 和 StandardError.ReadToEnd() 获得生成的输出,但是在操作运行时我再次没有显示任何进度;操作完成后我得到信息。此外,看起来进度信息是通过 StandardError 显示的。
    • 您可以将$process 包装在一个while 循环中,并随时读取输出。 - 查看更新的答案
    • 感谢 Byron 的更新,但它似乎仍然无法正常工作。在进程由于某种原因退出之前,使用 StandardOutput.ReadToEnd() 仍然不会显示,并且 StandardOutput.ReadToEnd() 只是以这种方式返回一个空字符串:S 我还尝试将事件处理程序注册到进程的 OutputDataReceived 和 ErrorDataReceived 事件。此方法在进程运行时确实返回了数据,但由于某种原因,错误数据始终为空。
    • 如果您想“实时”查看每个输出,您可能需要为每个输出创建一个线程。见msdn.microsoft.com/en-us/library/…
    • 是的,我忘了提及它,但是在使用事件处理程序时,我使用的是 .BeginOutputReadLine() 和 .BeginErrorReadLine(),而不是 .ReadToEnd()。感谢您对我的坚持! :)
    【解决方案3】:

    您可以通过将 git clone 命令放在高级函数中来做到这一点,例如:

    function Clone-Git {
        [CmdletBinding()]
        param($repoUrl, $localRepoDir)
    
        git clone $repoUrl $localRepoDir
    }
    
    Clone-Git $RepositoryUrl $localRepoDirectory -ev cloneErrors
    
    $cloneErrors
    

    【讨论】:

    • 我对这个答案感到非常兴奋,因为它看起来很简单。不幸的是,它似乎不适用于我的情况。 git.exe 似乎没有使用 stderr,因为 $cloneErrors 变量始终为空。然而,它确实将错误消息写入控制台,但使用常规的白色前景色,而不是典型的红色表示错误或黄色表示警告。但是,使用“2>”确实会将错误消息通过管道传输到文件,因此它看起来像是在使用 stderr。我糊涂了。如果我从 Clone-Git 函数执行自己的 Write-Error,$cloneErrors 变量会显示我的错误。还有其他提示吗?
    • 听起来 git.exe 正在使用 stderr。 IIRC 这将在 ISE 中有效,但在控制台中无效。嗬! PowerShell.exe(相对于 ISE)为控制台 exe 提供了一个句柄写入,因此当 git.exe 输出到 stderr 时,它绕过了 PowerShell 的错误流。您可以做的是将 stderr 重定向到函数中的文件,然后如果 $LastExitCode 不等于 0(在 git clone 调用之后),则写入错误该文件的内容。
    • 我应该如何将 stderr 重定向到文件,同时仍将进度信息显示到控制台?使用“git clone $repoUrl $localRepoDirectory 2> C:\GitHubErrors.log”停止显示进度信息。
    • 如果你使用Diagnostics.Process你可以独立选择重定向stderr和stdout
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-11-17
    • 1970-01-01
    • 2012-05-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多