PowerShell:捕获外部进程写入变量中的stderr的输出

dxxyhpgq  于 2023-08-05  发布在  Shell
关注(0)|答案(2)|浏览(123)

我需要将外部进程的输出捕获到一个变量(字符串)中,这样我就可以对它进行一些处理。
只要进程写入stdout,来自here的答案就能很好地工作。但是,如果进程失败,它将写入stderr。我也想捕获这个字符串,但我不知道该怎么做。
示例如下:

$cmdOutput = (svn info) | out-string

字符串
除非SVN有错误,否则这是可行的。如果发生错误,SVN会写入stderr,$cmdOutput为空。
如何在PowerShell的变量中捕获写入stderr的文本?

0ejtzxu1

0ejtzxu11#

试试这个:

$cmdOutput = svn info 2>&1

字符串

cvxl0en2

cvxl0en22#

为了补充manojlds' helpful answer的概述:
简单解释一下重定向表达式2>&1
2>重定向(>)PowerShell的 error 输出流,其编号为2(并Map到标准错误)到(&)PowerShell的 success 输出流,其编号为1(并Map到标准输出)。
运行Get-Help about_Redirection了解更多信息。

字符编码注意事项,适用于以下所有解决方案:

  • 从外部程序捕获输出或将其传输到另一个命令总是会使其 * 解码为.NET字符串 *,这可能会显示给定程序使用的实际字符编码与PowerShell用于解码的字符编码之间的不匹配,如[Console]::OutputEncoding所反映的那样,它本身默认为控制台的活动代码页(如chcp所报告的那样)。
  • 因此,您可能必须(临时)将[Console]::OutputEncoding设置为实际编码,以确保输出中非ASCII字符的正确解释-有关详细信息,请参阅this answer

捕获stdout和stderr输出 combined,作为合并流:

*作为输出行的集合 * 作为字符串 ,无法区分哪一行来自哪个流:

使用平台原生shell在源代码中执行合并 *,使PowerShell只能看到 stdout 输出,像往常一样,它收集在字符串数组中。
在下面的示例中,每个命令都创建stdout和stderr输出。
为了方便和简洁,所有命令都使用PSv 3+语法。

  • Windows* 示例:
# Collect combined output across both streams as an array of
# strings, where each string represents and output line.
# Note the selective quoting around & and 2>&1 to make sure they
# are passed through to cmd.exe rather than PowerShell itself interpreting them.
$allOutput = cmd /c ver '&' dir \nosuch '2>&1'

字符串

  • Unix* 示例(PowerShell Core):
# sh, the Unix default shell, expects the entire command as a *single* argument.
$allOutput = sh -c '{ date; ls /nosuch; } 2>&1'


注意事项:

  • 您确实需要显式调用平台原生shell(Windows上的cmd /c,Unix上的sh -c),这使得这种方法的可移植性较差。
  • 您将无法从结果行数组中分辨出哪一行来自哪一个流。

下面我们来看看如何进行这种区分。

作为 string lines(来自stdout)和[System.Management.Automation.ErrorRecord]“lines”(来自stderr)的混合:

通过使用 *PowerShell的 * 2>&1重定向,您还可以获得表示合并的stdout和stderr流的单个行流,但 stderr 行不会被捕获为 strings,而是作为[System.Management.Automation.ErrorRecord]示例。

注意事项:Windows PowerShell v5.1中的一个bug(自PowerShell (Core) 7+中修复后)在$ErrorActionPreference = 'Stop'生效时使用2>重定向PowerShell的错误流时会导致意外行为-请参阅this GitHub issue

这使您可以通过检查每个输出对象的数据类型来灵活地区分stdout和stderr行。
另一方面,您可能必须将stderr行转换为字符串。
PowerShell (Core) 7+中,当你只是 * 显示 * 捕获的输出时,不同的数据类型并不明显,但是你可以通过反射来分辨:

# Let PowerShell merge the streams with 2>&1, which captures
# stdout lines as strings and stderr lines as [System.Management.Automation.ErrorRecord] 
# instances.
$allOutput = cmd /c ver '&' dir \nosuch 2>&1


