In PowerShell, how can I get stdout and stderr from a native command interleave correctly when redirecting stderr to stdout?

huangapple go评论116阅读模式
英文:

In PowerShell, how can I get stdout and stderr from a native command interleave correctly when redirecting stderr to stdout?

问题

在(Windows)PowerShell中,导致这种行为的原因是输出流(stdout和stderr)的异步处理方式以及它们的缓冲机制。默认情况下,PowerShell将stdout和stderr流分别缓冲并异步处理,这可能导致它们的输出顺序不同于命令行中指定的顺序。

要更改此行为,您可以使用[Console]::Out.Flush()[Console]::Error.Flush()方法来刷新stdout和stderr的缓冲区,以确保它们按照命令行中的顺序输出。以下是如何在PowerShell中使用这些方法:

# 执行命令并刷新stdout和stderr缓冲区
.\output.exe -o 1 -e 2 -o 3
[Console]::Out.Flush()
[Console]::Error.Flush()

这将强制stdout和stderr按照命令行中指定的顺序进行输出。

请注意,这种行为在不同版本的PowerShell和操作系统中可能会有所不同,因此您可能需要根据您的具体环境进行适当的测试和调整。

英文:

Given a native command output(.exe) that writes output to stdout and/or stderr in the order specified on the command-line.

PowerShell 7.2 on macOS:

PS> ./output -o 1 -e 2 -o 3
1
2
3
PS> ./output -o 1 -e 2 -o 3 2>&1
1
3
2

Compare with Zsh on macOS:

% ./output -o 1 -e 2 -o 3
1
2
3
% ./output -o 1 -e 2 -o 3 2>&1
1
2
3

Windows PowerShell 5.1 (stderr is formatted differently when it's redirected, but other than that, order is the same as PowerShell 7.2 on macOS):

PS> .\output.exe -o 1 -e 2 -o 3
1
2
3
PS> .\output.exe -o 1 -e 2 -o 3 2>&1
1
3
./output.exe : 2
At line:1 char:1
+ ./output.exe -o 1 -e 2 -o 3 2>&1
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (2:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

Windows Command Processor (cmd.exe):

C:\> .\output.exe -o 1 -e 2 -o 3
1
2
3
C:\> .\output.exe -o 1 -e 2 -o 3 2>&1
1
2
3

What causes this behaviour in (Windows) PowerShell, and is there a way to change this behaviour?

答案1

得分: 1

以下是翻译好的内容:

tl;dr

  • 您正在看到PowerShell中的一个设计权衡的不幸后果,详细讨论请参阅GitHub问题#5424

  • 由于下面总结的设计选择,当将_外部程序_的stderr输出与PowerShell的成功输出流合并使用2>&1时,无法保证原始输出顺序

  • 解决方法:使用**_本机Shell的_重定向功能**,它确实保证输出顺序(有关其他信息,请参见底部部分):

    • 类Unix平台:

      /bin/sh -c './output -o 1 -e 2 -o 3 2>&1'
      
    • Windows:

      cmd /c '.\output -o 1 -e 2 -o 3 2>&1'
      

PowerShell行为摘要:

当PowerShell重定向外部程序的stderr流 - 这对应于PowerShell的松散类似的错误(输出)流2),将其合并到其成功(输出)流(1,PowerShell的类似stdout的流)中,通过重定向 2>&1*>&1(仅对于_外部_程序而言,这两者是等效的),它仍然 分别 捕获外部程序的输出流,然后将它们发送到同一个输出流。

这允许将各自流的输出作为具有不同(.NET)数据类型的对象发送到成功输出流(到管道):

  • _Stdout_行作为字符串(System.String)发送。

    • 请注意,至少到v7.3,PowerShell不变地将外部程序的输出解析为文本,即根据存储在[Console]::OutputEncoding]中的字符编码将其解析为表示输出的.NET字符串,对于Windows来说,这反映了当前控制台的输出代码页。有关更多信息,请参阅此答案
  • _Stderr_行作为字符串包装在System.Management.Automation.ErrorRecord实例中,即包装在PowerShell用于表示自己的错误的相同类型中。

这种方法的优势是:

  • 您可以在内存中捕获stderr行(截至v7.3.x,否则无法做到;只能将它们发送到文件)。

    • 但是,GitHub问题#4332提出了引入语法的提案,该语法允许通过诸如2>variable:stdErrOutput之类的重定向有选择地变量中捕获stderr输出(以在变量$stdErrOutput中捕获)
  • 您稍后可以通过它们的数据类型将stdout产生的行与stderr产生的行分开。

有关详细信息和示例代码,请参阅此答案

通过分别捕获流,通过分开(系统)管道在组合它们之前相对输出顺序无法保证(这似乎是这种管道的固有限制)的成本

在程序输出很长的情况下,这是有问题的,在这种情况下,通常需要在stdout输出之前立即看到stderr输出_上下文中_


通过本机Shell进行解决:

