【问题标题】:Powershell Pipeline data to external console applicationPowershell 管道数据到外部控制台应用程序
【发布时间】:2020-11-03 12:30:43
【问题描述】:

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

目前,我正在从 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 管道行为扩展到外部应用程序。这可能吗?

【问题讨论】:

  • 我想这一切都取决于您要通过管道传输到该函数的数据。你可以让它接受一个值数组,也许使用 Splatting 来调用它?我们能看到一些代码吗?
  • 那么你试过你的示例函数了吗?这就是我要开始的。
  • 部分谜团已解开:PowerShell 端的缓冲行为在 Windows PowerShell 和 PowerShell [Core] v6+(现在流式传输,不再预先收集)之间发生了变化。 marsze 的答案中的函数是一种解决方法,用于消除 PowerShell 端缓冲并使其像在 PowerShell [Core] v6+ 中一样工作,但我不希望这能解决您的问题。

标签: powershell console-application buffer stdin pipeline


【解决方案1】:

我希望将 Powershell 管道行为扩展到外部应用程序。
我有一整套 Write-Host 语句闪过,然后你才能看到输出。

marsze致敬。

  • PowerShell [Core] v6+ 执行完全不缓冲,并在生成时发送(字符串化)输出 /em> 通过命令发送到 外部程序,其输出方式与 PowerShell 命令之间的流式传输方式相同。[1]

  • PowerShell 的旧版(最高 5.1 的版本),Windows PowerShell,缓冲,因为它收集命令的所有输出首先 在将其(字符串化)发送到外部程序之前。

但是,我认为即使是 Windows PowerShell 的行为也不是问题所在:您的 Run-Commands 函数执行得非常快 - 因为它调用的函数只是输出字符串文字 - 并且结果然后将行数组一次全部发送到netapp.exe - 然后进一步处理,包括何时产生输出,直到netapp.exe。在 PowerShell [Core] v6+ 中,由于 PowerShell 端缓冲不在图片中,单个 Run-Commmand<n> 函数的输出将稍早发送到 netapp.exe,但我不希望这会有所作为。

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


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

  • 它发送每个对象的字符串化表示
    • PowerShell [Core] v6+ 中:当对象可用时
    • Windows PowerShell中:在内存中收集所有输出对象后首先

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

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

    • 同样,外部程序通常缓冲它们的 stdout(标准输出)流(但 PowerShell 在将输出传递到,例如到终端(控制台))。

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


可选读物:当管道传输到外部程序时,PowerShell 如何字符串化对象:

  • PowerShell,从 v7.1 开始,在与外部程序通信时只知道 文本;也就是说,发送此类程序的数据被转换为文本,并且此类程序的输出被解释为文本 - 即使底层系统 IPC特征只是 byte 管道。

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

    • 换句话说:通过$OutputEncoding指定的编码必须与外部程序期望的字符编码相匹配。

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

  • 您在 PowerShell 控制台(终端) 中看到的用于显示的表示也是通过管道发送到外部程序的内容,作为文本行,具体来说:

    • 如果一个对象(已经)一个字符串(或[char]实例),PowerShell 将它原样发送到管道,但是总是附加一个适合平台的换行符

      • 也就是说,在 Windows 上附加一个 CRLF 换行符,在类 Unix 平台上附加一个 LF-only 换行符。

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

    • 如果一个对象是,松散地说,一个原始类型 - 概念上是一个单一值的东西,特别是各种数字 类型 - 它是文化敏感方式进行字符串化,如果可用[3] , 总是会添加适合平台的换行符

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

      • 请注意,[bool] 实例区分区域性,并且始终转换为字符串 TrueFalse

    • 所有其他(复杂)类型都受 PowerShell 丰富的显示输出格式的约束,您在终端(控制台)中看到的内容也会发送到外部程序 - 这不仅再次可能包含对文化敏感的表示,但通常存在问题,因为这些表示是为人类观察者设计的,而不是为程序化处理而设计的。

结果

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

  • 为避免意外的文化敏感性和意外的显示格式,最好有意构造您要发送的字符串表示形式。


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

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

[2] 在最近的 macOS 和 Linux 平台上,标准输入缓冲区大小为 64KB。在类 Unix 平台上,实用程序通常会在 interactive 调用中切换到 line-缓冲,即当相关流连接到 终端 时。

[3] 行为被委托给现有类型的 .ToString() 方法,即该方法是否输出文化敏感表示。

【讨论】:

    【解决方案2】:

    编辑:正如@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"
    

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

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

    【讨论】:

    • 我之前尝试过,但无法正常工作。您的代码正在向 EXE 发送正确的信息,但原样不会向 Powershell 返回任何内容。我现在尝试添加 RedirectStandardOutput = $true,并测试 $process.StandardOutput.EndOfStream,如果是这样,它会执行 ReadLine()。遗憾的是,在第一个命令返回所有数据后,最后一个 ReadLine() 永远不会返回。但基本上,我们快到了!
    • 嗯,这就是问题的重点。我能够将数据传输到控制台 EXE,但 Powershell 将其缓冲,然后发送数据。我想要 Powershell 管道工作的松散方式。
    • 非常酷。它只是告诉我我必须弄清楚 Powershell 事件。我猜想同步流式传输 stdinput 不太适用于 Powershell。
    猜你喜欢
    • 1970-01-01
    • 2016-06-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-11-21
    • 1970-01-01
    • 2010-12-22
    相关资源
    最近更新 更多