Git克隆:将标准错误重定向到标准输出,但保持错误仍写入标准错误流。

40

git clone 的输出被写入到 stderr 中,如此处所述。我可以使用以下命令重定向它的输出:

git clone https://myrepo c:\repo 2>&1

但这将重定向所有输出,包括错误,从stderrstdout。有没有一种方法可以将进度消息重定向到stdout,但仍将错误消息写入stderr


2
我不明白。如果您不重定向stderr,则将有两个输出流,一个用于stdout(包括“进度”消息),另一个用于错误。请记住,Git是WIn32程序,不实现PS脚本可以的各种流。 - Χpẘ
实际上,git是一个基于Unix的程序,因此会将所有不属于输出管道的内容发送到stderr,包括详细信息。如果您想获取这些详细信息,最好使用管道命令。 - Eris
谢谢。我知道git是一个基于Unix的程序,并且会将一些内容写入stderr。 @Eris:你指的是哪些管道命令? - Pascal Berger
7个回答

29

明确了一个处理Git重定向的新方法, 通过MingW更新,自Git 2.15.x/2.16(2018年第一季度)开始提供。

参见提交b2f5571提交1a172e4提交3f94442(由Johannes Schindelin (dscho)于2017年11月1日完成)。
(由Junio C Hamano -- gitster --提交421f21c中合并,2017年11月9日)

mingw:添加重定向标准句柄的实验性功能

特别是在从应用程序调用Git时,如Visual Studio的Team Explorer,关闭标准输入/输出/错误很重要。
然而,在Windows上生成进程时,如果要使用它们,则必须将这些句柄标记为可继承的,但该标志是全局标志,可能会被其他生成的进程使用,然后不知道关闭那些句柄。

让我们引入一组环境变量(GIT_REDIRECT_STDIN等),指定文件路径,甚至更好的是命名管道(类似于Unix套接字),由生成的Git进程使用。
这有助于解决上述问题:这些命名管道将在启动时以非可继承方式打开,并且不会传递任何句柄(因此任何生成的子代都不需要关闭继承的句柄)。

此功能已随Git for Windows一起发布(标记为实验性)自v2.11.0(2)以来,因此它已经进行了一些严格的测试。

Git文档现在包含:

GIT_REDIRECT_STDIN:
GIT_REDIRECT_STDOUT:
GIT_REDIRECT_STDERR:

仅限Windows:允许将标准输入/输出/错误句柄重定向到环境变量指定的路径。 这在多线程应用程序中非常有用,因为通过CreateProcess()传递标准句柄的规范方法不可行,因为它需要将句柄标记为可继承(因此每个衍生进程都会继承它们,可能会阻止常规的Git操作)。

主要预期使用情况是使用命名管道进行通信(例如:\\.\pipe\my-git-stdin-123)。

并且还添加了:

mingw:可选择通过相同的句柄重定向stderr/stdout

Powershell和Unix shell中的"2>&1"符号表示将stderr重定向到已经写入stdout的相同句柄中。

让我们使用这个特殊值来实现相同的技巧,使用GIT_REDIRECT_STDERRGIT_REDIRECT_STDOUT:如果前者的值为2>&1,则stderr将简单地写入与stdout相同的句柄。

该功能是由Jeff Hostetler建议的。

用法示例:$env:GIT_REDIRECT_STDERR = '2>&1'


9
谢谢!我在 TFS 构建中使用 PowerShell 脚本时出现了奇怪的问题。尽管脚本成功运行,但却被报告为错误(当然没有任何错误信息)。只需在运行 git 命令之前添加 $env:GIT_REDIRECT_STDERR = '2>&1' 即可解决该问题。 - Oscar Vasquez
它解决了我的问题,我有一个cron脚本定期更新git镜像。 - FractalSpace

20
我使用这个脚本来运行git命令。由于即使成功(例如,在同步时进行拉取),git也会写入stderr,因此该脚本处理了这些情况并输出了第一行输出,通常这就是您需要知道的内容。
<#
.Synopsis
    Invoke git, handling its quirky stderr that isn't error

.Outputs
    Git messages, and lastly the exit code

.Example
    Invoke-Git push

.Example
    Invoke-Git "add ."
#>
function Invoke-Git
{
param(
[Parameter(Mandatory)]
[string] $Command )

    try {

        $exit = 0
        $path = [System.IO.Path]::GetTempFileName()

        Invoke-Expression "git $Command 2> $path"
        $exit = $LASTEXITCODE
        if ( $exit -gt 0 )
        {
            Write-Error (Get-Content $path).ToString()
        }
        else
        {
            Get-Content $path | Select-Object -First 1
        }
        $exit
    }
    catch
    {
        Write-Host "Error: $_`n$($_.ScriptStackTrace)"
    }
    finally
    {
        if ( Test-Path $path )
        {
            Remove-Item $path
        }
    }
}

12