由于本机平台Shell(类Unix平台的/bin/shcmd.exe确实保证顺序,即通过在源处_合并流_ - 导致字节的单一、统一输出流,调用_它们_,使用_它们的_(类似的)重定向功能解决了问题(请参阅顶部部分),代价是:

  • 无法知道哪些行来自哪个流。

  • 额外子进程的开销。

  • 需要了解本机Shell的语法规则,与PowerShell的不同。

  • 使_跨平台_解决方案困难,因为不同的路径分隔符(/\)和引用语法;应用于您的调用:

    $shell = & ('/bin/sh', 'cmd')[$env:OS -eq 'Windows_NT']
    $opt = ('-c', '/c')[$env:OS -eq 'Windows_NT']
    $pathSep = ('/', '\')[$env:OS -eq 'Windows_NT']
    $cmdLine = '.{0}output -o 1 -e 2 -o 3 2>&1' -f $pathSep
    & $shell $opt $cmdLine
    
  • 在相关注意中,需要使用_字符串插值_将PowerShell变量和表达式嵌入到传递给本机Shell的命令行中,使用可扩展的(双引号括起的)字符串("...")【](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Quoting_Rules#double-

英文:

tl;dr

  • You'e seeing the unfortunate consequence of a design trade-off in PowerShell, discussed in detail in GitHub issue #5424.

  • As a result of the design choices summarized below, when merging an external program's stderr output into PowerShell's success output stream with 2>&1, the original output ordering cannot be guaranteed.

  • Workaround: Use the native shell's redirection features, which do guarantee output ordering (see bottom section for additional information):

    • Unix-like platforms:

      /bin/sh -c './output -o 1 -e 2 -o 3 2>&1'
      
    • Windows:

      cmd /c '.\output -o 1 -e 2 -o 3 2>&1'
      

Summary of PowerShell's behavior:

When PowerShell redirects an external program's stderr stream - which maps onto PowerShell's loosely analogous error (output) stream (2) into its success (output) stream (1, PowerShell's analog to stdout) via a redirection 2>&1 or *>&1 (the two are equivalent only for external programs), it still captures the external program's output streams separately, before sending them to the same output stream.

This allows it to send the respective stream's output as objects with distinct (.NET) data types to the success output stream (to the pipeline):

  • Stdout lines are sent as strings (System.String).

    • Note that, up to at least v7.3, PowerShell invariably parses an external program's output as text,<sup>[1]</sup> i.e. it parses it into .NET strings representing the lines of output based on the character encoding stored in [Console]::OutputEncoding], which on Windows reflects the current console's output code page. See this answer for more information.
  • Stderr lines are sent as strings wrapped in System.Management.Automation.ErrorRecord instances, i.e. wrapped in the same type PowerShell uses to represent its own errors.

The potential advantage of this approach is:

  • You can capture stderr lines in memory (which as of v7.3.x isn't otherwise possible; you can only send them to a file).

    • However, GitHub issue #4332 proposes introducing syntax that would allow capturing stderr output selectively in variables via a redirection such as 2&gt;variable:stdErrOutput (to capture in variable $stdErrOutput)
  • You can later separate stdout-originating lines from stderr-originating ones by their data type.

See this answer for details and sample code.

The cost of capturing the streams separately, via separate (system) pipes, before combining them is that their relative output ordering cannot be guaranteed (which appears to be an inherent limitation of such pipes).

This is problematic in lengthy program output, where you generally do want to see stderr output in context, namely right next to stdout output that was emitted just before.


Workaround via the native shells:

Since the platform-native shells (/bin/sh on Unix-like platforms, cmd.exe) do guarantee ordering, namely by merging the streams at the source - resulting in a single, unified output stream of bytes, calling them, using their (analogous) redirection features solves the problem (see top section), at the expense of:

  • Being able to tell which lines came from which stream.

  • The overhead of an extra child process.

  • Needing to know the native shell's syntax rules, which differ from PowerShell's.

  • Making cross-platform solutions difficult, due to differing path separators (/ vs. \) and quoting syntax; applied to your call:

    $shell = &amp; (&#39;/bin/sh&#39;, &#39;cmd&#39;)[$env:OS -eq &#39;Windows_NT&#39;]
    $opt = (&#39;-c&#39;, &#39;/c&#39;)[$env:OS -eq &#39;Windows_NT&#39;]
    $pathSep = (&#39;/&#39;, &#39;\&#39;)[$env:OS -eq &#39;Windows_NT&#39;]
    $cmdLine = &#39;.{0}output -o 1 -e 2 -o 3 2&gt;&amp;1&#39; -f $pathSep
    &amp; $shell $opt $cmdLine
    
  • On a related note, needing to use string interpolation to embed PowerShell variables and expressions in the command line passed to the native shell, using an expandable (double-quoted) string (&quot;...&quot;)

    • This in turn can necessitate use of embedded quoting, which up to PowerShell 7.2.x is broken with respect to embedded double-quoting (&quot;...&quot;) - see this answer

    • With cmd.exe only (Windows), you can often - but not always - mitigate this problem by passing the parts of the command line as individual arguments, making sure that 2&gt;&amp;1 and cmd.exe metacharacters in general are individually quoted; e.g.

      cmd /c echo $HOME &#39;&amp;&#39; dir /b nosuch &#39;2&gt;&amp;1&#39;
      

<sup>[1] Preview versions of PowerShell (Core) v7.4 have an experimental feature named PSNativeCommandPreserveBytePipe that treats &gt; and | when applied to external (native) programs as raw byte conduits, i.e. it bypasses the usual string-decoding and re-encoding cycle in favor of passing the raw data through.
(Note that, as with any experimental feature, it isn't guaranteed to become a stable feature.) However, the raw-bytes behavior by design does not apply when using a stderr redirection.
</sup>

huangapple
  • 本文由 发表于 2023年8月5日 13:27:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/76840261.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定