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

283
我想在PowerShell中运行外部进程并将其命令输出捕获到一个变量中。目前我正在使用以下命令:
$params = "/verify $pc /domain:hosp.uhhg.org"
start-process "netdom.exe" $params -WindowStyle Hidden -Wait

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


5
首要注意:不要使用“Start-Process”同步执行(根据定义是外部的)控制台应用程序,只需像在任何 shell 中一样直接调用它们,例如:“netdom /verify $pc /domain:hosp.uhhg.org”。这样做可以使应用程序连接到调用控制台的标准流,从而使其输出可以通过简单的赋值“$output = netdom ...”来捕获。下面给出的大多数答案都隐含放弃了“Start-Process”,而选择直接执行。 - mklement0
1
除非有人想使用“-Credential”参数,否则@mklement0的建议是不需要的。 - CJBS
@CJBS 是的,为了使用不同的用户身份运行,请务必使用Start-Process -但只有在这种情况下才需要(如果您想在单独的窗口中运行命令)。并且在这种情况下应该意识到不可避免的限制:除非通过-RedirectStandardOutput-RedirectStandardError以-_文本_文件形式-捕获输出,否则没有捕获输出的能力。 - mklement0
11个回答

364
Note: 问题中的命令使用了 Start-Process,这会阻止直接捕获目标程序的输出。通常情况下,不要使用 Start-Process 同步执行控制台应用程序 - 直接调用它们即可,就像在任何 shell 中一样。这样做可以保持应用程序的输出流与 PowerShell 的流连接,使得它们的输出可以通过简单的赋值 $output = netdom ...(使用 2> 输出stderr),如下所述。
外部程序中捕获输出的基本原理与使用 PowerShell 本地命令相同(您可能需要回顾一下 如何执行外部程序<command> 是下面任何有效命令的占位符)。
# IMPORTANT: 
# <command> is a *placeholder* for any valid command; e.g.:
#    $cmdOutput = Get-Date
#    $cmdOutput = attrib.exe +R readonly.txt
$cmdOutput = <command>   # captures the command's success stream / stdout output

请注意,如果<command>产生多个输出对象,则$cmdOutput 会接收到一个对象的数组,对于外部程序而言,这意味着一个包含程序输出字符串[1]数组。

如果您想确保结果始终是一个数组,即使只有一个对象被输出,可以将变量类型限定为数组([object[]]),或者将命令括在@(...)中,使用数组子表达式运算符[2]

[array] $cmdOutput = <command>
$cmdOutput = @(<command>)       # alternative

相比之下,如果您希望$cmdOutput始终接收一个单一的 - 可能是多行的 - 字符串,请使用Out-String,但请注意通常会添加一个尾随换行符GitHub问题#14444讨论了这种有问题的行为):
# Note: Adds a trailing newline.
$cmdOutput = <command> | Out-String

如果在PowerShell中调用外部程序,由于其只返回字符串[1],您可以使用-join运算符来避免这种情况:

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

注意:为了简化,上述内容使用"`n"来创建Unix风格的仅包含LF的换行符,PowerShell在所有平台上都可以愉快地接受它;如果您需要平台适当的换行符(Windows上的CRLF,Unix上的LF),请改用[Environment] ::NewLine
为了将输出内容捕获到一个变量中并在屏幕上打印:

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

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

<command> -OutVariable cmdOutput   # cmdlets and advanced functions only

请注意,使用-OutVariable时,与其他情况不同,即使只输出一个对象,$cmdOutput始终是集合。具体来说,返回类似数组的[System.Collections.ArrayList]类型的实例。
有关此差异的讨论,请参见this GitHub问题
为了捕获来自多个命令的输出,可以使用子表达式($(...))或者使用&或者.调用脚本块({ ... }):
$cmdOutput = $(<command>; ...)  # subexpression

$cmdOutput = & {<command>; ...} # script block with & - creates child scope for vars.

$cmdOutput = . {<command>; ...} # script block with . - no child scope

请注意,需要在引用的命令名称/路径前加上&(调用运算符)的通用需求 - 例如,$cmdOutput = & 'netdom.exe' ... - 不是与外部程序本身相关(它同样适用于PowerShell脚本),而是语法要求:PowerShell默认在表达式模式下解析以引号括起来的字符串开头的语句,而参数模式需要调用命令(cmdlets、外部程序、函数、别名),这就是&所确保的。 $(...)& { ... }/. { ... }之间的主要区别在于前者在返回整个输入之前在内存中收集所有输入,而后者流式传输输出,适合逐一进行管道处理。

重定向基本上也是一样的(但请参见下面的注意事项):

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

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

