PowerShell - 将可执行文件的 stderr 重定向到文件或变量,但仍有 stdout 转到控制台



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

git clone "$RepositoryUrl" "$localRepoDirectory"

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

问题是,我也希望能够检测是否有任何错误发生,而下载。我发现了这篇关于重定向各种流的文章,所以我试着:

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

这会将任何错误从stderr管道到我的文件,但不再在控制台中显示良好的进度信息。

我可以像这样使用Tee-Object:
(git clone "$RepositoryUrl" "$localRepoDirectory") | Tee-Object -FilePath $errorLogFilePath

和我仍然得到不错的进度输出,但这管道stdout到文件,而不是stderr;我只关心检测错误。

是否有一种方法,我可以存储任何错误,发生在一个文件或(最好)一个变量,同时仍然有进度信息管道到控制台窗口?我有一种感觉,答案可能在于将不同的流重定向到其他流,就像这篇文章中讨论的那样,但我不确定。

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

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

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

$output总是包含文本"Cloning into '[localRepoDirectory]'…",无论命令成功完成还是产生错误。此外,在执行此操作时,进度信息仍将写入控制台。这使我认为进度信息不是通过标准输出写入的,而是通过其他流写入的。

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

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

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

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

所以在Byron的帮助下,我用一种新的方法尝试了更多的解决方案,但仍然不能得到我想要的。当使用一个新进程时,我从来没有把好的进度写入控制台。

我尝试的三个新方法都使用了这段代码:
$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()

方法2 -同步获取输出:

$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
}

这在过程工作时输出数据,这很好,但它仍然没有显示好的进度信息:(

我想我知道你的答案了。我用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 - Cloning into "…"错误-检查文件:22% (666/2971)
错误-检查文件:23% (684/2971)
错误- Checking out files: 24% (714/2971)

你可以把git clone命令放在一个高级函数中,例如:

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

如果你使用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
}

当然,你可以用合适的参数把它包装在一个函数中…

最新更新