如何在PowerShell中从外部进程捕获输出并存储到变量中?

19 浏览
0 Comments

如何在PowerShell中从外部进程捕获输出并存储到变量中?

我想在PowerShell中运行一个外部进程并将其命令输出捕获到一个变量中。我目前正在使用以下命令:

$params = "/verify $pc /domain:hosp.uhhg.org"
start-process "netdom.exe" $params -WindowStyle Hidden -Wait

我已确认命令正在执行,但我需要将输出捕获到一个变量中。这意味着我不能使用-RedirectOutput,因为这只能重定向到一个文件。

admin 更改状态以发布 2023年5月21日
0
0 Comments

你试过这样吗:

$OutputVariable = (Shell命令) | Out-String

0
0 Comments

注:问题中的命令使用了Start-Process,这会阻止直接捕获目标程序的输出。通常,不要使用Start-Process来同步执行控制台应用程序 - 直接调用它们,就像在任何Shell中一样。这样做可以使应用程序的输出流连接到PowerShell的流,允许它们的输出通过简单的赋值$output = netdom ...(并使用2>获取stderr输出)来捕获,如下所述。

基本上,从外部程序捕获输出的工作方式与使用PowerShell本地命令相同(你可能需要复习一下如何执行外部程序是下面任何有效命令的占位符):

# IMPORTANT: 
#  is a *placeholder* for any valid command; e.g.:
#    $cmdOutput = Get-Date
#    $cmdOutput = attrib.exe +R readonly.txt
$cmdOutput =    # captures the command's success stream / stdout output

请注意,如果产生多个输出对象,$cmdOutput 将接收一个对象数组,在外部程序的情况下,这意味着一个包含程序输出行的string[1]数组。

如果你希望确保结果始终是一个数组,即使只有一个对象输出,请将变量类型限制为数组([object[]]),或将命令括在@(...)中,使用数组子表达式运算符:[2]

[array] $cmdOutput = 
$cmdOutput = @()       # alternative

相比之下,如果您想让 $cmdOutput 始终接收一个单一的(可能是多行的)字符串,请使用 Out-String,但请注意,总是会添加一个尾随换行符GitHub 问题#14444 对这种问题行为进行了讨论):

# Note: Adds a trailing newline.
$cmdOutput =  | Out-String

通过调用外部程序 - 这种程序在 PowerShell 中定义为仅返回字符串[1] - ,您可以改用 -join 运算符 避免这种情况:

# NO trailing newline.
$cmdOutput = () -join "`n"

注意:为了简单起见,上面使用 "`n" 来创建 Unix 风格的 LF-only 换行符,PowerShell 在所有平台上都可以接受;如果您需要平台适当的换行符(Windows 上的 CRLF,Unix 上的 LF),请改用 [Environment]::NewLine


捕获输出到一个变量并打印到屏幕上

 | Tee-Object -Variable cmdOutput # Note how the var name is NOT $-prefixed

或者,如果 是一个 cmdlet 或高级函数,您可以使用常见参数
-OutVariable / -ov

 -OutVariable cmdOutput   # cmdlets and advanced functions only

请注意,与其他情况不同,-OutVariable 中的 $cmdOutput 总是一个集合,即使只有一个对象被输出。具体来说,返回的是一个类似于数组的 [System.Collections.ArrayList] 类型的实例。
请参阅 此 GitHub 问题,以了解这种差异的讨论。

为了捕获多个命令的输出,可以使用子表达式($(...))或调用带有&.的脚本块({...}):

$cmdOutput = $(; ...)  # subexpression
$cmdOutput = & {; ...} # script block with & - creates child scope for vars.
$cmdOutput = . {; ...} # script block with . - no child scope

请注意,需要在带引号的个别命令(例如$cmdOutput = & 'netdom.exe' ...)之前加上& (调用操作符)不仅适用于外部程序(同样适用于PowerShell脚本),这是一个语法要求:PowerShell默认在表达式模式下解析以引号括起来的语句,而调用命令(cmdlets、外部程序、函数、别名)需要参数模式,这就是&的作用。

$(...){...} / .{...}之间的主要区别在于前者在返回之前将所有输入收集到内存中,然后作为整体返回,而后者将输出流式传输,适合逐个管道处理。


重定向的基本原理也是相同的(但请注意下面的注意事项):

$cmdOutput =  2>&1 # redirect error stream (2) to success stream (1)

然而,对于外部命令,以下方式更可能按预期工作:

$cmdOutput = cmd /c  '2>&1' # Let cmd.exe handle redirection - see below.