$cmdOutput = cmd /c <command> '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 代码页。

      • 有关更多信息,请参见 this answerthis answer 讨论了截至本文写作时仍处于测试阶段的 Windows 10 功能,该功能允许您将 UTF-8 设置为整个系统的 ANSI 和 OEM 代码页。

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

  • 如果您希望将输出作为一个单独的、潜在的多行字符串,请使用 -join 运算符 (您也可以将其管道传输到 Out-String,但这会不可避免地添加一个尾随换行符):
    $cmdOutput = (<command>) -join [Environment]::NewLine

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

    • 要在源上执行此操作cmd.exe 处理重定向,使用以下惯用法(与 Unix-like 平台上的 sh 类似):
      $cmdOutput = cmd /c <command> '2>&1' # *array* of strings (typically)
      $cmdOutput = (cmd /c <command> '2>&1') -join "`r`n" # single string

      • cmd /c 使用指定的命令 <command> 调用 cmd.exe,并在执行完 <command> 后退出。

      • 注意单引号包围 2>&1,这确保了重定向被传递到 cmd.exe 而不是由 PowerShell 解释。

      • 请注意,涉及 cmd.exe 意味着其转义字符和扩展环境变量的规则会生效,默认情况下除了 PowerShell 自己的要求之外; 在 PS v3+ 中,您可以使用特殊参数 --% (所谓的停止解析符号)将剩余参数的解释权彻底移交给 PowerShell 之外的程序,但 cmd.exe

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

        关于参数传递的说明,截至PowerShell 7.2.x:

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

        • 此外,像msiexec.exe和批处理文件等可执行文件的(非标准)引号需求也无法满足。

        仅针对前一个问题,可能会有解决方法(尽管在类Unix平台上的解决方法是完整的),如this answer所述,该答案还详细介绍了所有当前问题和解决方法。

        如果安装第三方模块是一种选择,则Native moduleInstall-Module Native)中的ie函数提供了一种全面的解决方案。


        [1] 截至 PowerShell 7.1 版本,PowerShell 与外部程序通信时仅识别字符串。在 PowerShell 管道中通常不存在原始字节数据的概念。如果您想从外部程序返回原始字节数据,则必须使用 cmd.exe /c(Windows)或 sh -c(Unix)来调用外壳程序,在那里保存文件,然后在 PowerShell 中读取该文件。有关详细信息,请参见 this answer

        [2] 这两种方法之间存在微妙的差异(您可以将它们组合在一起),尽管它们通常不重要:如果命令没有输出,则 [array] 类型约束方法会导致将 $null 存储在目标变量中,而在 @(...) 的情况下则是一个空的([object[]])数组。此外,[array] 类型约束意味着将来对同一变量进行的(非空)赋值也会被强制转换为数组。


2
@Dan:当PowerShell本身解释<command>时,您不应该将可执行文件和其参数组合在单个字符串中;通过cmd /c调用时,您可以这样做,具体取决于情况是否有意义。您指的是哪种情况?能否给出一个最简示例? - mklement0
1
@Dan:是的,那个可以用,不过你不需要中间变量和使用+运算符显式构造字符串;以下也可以:cmd /c c:\mycommand.exe $Args '2>&1' - 在这种情况下,PowerShell会将$Args的元素作为空格分隔的字符串传递,这是一种名为splatting的特性。 - mklement0
1
终于有一个在PS6.1+中有效的答案了。秘密确实在于'2>&1'这一部分,而不是像许多脚本那样用()括起来。 - not2qubit
谢谢你的帮助!#-es被发送到了错误输出(stderr)。这很令人困惑,尤其是它们似乎与标准输出(stdout,即ssh-keyscan的输出)在同一行中。 - Johannes Schaub - litb
@JohannesSchaub-litb,是的,使用直接输出到终端的方式,标准输出(stdout)和标准错误(stderr)流会_交错_在一起,因此你无法确定每一行来自哪个流。要以_编程方式_捕获这种交错输出,你需要将标准错误合并到标准输出中(2>&1),使得标准输出映射到PowerShell的_成功_输出流(这种合并的推广形式也适用于所有PowerShell的输出流,即 *>&1)。与标准输出/成功输出不同的流的输出被包装在特定流的_对象_中,关于标准错误/PowerShell的错误流已在答案中讨论过。 - mklement0
显示剩余8条评论

192

你尝试过这个吗:

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


