Powershell 管道数据到外部控制台应用程序

Powershell Pipeline data to external console application

我有一个可以采用标准输入的控制台应用程序。它缓冲数据直到执行命令,此时它执行所有命令,并将输出发送到标准输出。

目前,我正在 运行 这个来自 Powershell 的应用程序,通过管道将命令输入其中,然后解析输出。输入的数据相对较小;然而,这个应用程序被调用了大约 1000 次。每次执行时,它都必须加载并创建网络连接。我想知道将所有命令管道化到控制台应用程序的单个实例中是否更有效。

我尝试过将所有为控制台生成标准输入的 Powershell 脚本添加到一个函数中,然后将该函数通过管道传输到控制台应用程序。起初这似乎可行,但您最终意识到它正在缓冲 Powershell 中的所有数据,直到函数完成,然后将其发送到控制台的 StdIn。你可以看到这个,因为我有一大堆 Write-Host 语句闪过,然后你才能看到输出。

例如

Function Run-Command1
{
    Write-Host "Run-Command1"
    "GET nethost xxxx COLS id,name"
    "EXEC"
}

Function Run-Command2
{
    Write-Host "Run-Command2"
    "GET nethost yyyy COLS id,name"
    "GET users yyyy COLS id,name"
    "EXEC"
}

...

Function Run-CommandX 
{
...
}

以前,我会将其用作:

Run-Command1 | netapp.exe -connect QQQQ -U user -P password
Run-Command2 | netapp.exe -connect QQQQ -U user -P password
...
Run-CommandX | netapp.exe -connect QQQQ -U user -P password

但现在我想做的是:

Function Run-Commands
{
    Run-Command1
    Run-Command2
    ...
    Run-CommandX
}

Run-Commands |
netapp.exe -connect QQQQ -U user -P password

理想情况下,我希望将 Powershell 管道行为扩展到外部应用程序。这可能吗?

编辑: 正如@mklement0 所指出的,这在 PowerShell Core 中是不同的。

在 PowerShell 5.1(及更低版本)中,您必须手动将每个管道项写入外部应用程序的输入流。

尝试为此构建一个函数:

function Invoke-Pipeline {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string]$FileName,

        [Parameter(Position = 1)]
        [string[]]$ArgumentList,

        [int]$TimeoutMilliseconds = -1,

        [Parameter(ValueFromPipeline)]
        $InputObject
    )
    begin {
        $process = [System.Diagnostics.Process]::Start((New-Object System.Diagnostics.ProcessStartInfo -Property @{
            FileName = $FileName
            Arguments = $ArgumentList
            UseShellExecute = $false
            RedirectStandardInput = $true
            RedirectStandardOutput = $true
        }))
        $output = [System.Collections.Concurrent.ConcurrentQueue[string]]::new()
        $event = Register-ObjectEvent -InputObject $process -EventName 'OutputDataReceived' ` -Action {
             $Event.MessageData.TryAdd($EventArgs.Data)
        } -MessageData $output
        $process.BeginOutputReadLine()
    }
    process {
        $process.StandardInput.WriteLine($InputObject)
        [string]$line = ""
        while (-not ($output.TryDequeue([ref]$line))) {
            start-sleep -Milliseconds 1
        }
        do {
            $line
        } while ($output.TryDequeue([ref]$line))
    }
    end {
        if ($TimeoutMilliseconds -lt 0) {
            $exited = $process.WaitForExit()
        }
        else {
            $exited = $process.WaitForExit($TimeoutMilliseconds)
        }
        if ($exited) {
            $process.Close()
        }
        else {
            try {$process.Kill()} catch {}
        }
    }
}

Run-Commands | Invoke-Pipeline netapp.exe "-connect QQQQ -U user -P password"

问题是,没有完美的解决方案,因为根据定义,您无法知道何时外部程序会向其输出流写入内容,或者写入多少内容。

注意:此函数不会重定向错误流。不过方法是一样的。

I would like the Powershell pipeline behaviour to be extended to an external application.
I have a whole load of Write-Host statements that flash by, and only then do you see the output.

帽子尖到marsze

  • PowerShell [Core] v6+ 执行完全没有缓冲 , 并发送(字符串化)输出 当它被命令生成时 外部程序 ,输出在 [=之间流式传输的方式相同=98=]PowerShell 命令。[1]

  • PowerShell的legacy edition(版本高达5.1),Windows PowerShell, buffers in that它 收集命令的所有输出 first,然后再将其(字符串化)发送到外部程序。

    • 显示了基于直接使用 .NET API 的解决方法。

但是,我认为即使 Windows PowerShell 的行为也不是这里的问题:您的 Run-Commands 函数执行得非常快 - 鉴于它仅调用输出字符串文字的函数 - 然后将生成的行数组一次发送到 netapp.exe - 进一步处理,包括何时产生输出,然后最多 netapp.exe。在 PowerShell [Core] v6+ 中,PowerShell-side 缓冲不在画面中,单个 Run-Commmand<n> 函数的输出将一直发送到 netapp.exe稍微早一点,但我不希望这会有所作为。

结果是除非netapp.exe提供调整其输入和输出缓冲的方法,否则您将无法控制其输入处理和输出产生的时间.


PowerShell 如何通过管道将对象发送到外部程序(本机实用程序):

  • 它发送每个对象的 stringified 表示
    • PowerShell [Core] v6+ 中:随着对象变得可用.
    • in Windows PowerShell: 在内存中收集所有输出对象后first.

换句话说:在PowerShell方面,从v6开始,没有缓冲.[1]

  • 但是,接收外部程序通常 do 缓冲 ​​stdin(标准输入)数据 他们通过管道接收[2].

    • 类似地,外部程序通常 do 缓冲它们的 stdout (标准输出)流(但 PowerShell 在传递输出之前不执行额外的缓冲,例如传递到终端(控制台))。

    • PowerShell 无法控制此行为;外部程序本身提供调整缓冲的选项,或者在 Linux 上的有限情况下,您可以通过 stdbuf utility.[=40 调用外部程序=]


可选阅读:PowerShell 如何 stringifies objects when pipe to external programs:

  • 从 v7.1 开始,PowerShell 在与外部程序通信时只知道 text;即发送这样的程序的数据被转换成文本,从这样的程序输出这样的程序被解释为 文本 - 尽管底层系统 IPC 功能只是 byte 管道。

  • PowerShell 使用的基于 UTF-16 的 .NET 字符串根据 $OutputEncoding preference variable 中指定的字符编码转换为外部程序的字节流,遗憾的是,默认为 ASCII (!) 在 Windows PowerShell 中,现在明智地在 PowerShell [Core] v6+.

    中使用 (BOM-less) UTF-8
    • 换句话说:通过$OutputEncoding指定的编码必须匹配外部程序期望的字符编码。

    • 相反,它是 [Console]::OutputEncoding 中指定的编码,它决定了 PowerShell 如何解释从 外部程序接收到的文本,即它如何将接收到的字节转换为 .NET 字符串,逐行,并去除换行符(当在变量中捕获时,相当于单个字符串,如果只输出一行,或者 array 个字符串)。

  • 你在PowerShell中看到的for-display表示控制台(终端)也是通过管道将 发送到 外部程序,作为文本行,特别是:

    • 如果一个对象(已经)一个字符串(或[char]实例),PowerShell 将它 as-is 发送到管道,但是 总是附加 platform-appropriate 换行符 .

      • 即在Windows上附加一个CRLF换行符,在Unix-like平台上附加一个LF-only换行符。

      • [=172=

        这种行为可能会产生问题,因为在某些情况下您 想要这种行为,并且没有办法阻止它 - 请参阅 GitHub issue #5974, GitHub issue #13579, and 了解解决方法。

    • 如果一个对象,粗略地说,是一个原始类型——概念上是单值,特别是各种数字类型 - 它在[=98=中被字符串化]culture-sensitive 方式,在可用的地方[3]a platform-appropriate 换行符再次总是附加.

      • 例如,有效的法国文化(如 Get-Culture 所示),小数部分 1.2 - PowerShell 将其解析为 [double] 值 -发送为 1,2<newline>.

      • 请注意 [bool] 实例是 而不是 culture-sensitive 并且总是转换为字符串 TrueFalse.

    • 所有其他(复杂)类型都受制于 PowerShell 丰富的 for-display 输出格式,以及您在终端(控制台)中看到的任何内容也是发送到外部程序的内容——它不仅再次可能包含 culture-sensitive 表示,而且通常存在问题,因为这些表示是为人类观察者设计的,而不是为 programmatic处理中。

结果:

  • 注意编码问题 - 确保 $OutputEncoding[Console]::OutputEncoding 设置正确。

  • 为了避免意外的culture-sensitivity和意外的for-display格式化,最好故意构造你要发送的字符串表示.


[1] 默认;但是,您可以通过 common -OutBuffer parameter

显式请求缓冲 - 表示为对象 count

[2] 在最新的 macOS 和 Linux 平台上,stdin 缓冲区大小为 64KB。在 Unix-like 平台上,实用程序通常会在 interactive 调用中切换到 line 缓冲,即当相关流连接到 终端机.

[3] 行为委托给手头类型的 .ToString() 方法,即该方法是否输出 culture-sensitive 表示。