特定于外部程序的注意事项:

  • 外部程序由于在PowerShell的类型系统之外操作,只通过其成功流(stdout)返回字符串;同样,PowerShell仅通过管道向外部程序发送字符串([1])。

    • 字符编码问题也可能会出现:
      • 在通过管道向外部程序发送数据时,PowerShell使用存储在$OutVariable首选项变量中的编码;在Windows PowerShell中默认为ASCII(!),而在PowerShell [Core]中默认为UTF-8。

      • 在从外部程序接收数据时,PowerShell使用存储在[Console]::OutputEncoding中的编码解码数据,这两个PowerShell版本都默认为系统的活动OEM代码页。

      • 有关更多信息,请参见此答案此答案讨论了仍处于测试版(在本篇文章撰写时)的Windows 10功能,该功能允许您在系统范围内将UTF-8设置为ANSI和OEM代码页。

    • 如果输出包含多行,PowerShell默认将其拆分为字符串数组。更准确地说,输出行逐一流式传输,捕获时存储在类型为[System.Object[]]、元素为字符串([System.String])的数组中。

    • 如果你希望输出为单个、可能有多行的字符串,请使用-join运算符(你也可以使用管道符号Out-String,但这总会添加一个尾随换行符):
      $cmdOutput = () -join [Environment]::NewLine

    • 将stderr合并到stdout中使用2>&1,以便将其作为成功流的一部分捕获,具有注意事项

    • 要在源代码中执行此操作cmd.exe 处理重定向,使用以下语法(在类 Unix 平台中使用 sh 类似):
      $cmdOutput = cmd /c '2>&1' # *string* 数组(通常)
      $cmdOutput = (cmd /c '2>&1') -join "`r`n" # 单个字符串

      • cmd /c 执行 命令,然后在 完成后退出。

      • 请注意在 2>&1 周围使用了单引号,这可以确保将重定向传递给 cmd.exe 而不是被 PowerShell 解释。

      • 请注意参与 cmd.exe 的规则,这意味着默认情况下,它的转义字符和环境变量扩展规则与 PowerShell 自己的要求相结合。在 PS v3+ 中,您可以使用特殊参数 --%(所谓的强制停止解析符号)来关闭 PowerShell 对剩余参数的解释,除了类似于 %PATH%cmd.exe-风格环境变量引用。

      • 请注意,由于这种方法在源头合并了 stdout 和 stderr,所以在 PowerShell 中无法区分 stdout 生成的行和 stderr 生成的行;如果确实需要这种区分,请使用 PowerShell 自己的 2>&1 重定向 - 请见下文。

    • 使用 PowerShell 的 2>&1 重定向来确定哪些行来自哪个流

      • 标准错误 (stderr) 的输出被捕获为 错误记录 ([System.Management.Automation.ErrorRecord]),而不是字符串,因此输出数组可能包含混合的字符串 (每个字符串代表一个 stdout 行) 和错误记录 (每个记录代表一个 stderr 行)。请注意,根据 2>&1 的要求,字符串和错误记录都通过 PowerShell 的成功输出流接收)。

      • 注意:以下内容仅适用于 Windows PowerShell - 这些问题已在 PowerShell [Core] v6+ 中得到纠正,但是下面展示的按对象类型过滤技术 ($_ -is [System.Management.Automation.ErrorRecord]) 在那里也可以很有用。

      • 在控制台中,错误记录以红色打印,第一个错误记录默认情况下会产生多行显示,以与 cmdlet 的非终止性错误相同的格式显示;随后的错误记录也会以红色打印,但只打印其错误消息,仅显示一行。

      • 在输出到控制台时,字符串通常位于输出数组的前面,后跟错误记录 (至少在输出“同时”产生的 stdout/stderr 行批次中),但幸运的是,当您捕获输出时,它会被正确地交错,使用在没有 2>&1 的情况下得到的相同输出顺序;换句话说:在输出到控制台时,捕获的输出不反映外部命令生成 stdout 和 stderr 行的顺序。

      • 如果您使用Out-String将整个输出捕获到单个字符串中,PowerShell会添加额外的行,因为错误记录的字符串表示包含额外的信息,例如位置 (At line:...) 和类别 (+CategoryInfo ...); 奇怪的是,这仅适用于第一个错误记录。

        • 为解决此问题,请对每个输出对象应用.ToString()方法,而不是使用管道传输到Out-String
          $cmdOutput = 2>&1 | % { $_.ToString() }
          在PS v3+中,您可以简化为:
          $cmdOutput = 2>&1 | % ToString
          (作为奖励,如果未捕获输出,则即使在打印到控制台时,也会产生正确交替的输出。)

        • 或者,过滤掉错误记录,并将它们发送到PowerShell的错误流中,使用Write-Error (作为奖励,如果未捕获输出,则即使在打印到控制台时也会产生正确交替的输出):

$cmdOutput =  2>&1 | ForEach-Object {
  if ($_ -is [System.Management.Automation.ErrorRecord]) {
    Write-Error $_
  } else {
    $_
  }
}


关于参数传递的一个除外情况,自PowerShell 7.2.x起:

  • 关于传递参数给外部程序的问题,在处理空字符串参数和包含嵌入"字符的参数时存在问题。

  • 此外,不适应msiexec.exe和批处理文件等可执行文件的(非标准的)引用需求。

只有针对前一个问题,正在考虑解决方法(尽管在类Unix平台上,解决方法已经完善了),可以参考此回答,其中详细介绍了所有当前问题和解决方法。

如果安装第三方模块是一种选择,那么Native模块中的ie函数(Install-Module Native)提供了一个综合解决方案。


[1] 截至PowerShell 7.1,PowerShell只支持与外部程序通信的字符串。通常在PowerShell管道中没有原始字节数据的概念。如果你想从外部程序返回原始字节数据,你必须调用cmd.exe /c(Windows)或sh -c(Unix)并将其保存到文件中,然后在PowerShell中读取该文件。更多信息,请参考这个答案

[2] 两种方法之间有细微的区别(你可以将它们结合起来使用),尽管通常不会影响结果。如果命令没有输出,类型约束方法[array]会将$null存储在目标变量中,而@(...)的情况下则是一个空([object[])数组。此外,[array]类型限制意味着将来对同一变量的(非空)赋值也会被强制转换为数组。

0