检查结果(%ForEach-Object cmdlet的内置别名):

PS> $allOutput | % GetType | % Name
String
String
String
String
String
String
ErrorRecord


可以看到,最后一个数组元素是一个错误记录,它表示dir \nosuch命令生成的单个File Not Found stderr输出行。
注意事项:

  • Windows PowerShell 中,当您将捕获的输出输出到控制台时,[System.Management.Automation.ErrorRecord]示例实际上以与PowerShell错误相同的格式呈现,可能会使其看起来好像发生了错误 * 然后 *。

PowerShell(Core)7+ 中,这些错误记录只打印它们的消息,即原始stderr行的内容,并且 * 视觉上 * 您无法分辨正在打印的是错误记录而不是字符串。
将所有捕获的输出**转换为 * 字符串 *(为简洁起见,%,使用ForEach-Object的内置别名;实际上,.ToString()方法在每个输出对象上被调用):

$allOutput = cmd /c ver '&' dir \nosuch 2>&1 | % ToString

过滤掉stderr行(并在此过程中将其转换为字符串;为简洁起见,使用?,使用Where-Object的内置别名):

$allOutput = cmd /c ver '&' dir \nosuch 2>&1
$stderrOnly = 
  $allOutput | 
  ? { $_ -is [System.Management.Automation.ErrorRecord] } | 
  % ToString

单独捕获stderr输出:

使用临时 * 文件*:

从PSv5.1开始,单独捕获stderr输出的唯一直接方法是使用带有 filename 目标的重定向2>;即,捕获stderr输出-作为文本-在 * 文件 * 中:

$stderrFile = New-TemporaryFile # PSv5+; PSv4-: use [io.path]::GetTempFileName()
$stdoutOutput = cmd /c ver '&' dir \nosuch 2>$stderrFile
$stderrOutput = Get-Content $stdErrFile
Remove-Item $stderrFile


显然,这是麻烦的,也比内存中的操作慢。

使用PSv 4+内部.Where()数组方法

PSv 4 + intrinsic .Where()数组方法允许您根据元素是否通过布尔测试将集合一分为二:

# Merge the streams first, so that stderr too goes to the success stream, 
 # then separate the objects in the merged stream by type.
 $stdoutOutput, [string[]] $stderrOutput = 
   (cmd /c ver '&' dir \nosuch 2>&1).Where({ $_ -is [string] }, 'Split')


注意事项:

  • [string[]]类型约束将构成stderr输出的[System.Management.Automation.ErrorRecord]示例转换为字符串数组。

  • .Where()发出的两个输出对象是 always 集合(类似数组,类型为[System.Collections.ObjectModel.Collection[psobject]]),即使它们只包含 * 一个 * 元素。

  • 虽然这种方法既方便又简洁,但其缺点是stdout输出也会首先在内存中完整收集,并且 * 然后 * 必须输出以便打印到控制台(如果需要),这否定了PowerShell通常的输出 * 流 * 的好处:中继输出线 *,因为它们正在被接收 *;如果stdout不需要作为一个整体收集在内存中,除了可能浪费内存之外,这在长期运行的外部程序中可能是不希望的,因为持续的视觉反馈很重要;请参阅下一个解决方案。
    将stderr输出的内存捕获与 streaming stdout结合使用:

这种方法避免了.Where()解决方案的collect-all-stdout-first行为,而是在接收到stdout行时对其进行流式传输:

$stderr = [Collections.Generic.List[string]]::new()

# Merge the streams first, so that stderr too goes to the success stream, 
# then decide based on the type whether to pass the line through (stdout)
# or to collect them in list $stderr.
cmd /c ver '&' dir \nosuch 2>&1 | 
  % { if ($_ -is [string]) { $_ } else { $stderr.Add($_) } }

未来可能的改进:

GitHub issue #4332建议扩展重定向语法,以便更简单地在 variable 中收集stderr行:

# As of v7.2.x: WISHFUL THINKING
 # Collect stderr lines in variable $stderr
 $stdout = cmd /c ver '&' dir \nosuch 2>variable:stderr

相关问题