通常来说:

  • 控制台(终端)应用程序 - 无论是在Windows还是Unix-like平台上 - 只有两个输出流可用:

    • stdout(标准输出) - 这是数据(“返回值”)所在的位置。
    • stderr(标准错误) - 这是错误消息和 - 由于缺少其他流 - 任何不是数据的内容都会被发送到此处,例如进度和状态信息。
  • 因此,不能且不应该通过stderr输出的存在来推断成功与失败

    • 相反,您必须仅依赖于应用程序的进程退出代码

      • 0表示成功
      • 任何非零表示失败
    • PowerShell中,进程退出代码反映在自动变量$LASTEXITCODE


具体来说,这意味着:

  • 鉴于git的stderr输出行,您无法推断它们代表错误消息还是其他类型的非数据信息,例如进度或状态消息,git经常使用它们。

    • 告诉git将其stderr输出分类重定向到stdout(通过将环境变量GIT_REDIRECT_STDERR设置为字符串2>&1; 在PowerShell中为$env:GIT_REDIRECT_STDERR = '2>&1')并不会有帮助,因为错误消息和进度/状态消息都会被发送到那里。
  • 如上所述,您应该只从非零退出代码中推断失败。


务实的方法是执行以下操作:

# Invoke git and capture both its stdout and stderr streams, as strings.
$result = git clone https://myrepo c:\repo 2>&1 | % ToString

# Throw an error, if git indicated failure.
if ($LASTEXITCODE) { Throw "git failed (exit code: $LASTEXITCODE):`n$($result -join "`n")" }                                            

# Output the captured result, as an array of lines.
$result

注意:

  • | % ToString(其中%是缩写形式的ForEach-Object)确保仅输出字符串,因为通过重定向到成功输出流(&1)的stderr行(通过流2)被包装在[System.Management.Automation.ErrorRecord]实例中。

    • PowerShell (Core) 7+中,如果您只想打印结果,则这并不是必需的,因为即使这些[System.Management.Automation.ErrorRecord]也可以字符串一样打印;相比之下,Windows PowerShell将它们打印为PowerShell错误。
  • 2>&1在PowerShell版本7.1及以下版本中可能会产生意外的副作用-请参见此答案以获取背景信息。

  • 更好地将外部程序调用集成到PowerShell的错误处理中是此RFC草案的主题,特别是具有在遇到非零退出代码时自动中止执行选项。


1
务实的方法确实有帮助。 - Mahernoz
那是一种非常聪明的做事方式。谢谢! - Pierre P.
1
很高兴听到这个好消息,@PierreP。我很乐意帮忙。请注意,我刚刚更新了答案,并调整了| % ToString的位置,同时更新了解释。 - mklement0

8

我想补充一下,如果你和我一样更关心的是在stderr中获取错误信息而不在意进度的话,那么有一个非常简单的解决方法——只需向命令添加--quiet(或-q)即可。

这会告诉git停止报告任何进度,除非出现实际的错误。


2
这非常有帮助,解决了我的问题。谢谢! - Skoempie

3
您可以使用以下命令消除 stderr

git clone https://myrepo c:\repo 2>$null

通过这样做,所有的stderr都将不会显示。
您无法仅显示进度并丢弃错误信息。如果命令失败,则所有输出均为stderr;如果成功,则为stdout。
编辑: 看起来即使在Windows上命令成功,git命令输出也始终为stderr。 T.

我知道我可以像问题中所写的那样重定向stderr。我正在寻找的是一种将冗长的消息重定向到stdout但保留stderr中的错误的方法。在我的情况下,即使命令成功,输出也会进入stderr - Pascal Berger
没错。我已经测试过了,似乎在Windows上git不太好用,而且所有的输出都是stderr。 看起来这是一个git命令无法完成的任务。 - Oz Bar-Shalom
这也是在Linux/BSD/*nix系统上的相同行为。对于任何应用程序,只有3个默认流可用:stdinstdoutstderr。唯一的解决办法是使用其中许多库之一,这些库使用Git API并自己完成所有操作。 - Eris
@OzBar-Shalom +1 我想补充一点,将 stderr 重定向到实际变量,如 $output,并在每个 git 命令后调用 write-host 可以很有用,这样您仍然可以获得标准输出,同时修复 Windows 上错误的 stderr 输出。 - Matías Fidemraizer

2

我已经修改了Invoke-Git,以更好地适应我的需求。

从我寻找解决方案时阅读的许多帖子中,我猜想有更多的人可以使用这个。

享受吧。

这个版本将会:

  • 执行传入的Git命令(假设Git已经在执行路径中)。
  • 如果一切顺利,则所有输出(stdout和stderr)都会在主机上显示,而不是通过stderr。
  • 检查$LASTEXITCODE以查看是否实际上存在错误。如果存在错误,则将所有输出抛给调用者处理。
<#
.Synopsis
    Invoke git, handling its quirky stderr that isn't error

.Outputs
    Git messages

.Example
    Invoke-Git push

.Example
    Invoke-Git "add ."
#>
function Invoke-Git
{
param(
[Parameter(Mandatory)]
[string] $Command )

    try {
        # I could do this in the main script just once, but then the caller would have to know to do that 
        # in every script where they use this function.
        $old_env = $env:GIT_REDIRECT_STDERR
        $env:GIT_REDIRECT_STDERR = '2>&1'

        Write-Host -ForegroundColor Green "`nExecuting: git $Command "
        $output = Invoke-Expression "git $Command "
        if ( $LASTEXITCODE -gt 0 )
        {
            # note: No catch below (only the try/finally). Let the caller handle the exception.
            Throw "Error Encountered executing: 'git $Command '"
        }
        else
        {
            # because $output probably has miultiple lines (array of strings), by piping it to write-host we get multiple lines.
            $output | Write-Host 
        }
    }
    # note: No catch here. Let the caller handle it.
    finally
    {
        $env:GIT_REDIRECT_STDERR = $old_env
    }
}