13
我不理解这里正在发生什么,也无法让其正常运作。 "Shell" 是一个 PowerShell 关键字吗?所以我们实际上不使用 Start-Process cmdlet 吗?您能否举个具体的例子(即用真实的示例替换“Shell”和/或“command”)? - deadlydog
3
@deadlydog 将“Shell Command”替换为您想要运行的任何内容。就是这么简单。 - JNK
不适用于ffmpeg命令。FFmpeg输出仍然会发送到标准输出而不是变量。 - Atiq Rahman
1
@stej,你说得对。我主要是想澄清你评论中的代码与答案中的代码具有不同的功能。像我这样的初学者可能会被这些微妙的行为差异所迷惑! - Sam
1
@Atique,我遇到了同样的问题。原来 ffmpeg 有时会将输出写入 stderr 而不是 stdout,例如,如果你使用 -i 选项而没有指定输出文件。按照其他答案中所述的方式重定向输出,使用 2>&1 即可解决。 - jmbpiano
显示剩余8条评论

37

如果你想同时重定向错误输出,你需要这样做:

$cmdOutput = command 2>&1

或者,如果程序名称中有空格:

$cmdOutput = & "command with spaces" 2>&1

6
2>&1 的意思是将命令 2 的输出放到命令 1 中运行。 - Richard
11
这句话的意思是“将标准错误输出(文件描述符2)重定向到标准输出(文件描述符1)所在的位置”。简单来说,就是把正常信息和错误信息一起重定向到同一个地方(比如控制台,如果标准输出没有被重定向到其他地方,比如文件)。 - Giovanni Tirloni

12

或者尝试这个。它会将输出捕获到变量 $scriptOutput 中:

& "netdom.exe" $params | Tee-Object -Variable scriptOutput | Out-Null

$scriptOutput

10
-1,过于复杂。$scriptOutput = & "netdom.exe" $params 的意思是运行名为"netdom.exe"的程序,并使用参数$params,并将输出结果存储在变量$scriptOutput中。 - CharlesB
9
移除"out-null"对于同时将输出传输到 shell 和变量中非常有帮助。 - ferventcoder

11

另一个现实生活中的例子:

$result = & "$env:cust_tls_store\Tools\WDK\x64\devcon.exe" enable $strHwid 2>&1 | Out-String

请注意此示例包括一个路径(以环境变量开头)。注意引号必须包围路径和EXE文件,但不包括参数!

注意:不要忘记命令前面的&字符,但在引号之外。

还会收集错误输出。

我花了一段时间才让这个组合工作,所以我想分享一下。


11

我尝试了那些答案,但在我的情况下,我没有得到原始输出。相反,它被转换为 PowerShell 异常。

我得到的原始结果是:

$rawOutput = (cmd /c <command> 2`>`&1)

5

我使用以下技术:

Function GetProgramOutput([string]$exe, [string]$arguments)
{
    $process = New-Object -TypeName System.Diagnostics.Process
    $process.StartInfo.FileName = $exe
    $process.StartInfo.Arguments = $arguments
    
    $process.StartInfo.UseShellExecute = $false
    $process.StartInfo.RedirectStandardOutput = $true
    $process.StartInfo.RedirectStandardError = $true
    $process.Start()
    
    $output = $process.StandardOutput.ReadToEnd()   
    $err = $process.StandardError.ReadToEnd()
    
    $process.WaitForExit()
    
    $output
    $err
}
    
$exe = "C:\Program Files\7-Zip\7z.exe"
$arguments = "i"
    
$runResult = (GetProgramOutput $exe $arguments)
$stdout = $runResult[-2]
$stderr = $runResult[-1]
    
[System.Console]::WriteLine("Standard out: " + $stdout)
[System.Console]::WriteLine("Standard error: " + $stderr)

4

我已经使以下内容正常工作:

$Command1="C:\\ProgramData\Amazon\Tools\ebsnvme-id.exe"
$result = & invoke-Expression $Command1 | Out-String

$result 能给你所需的


通常应避免使用Invoke-Expression(https://dev59.com/5azla4cB1Zd3GeqPBsLr#51252636);绝对不要使用它来调用外部程序或PowerShell脚本(https://dev59.com/DLbna4cB1Zd3GeqPZls5#57966347)。 - mklement0

2
这个东西对我很有用:
$scriptOutput = (cmd /s /c $FilePath $ArgumentList)

1
如果你只想捕获命令的输出,那么这个方法很有效。我用它来更改系统时间,因为[timezoneinfo]::local总是会产生相同的信息,即使在你更改系统之后。这是我验证和记录时区更改的唯一方式。
$NewTime = (powershell.exe -command [timezoneinfo]::local)
$NewTime | Tee-Object -FilePath $strLFpath\$strLFName -Append

意思是我必须打开一个新的 PowerShell 会话来重新加载系统变量。

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