当出现错误时,不会打印实际的错误信息,因此与 -ErrorVariable 不兼容。但是,如果将该 Throw 替换为类似 Write-Error $output 的内容,并将 Write-Host 替换为 Write-Output,则可以很好地使用。 - stijn

1
这里提供另一种方法,可能会启发一些人。正如其他人指出的那样,重定向和检查退出代码效果很好。与其他答案不同的是:
  • 将重定向到文件的输出改为返回输出并使用单独的错误消息,因为只要 git 在运行,您就无法看到任何操作。这是一种权衡,但我更喜欢它。
  • 使用 Write-Verbose 而不是 Write-Host,因为前者是可配置的。
  • 使用 Write-Error 生成错误而不是强制抛出错误。这样 ErrorAction、ErrorVariable 等功能可以按预期工作。
  • 支持设置要运行的目录,就像您会使用 -C 一样,而不必 cd 进目录,还支持那些通常没有此功能的命令。所以你可以做 igit -dir some/path stash。我主要在自动化脚本中使用它,否则就必须 cd 进目录,这很麻烦。
  • 使用 ValueFromRemainingArguments 等功能,使命令可以像直接编写 git 命令一样传递,而不需要字符串,但仍然允许它。因此,igit checkout master 就像 igit 'checkout master'。几乎是这样,因为标准的 PowerShell 警告适用:引号需要传递给底层命令,例如 igit log '--format="%h %d"'。而且 PowerShell 不需要您输入完整的参数名称,这意味着 igit push -v将被解释为igit push -Verbose而不是传递-v,即向 git 发送详细推送。使用双破折号来处理它igit -- push -v` 或在所有命令后添加引号。

代码:

<#
.SYNOPSIS
Run git, Powershell-style.
.DESCRIPTION
By default some git commands (clone, checkout, ...) write a part of their
output to stderr, resulting in PS treating that as an error.
Here we work around that by redirecting stderr and using git's exit code
to check if something was actually wrong, and use Write-Error if that's the case,
i.e. standard PS error handling which works with -ErrorAction/-ErrorVariable etc.
The command can be passed as a string or as separate strings.
Additionally takes a $Directory argument which when used has the same effect as git -C,
but also works for clone/stash/submodule/... commands making it easier to automate those.
The $Git argument can be used to specify the executable.
.EXAMPLE
Invoke-Git status
Invoke-Git -Directory some/path status
Invoke-Git 'push -v'
Invoke-Git -Verbose -- push -v  # Pass that last -v to git.
#>
function Invoke-Git {
  [CmdletBinding()]
  param(
    [Parameter()] [Alias('Dir')] [String] $Directory = $null,
    [Parameter()] [String] $Git = 'git',
    [Parameter(Mandatory, Position=0, ValueFromRemainingArguments=$true)] [string] $Command
  )
  try {
    $commandParts = $Command.Split(' ')
    $subCommand = $commandParts[0]
    if ($Directory -and $subCommand -eq 'clone') {
      # To make all commands look alike handle this one as well.
      $Command = ($commandParts + @($Directory)) -join ' '
    } elseif ($Directory -and @('submodule', 'stash', 'init') -eq $subCommand) {
      # These currently require one to be in the git directory so go there.
      $currentDir = Get-Location
      cd $Directory
    } elseif ($Directory) {
      if ($commandParts -eq '-C') {
        # Not an error, git will pick the last one, but unexpected.
        Write-Warning 'Better use either -Directory or -C, not both'
      }
      $Command = "-C $Directory " + $Command
    }
    Write-Verbose "Invoke-Git on '$Directory' with command '$Command'"
    $gitRedirection = $env:GIT_REDIRECT_STDERR
    $env:GIT_REDIRECT_STDERR = '2>&1'
    # Deliberately not getting output here: while this means we cannot pass the actual error to Write-Error,
    # it does result in all commands being shown 'live'. Otherwise when doing a clone for instance,
    # nothing gets displayed while git is doing it's thing which is unexepected and too different from normal usage.
    Invoke-Expression "$Git $Command"
    if ($LASTEXITCODE -ne 0) {
      Write-Error "git exited with code $LASTEXITCODE"
    }
  } finally {
    $env:GIT_REDIRECT_STDERR = $gitRedirection
    if ($currentDir) {
      cd $currentDir
    }
  }
}

New-Alias -Name IGit -Value Invoke-Git -ErrorAction SilentlyContinue

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