PowerShell的管道符会添加换行符

22

我正在尝试将一个字符串导入程序的标准输入(STDIN),而不是在末尾添加任何换行符(除非该字符串本身实际上以换行符结尾)。我尝试过搜索,但我只找到了人们试图在 控制台 上打印没有尾随换行符的内容,在这种情况下 Write-Host 接受参数 -NoNewLine。然而,要将其传递到另一个程序中,我需要使用 Write-Output 或类似的命令,它没有这样的参数。现在看来问题并不是由于 Write-Output

Z:\> (Write-Output "abc").Length
3

但是,一旦我将它传递给另一个程序并在那里读取字符串,就会多出一个换行符。例如,我尝试了这个 Ruby 代码片段:

但是一旦我通过管道将其传递到另一个程序并在那里读取字符串,就会得到额外的换行符。例如,我尝试了这段 Ruby 代码:

Z:\> Write-Output "abc" | ruby -e "p ARGF.read"
"abc\n"

我检查了实际接收到的字符串是abc\n。在其他几种语言(至少包括C#,Java和Python)中也会发生相同的情况,因此我认为这是PowerShell的问题,而不是读取内容的语言的问题。

作为进一步的测试,我用另一个Ruby脚本替换了Write-Output本身:

Z:\> ruby -e "$> << 'abc'"
abcZ:\>

也就是说,脚本的 STDOUT 上绝对没有 \n

但是,当我将其管道传递到另一个脚本时:

Z:\> ruby -e "$> << 'abc'" | ruby -e "p ARGF.read"
"abc\n"

我相当确信是管道符号添加了换行符。我该如何避免这种情况?实际上,我想要能够控制输入是否以换行符结尾(通过将其包含在输入中或省略它)。

(供参考,我还测试了已经包含尾随换行符的字符串,在这种情况下,管道符号不会再添加另一个换行符,因此我猜它只是确保有尾随的换行符。)

我最初在PowerShell v3中遇到了这个问题,但现在我正在使用v5,仍然存在同样的问题。


2
是的,这很烦人。当你使用 get-content 从文件中读取内容,然后使用 out-file 将其写回时,也会发生这种情况。除了像上面那样通过一个单独的程序进行管道传输(除了修剪尾随字符的程序),我不确定是否有其他解决方法。 - NextInLine
1
您是否在 https://connect.microsoft.com/PowerShell 上发布了这个观察到的行为? - user4317867
1
这不是一个错误,而是PowerShell用于数据输出的cmdlets Out-Default / Out-Host的默认行为。许多人通过自己的cmdlets或使用自定义函数来“处理”它。这应该是一种功能,以准备更多的输出。我可以看到这很烦人。我相信像Keith Hill这样更有经验的人可能会对此有所发言。 - Matt
1
我希望能够更加关注这个问题。我想知道是否还有其他的方法。 - Matt
1
@Matt 你可以再发布一个赏金,但除非将此事特别带到像Keith这样的人的注意下,否则我不确定会有多少帮助。 - Martin Ender
显示剩余2条评论
5个回答

11

介绍

这是我的Invoke-RawPipeline函数(从此Gist获取最新版本)。

使用它在进程的标准输出和标准输入流之间传输二进制数据。它可以从文件/管道读取输入流,并将生成的输出流保存到文件中。

它需要PsAsync模块才能启动并在多个进程中传输数据。

如果出现问题,请使用-Verbose开关查看调试输出。

示例

重定向到文件

  • 批处理:
    findstr.exe /C:"Warning" /I C:\Windows\WindowsUpdate.log > C:\WU_Warnings.txt
  • PowerShell:
    Invoke-RawPipeline -Command @{Path = 'findstr.exe' ; Arguments = '/C:"Warning" /I C:\Windows\WindowsUpdate.log'} -OutFile 'C:\WU_Warnings.txt'

从文件重定向

  • 批处理:
    svnadmin load < C:\RepoDumps\MyRepo.dump
  • PowerShell:
    Invoke-RawPipeline -InFile 'C:\RepoDumps\MyRepo.dump' -Command @{Path = 'svnadmin.exe' ; Arguments = 'load'}

传递字符串

  • 批处理:
    echo TestString | find /I "test" > C:\SearchResult.log
  • PowerShell:
    'TestString' | Invoke-RawPipeline -Command @{Path = 'find.exe' ; Arguments = '/I "test"'} -OutFile 'C:\SearchResult.log'

在多个进程之间传输数据

  • 批处理:
    ipconfig | findstr /C:"IPv4 Address" /I
  • PowerShell:
    Invoke-RawPipeline -Command @{Path = 'ipconfig'}, @{Path = 'findstr' ; Arguments = '/C:"IPv4 Address" /I'} -RawData

代码:

<#
.Synopsis
    Pipe binary data between processes' Standard Output and Standard Input streams.
    Can read input stream from file and save resulting output stream to file.

.Description
    Pipe binary data between processes' Standard Output and Standard Input streams.
    Can read input stream from file/pipeline and save resulting output stream to file.
    Requires PsAsync module: http://psasync.codeplex.com

.Notes
    Author: beatcracker (https://beatcracker.wordpress.com, https://github.com/beatcracker)
    License: Microsoft Public License (http://opensource.org/licenses/MS-PL)

.Component
    Requires PsAsync module: http://psasync.codeplex.com

.Parameter Command
    An array of hashtables, each containing Command Name, Working Directory and Arguments

.Parameter InFile
    This parameter is optional.

    A string representing path to file, to read input stream from.

.Parameter OutFile
    This parameter is optional.

    A string representing path to file, to save resulting output stream to.

.Parameter Append
    This parameter is optional. Default is false.

    A switch controlling wheither ovewrite or append output file if it already exists. Default is to overwrite.

.Parameter IoTimeout
    This parameter is optional. Default is 0.

    A number of seconds to wait if Input/Output streams are blocked. Default is to wait indefinetely.

.Parameter ProcessTimeout
    This parameter is optional. Default is 0.

    A number of seconds to wait for process to exit after finishing all pipeline operations. Default is to wait indefinetely.
    Details: https://msdn.microsoft.com/en-us/library/ty0d8k56.aspx

.Parameter BufferSize
    This parameter is optional. Default is 4096.

    Size of buffer in bytes for read\write operations. Supports standard Powershell multipliers: KB, MB, GB, TB, and PB.
    Total number of buffers is: Command.Count * 2 + InFile + OutFile.

.Parameter ForceGC
    This parameter is optional.

    A switch, that if specified will force .Net garbage collection.
    Use to immediately release memory on function exit, if large buffer size was used.

.Parameter RawData
    This parameter is optional.

    By default function returns object with StdOut/StdErr streams and process' exit codes.
    If this switch is specified, function will return raw Standard Output stream.

.Example
    Invoke-RawPipeline -Command @{Path = 'findstr.exe' ; Arguments = '/C:"Warning" /I C:\Windows\WindowsUpdate.log'} -OutFile 'C:\WU_Warnings.txt'

    Batch analog: findstr.exe /C:"Warning" /I C:\Windows\WindowsUpdate.log' > C:\WU_Warnings.txt

.Example
    Invoke-RawPipeline -Command @{Path = 'findstr.exe' ; WorkingDirectory = 'C:\Windows' ; Arguments = '/C:"Warning" /I .\WindowsUpdate.log'} -RawData

    Batch analog: cd /D C:\Windows && findstr.exe /C:"Warning" /I .\WindowsUpdate.log

.Example
    'TestString' | Invoke-RawPipeline -Command @{Path = 'find.exe' ; Arguments = '/I "test"'} -OutFile 'C:\SearchResult.log'

    Batch analog: echo TestString | find /I "test" > C:\SearchResult.log

.Example
    Invoke-RawPipeline -Command @{Path = 'ipconfig'}, @{Path = 'findstr' ; Arguments = '/C:"IPv4 Address" /I'} -RawData

    Batch analog: ipconfig | findstr /C:"IPv4 Address" /I

.Example
    Invoke-RawPipeline -InFile 'C:\RepoDumps\Repo.svn' -Command @{Path = 'svnadmin.exe' ; Arguments = 'load'}

    Batch analog: svnadmin load < C:\RepoDumps\MyRepo.dump
#>

function Invoke-RawPipeline
{
    [CmdletBinding()]
    Param
    (
        [Parameter(ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if($_.psobject.Methods.Match.('ToString'))
            {
                $true
            }
            else
            {
                throw 'Can''t convert pipeline object to string!'
            }
        })]
        $InVariable,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateScript({
            $_ | ForEach-Object {
                $Path = $_.Path
                $WorkingDirectory = $_.WorkingDirectory

                if(!(Get-Command -Name $Path -CommandType Application -ErrorAction SilentlyContinue))
                {
                    throw "Command not found: $Path"
                }

                if($WorkingDirectory)
                {
                    if(!(Test-Path -LiteralPath $WorkingDirectory -PathType Container -ErrorAction SilentlyContinue))
                    {
                        throw "Working directory not found: $WorkingDirectory"
                    }
                }
            }
            $true
        })]
        [ValidateNotNullOrEmpty()]
        [array]$Command,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateScript({
            if(!(Test-Path -LiteralPath $_))
            {
                throw "File not found: $_"
            }
            $true
        })]
        [ValidateNotNullOrEmpty()]
        [string]$InFile,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateScript({
            if(!(Test-Path -LiteralPath (Split-Path $_)))
            {
                throw "Folder not found: $_"
            }
            $true
        })]
        [ValidateNotNullOrEmpty()]
        [string]$OutFile,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [switch]$Append,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateRange(0, 2147483)]
        [int]$IoTimeout = 0,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateRange(0, 2147483)]
        [int]$ProcessTimeout = 0,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [long]$BufferSize = 4096,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [switch]$RawData,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [switch]$ForceGC
    )

    Begin
    {

        $Modules = @{PsAsync = 'http://psasync.codeplex.com'}

        'Loading modules:', ($Modules | Format-Table -HideTableHeaders -AutoSize | Out-String) | Write-Verbose

        foreach($module in $Modules.GetEnumerator())
        {
            if(!(Get-Module -Name $module.Key))
            {
                Try
                { 
                    Import-Module -Name $module.Key -ErrorAction Stop
                }
                Catch
                {
                    throw "$($module.Key) module not available. Get it here: $($module.Value)"
                }
            }
        }

        function New-ConsoleProcess
        {
            Param
            (
                [string]$Path,
                [string]$Arguments,
                [string]$WorkingDirectory,
                [switch]$CreateNoWindow = $true,
                [switch]$RedirectStdIn = $true,
                [switch]$RedirectStdOut = $true,
                [switch]$RedirectStdErr = $true
            )

            if(!$WorkingDirectory)
            {
                if(!$script:MyInvocation.MyCommand.Path)
                {
                    $WorkingDirectory = [System.AppDomain]::CurrentDomain.BaseDirectory
                }
                else
                {
                    $WorkingDirectory = Split-Path $script:MyInvocation.MyCommand.Path
                }
            }

            Try
            {
                $ps = New-Object -TypeName System.Diagnostics.Process -ErrorAction Stop
                $ps.StartInfo.Filename = $Path
                $ps.StartInfo.Arguments = $Arguments
                $ps.StartInfo.UseShellExecute = $false
                $ps.StartInfo.RedirectStandardInput = $RedirectStdIn
                $ps.StartInfo.RedirectStandardOutput = $RedirectStdOut
                $ps.StartInfo.RedirectStandardError = $RedirectStdErr
                $ps.StartInfo.CreateNoWindow = $CreateNoWindow
                $ps.StartInfo.WorkingDirectory = $WorkingDirectory
            }
            Catch
            {
                throw $_
            }

            return $ps
        }

        function Invoke-GarbageCollection
        {
            [gc]::Collect()
            [gc]::WaitForPendingFinalizers()
        }

        $CleanUp = {
            $IoWorkers + $StdErrWorkers |
            ForEach-Object {
                $_.Src, $_.Dst |
                ForEach-Object {
                    if(!($_ -is [System.Diagnostics.Process]))
                    {
                        Try
                        {
                            $_.Close()
                        }
                        Catch
                        {
                            Write-Error "Failed to close $_"
                        }
                        $_.Dispose()
                    }

                }
            }
        }

        $PumpData = {
            Param
            (
                [hashtable]$Cfg
            )
            # Fail hard, we don't want stuck threads
            $Private:ErrorActionPreference = 'Stop'

            $Src = $Cfg.Src
            $SrcEndpoint = $Cfg.SrcEndpoint
            $Dst = $Cfg.Dst
            $DstEndpoint = $Cfg.DstEndpoint
            $BufferSize = $Cfg.BufferSize
            $SyncHash = $Cfg.SyncHash
            $RunspaceId = $Cfg.Id
        
            # Setup Input and Output streams
            if($Src -is [System.Diagnostics.Process])
            {
                switch ($SrcEndpoint)
                {
                    'StdOut' {$InStream = $Src.StandardOutput.BaseStream}
                    'StdIn' {$InStream = $Src.StandardInput.BaseStream}
                    'StdErr' {$InStream = $Src.StandardError.BaseStream}
                    default {throw "Not valid source endpoint: $_"}
                }
            }
            else
            {
                $InStream = $Src
            }

            if($Dst -is [System.Diagnostics.Process])
            {
                switch ($DstEndpoint)
                {
                    'StdOut' {$OutStream = $Dst.StandardOutput.BaseStream}
                    'StdIn' {$OutStream = $Dst.StandardInput.BaseStream}
                    'StdErr' {$OutStream = $Dst.StandardError.BaseStream}
                    default {throw "Not valid destination endpoint: $_"}
                }
            }            
            else
            {
                $OutStream = $Dst
            }

            $InStream | Out-String | ForEach-Object {$SyncHash.$RunspaceId.Status += "InStream: $_"}
            $OutStream | Out-String | ForEach-Object {$SyncHash.$RunspaceId.Status += "OutStream: $_"}

            # Main data copy loop
            $Buffer = New-Object -TypeName byte[] $BufferSize
            $BytesThru = 0

            Try
            {
                Do
                {
                    $SyncHash.$RunspaceId.IoStartTime = [DateTime]::UtcNow.Ticks
                    $ReadCount = $InStream.Read($Buffer, 0, $Buffer.Length)
                    $OutStream.Write($Buffer, 0, $ReadCount)
                    $OutStream.Flush()
                    $BytesThru += $ReadCount
                }
                While($readCount -gt 0)
            }
            Catch
            {
                $SyncHash.$RunspaceId.Status += $_
        
            }
            Finally
            {
                $OutStream.Close()
                $InStream.Close()
            }
        }
    }

    Process
    {
        $PsCommand = @()
        if($Command.Length)
        {
            Write-Verbose 'Creating new process objects'
            $i = 0
            foreach($cmd in $Command.GetEnumerator())
            {
                $PsCommand += New-ConsoleProcess @cmd
                $i++
            }
        }

        Write-Verbose 'Building I\O pipeline'
        $PipeLine = @()
        if($InVariable)
        {
                [Byte[]]$InVarBytes = [Text.Encoding]::UTF8.GetBytes($InVariable.ToString())
                $PipeLine += New-Object -TypeName System.IO.MemoryStream -ArgumentList $BufferSize -ErrorAction Stop
                $PipeLine[-1].Write($InVarBytes, 0, $InVarBytes.Length)
                [Void]$PipeLine[-1].Seek(0, 'Begin')
        }
        elseif($InFile)
        {
            $PipeLine += New-Object -TypeName System.IO.FileStream -ArgumentList ($InFile, [IO.FileMode]::Open) -ErrorAction Stop
            if($PsCommand.Length)
            {
                $PsCommand[0].StartInfo.RedirectStandardInput = $true
            }
        }
        else
        {
            if($PsCommand.Length)
            {
                $PsCommand[0].StartInfo.RedirectStandardInput = $false
            }
        }

        $PipeLine += $PsCommand

        if($OutFile)
        {
            if($PsCommand.Length)
            {
                $PsCommand[-1].StartInfo.RedirectStandardOutput = $true
            }

            if($Append)
            {
                $FileMode = [System.IO.FileMode]::Append
            }
            else
            {
                $FileMode = [System.IO.FileMode]::Create
            }

            $PipeLine += New-Object -TypeName System.IO.FileStream -ArgumentList ($OutFile, $FileMode, [System.IO.FileAccess]::Write) -ErrorAction Stop
        }
        else
        {
            if($PsCommand.Length)
            {
                $PipeLine += New-Object -TypeName System.IO.MemoryStream -ArgumentList $BufferSize -ErrorAction Stop
            }
        }
    
        Write-Verbose 'Creating I\O threads'
        $IoWorkers = @()
        for($i=0 ; $i -lt ($PipeLine.Length-1) ; $i++)
        {
            $SrcEndpoint = $DstEndpoint = $null
            if($PipeLine[$i] -is [System.Diagnostics.Process])
            {
                $SrcEndpoint = 'StdOut'
            }
            if($PipeLine[$i+1] -is [System.Diagnostics.Process])
            {
                $DstEndpoint = 'StdIn'
            }

            $IoWorkers += @{
                Src = $PipeLine[$i]
                SrcEndpoint = $SrcEndpoint
                Dst = $PipeLine[$i+1]
                DstEndpoint = $DstEndpoint
            }
        }
        Write-Verbose "Created $($IoWorkers.Length) I\O worker objects"

        Write-Verbose 'Creating StdErr readers'
        $StdErrWorkers = @()
        for($i=0 ; $i -lt $PsCommand.Length ; $i++)
        {
            $StdErrWorkers += @{
                Src = $PsCommand[$i]
                SrcEndpoint = 'StdErr'
                Dst = New-Object -TypeName System.IO.MemoryStream -ArgumentList $BufferSize -ErrorAction Stop
            }
        }
        Write-Verbose "Created $($StdErrWorkers.Length) StdErr reader objects"

        Write-Verbose 'Starting processes'
        $PsCommand |
            ForEach-Object {
                $ps = $_
                Try
                {
                    [void]$ps.Start()
                }
                Catch
                {
                    Write-Error "Failed to start process: $($ps.StartInfo.FileName)"
                    Write-Verbose "Can't launch process, killing and disposing all"

                    if($PsCommand)
                    {
                        $PsCommand |
                            ForEach-Object {
                                Try{$_.Kill()}Catch{} # Can't do much if kill fails...
                                $_.Dispose()
                            }
                    }

                        Write-Verbose 'Closing and disposing I\O streams'
                    . $CleanUp
                }
                Write-Verbose "Started new process: Name=$($ps.Name), Id=$($ps.Id)"
            }

        $WorkersCount = $IoWorkers.Length + $StdErrWorkers.Length
        Write-Verbose 'Creating sync hashtable'
        $sync = @{}
        for($i=0 ; $i -lt $WorkersCount ; $i++)
        {
            $sync += @{$i = @{IoStartTime = $nul ; Status = $null}}
        }
        $SyncHash = [hashtable]::Synchronized($sync)

        Write-Verbose 'Creating runspace pool'
        $RunspacePool = Get-RunspacePool $WorkersCount

        Write-Verbose 'Loading workers on the runspace pool'
        $AsyncPipelines = @()
        $i = 0
        $IoWorkers + $StdErrWorkers |
        ForEach-Object {
            $Param = @{
                BufferSize = $BufferSize
                Id = $i
                SyncHash = $SyncHash
            } + $_

            $AsyncPipelines += Invoke-Async -RunspacePool $RunspacePool -ScriptBlock $PumpData -Parameters $Param
            $i++

            Write-Verbose 'Started working thread'
            $Param | Format-Table -HideTableHeaders -AutoSize | Out-String | Write-Debug
        }

        Write-Verbose 'Waiting for I\O to complete...'
        if($IoTimeout){Write-Verbose "Timeout is $IoTimeout seconds"}

        Do
        {
            # Check for pipelines with errors
            [array]$FailedPipelines = Receive-AsyncStatus -Pipelines $AsyncPipelines | Where-Object {$_.Completed -and $_.Error}
            if($FailedPipelines)
            {
                "$($FailedPipelines.Length) pipeline(s) failed!",
                ($FailedPipelines | Select-Object -ExpandProperty Error | Format-Table -AutoSize | Out-String) | Write-Debug
            }

            if($IoTimeout)
            {
                # Compare I\O start time of thread with current time
                [array]$LockedReaders = $SyncHash.Keys | Where-Object {[TimeSpan]::FromTicks([DateTime]::UtcNow.Ticks - $SyncHash.$_.IoStartTime).TotalSeconds -gt $IoTimeout}
                if($LockedReaders)
                {
                    # Yikes, someone is stuck
                    "$($LockedReaders.Length) I\O operations reached timeout!" | Write-Verbose
                    $SyncHash.GetEnumerator() | ForEach-Object {"$($_.Key) = $($_.Value.Status)"} | Sort-Object | Out-String | Write-Debug
                    $PsCommand | ForEach-Object {
                        Write-Verbose "Killing process: Name=$($_.Name), Id=$($_.Id)"
                        Try
                        {
                            $_.Kill()
                        }
                        Catch
                        {
                            Write-Error 'Failed to kill process!'
                        }
                    }
                    break
                }
            }
            Start-Sleep 1
        }
        While(Receive-AsyncStatus -Pipelines $AsyncPipelines | Where-Object {!$_.Completed}) # Loop until all pipelines are finished

        Write-Verbose 'Waiting for all pipelines to finish...'
        $IoStats = Receive-AsyncResults -Pipelines $AsyncPipelines
        Write-Verbose 'All pipelines are finished'

        Write-Verbose 'Collecting StdErr for all processes'
        $PipeStdErr = $StdErrWorkers |
            ForEach-Object {
                $Encoding = $_.Src.StartInfo.StandardOutputEncoding
                if(!$Encoding)
                {
                    $Encoding = [System.Text.Encoding]::Default
                }
                @{
                    FileName = $_.Src.StartInfo.FileName
                    StdErr = $Encoding.GetString($_.Dst.ToArray())
                    ExitCode = $_.Src.ExitCode
                }
            } | 
                Select-Object   @{Name = 'FileName' ; Expression = {$_.FileName}},
                                @{Name = 'StdErr' ; Expression = {$_.StdErr}},
                                @{Name = 'ExitCode' ; Expression = {$_.ExitCode}}

        if($IoWorkers[-1].Dst -is [System.IO.MemoryStream])
        {
            Write-Verbose 'Collecting final pipeline output'
                if($IoWorkers[-1].Src -is [System.Diagnostics.Process])
                {
                    $Encoding = $IoWorkers[-1].Src.StartInfo.StandardOutputEncoding
                }
                if(!$Encoding)
                {
                    $Encoding = [System.Text.Encoding]::Default
                }
                $PipeResult = $Encoding.GetString($IoWorkers[-1].Dst.ToArray())
        }


        Write-Verbose 'Closing and disposing I\O streams'
        . $CleanUp

        $PsCommand |
            ForEach-Object {
                $_.Refresh()
                if(!$_.HasExited)
                {
                    Write-Verbose "Process is still active: Name=$($_.Name), Id=$($_.Id)"
                    if(!$ProcessTimeout)
                    {
                        $ProcessTimeout = -1
                    }
                    else
                    {
                        $WaitForExitProcessTimeout = $ProcessTimeout * 1000
                    }
                    Write-Verbose "Waiting for process to exit (Process Timeout = $ProcessTimeout)"
                    if(!$_.WaitForExit($WaitForExitProcessTimeout))
                    {
                        Try
                        {
                            Write-Verbose 'Trying to kill it'
                            $_.Kill()
                        }
                        Catch
                        {
                            Write-Error "Failed to kill process $_"
                        }
                    }
                }
                Write-Verbose "Disposing process object: Name=$($_.StartInfo.FileName)"
                $_.Dispose()
            }

        Write-Verbose 'Disposing runspace pool'
        # http://stackoverflow.com/questions/21454252/how-to-cleanup-resources-in-a-dll-when-powershell-ise-exits-like-new-pssession
        $RunspacePool.Dispose()

        if($ForceGC)
        {
            Write-Verbose 'Forcing garbage collection'
            Invoke-GarbageCollection
        }
    
        if(!$RawData)
        {
            New-Object -TypeName psobject -Property @{Result = $PipeResult ; Status = $PipeStdErr}
        }
        else
        {
            $PipeResult
        }
    }
}

2
虽然这很聪明,但它在很大程度上是不必要的(除非作为解决此特定问题的解决方案)。任何可以完全在 cmd 中完成的事情都可以使用 cmd /cpowershell 调用。因此,使用字节重定向或管道 stdin、stdout 甚至 stderr 调用旧命令很容易。此外,使用 Get/Set/Add-Content -AsByteStream(或 -Encoding Byte 在 V6 之前)可以让 powershell 获取原始字节并将其写入文件。只有当您需要在 powershell 和旧程序之间进行原始字节管道传输(输入或输出),而无需使用临时文件(这个问题)时,Invoke-RawPipeline 才是理想的选择。 - Uber Kluger

4

简单的方法是创建一个 cmd 进程并执行它。

$cmdArgs = @('/c','something.exe','arg1', .. , 'arg2' , $anotherArg , '<', '$somefile.txt' )
&'cmd.exe' $cmdArgs

很适合将信息导入标准输入流,这正是我想要的。


1
谢谢,这确实按预期工作,尽管我担心它并不是非常方便。 :) - Martin Ender

4

暴力破解方法:将二进制数据输入到进程的标准输入中。我已经在UnixUtils中的cat.exe上测试了这段代码,似乎可以实现你想要的功能:

# Text to send
$InputVar = "No Newline, No NewLine,`nNewLine, No NewLine,`nNewLine, No NewLine"

# Buffer & initial size of MemoryStream
$BufferSize = 4096

# Convert text to bytes and write to MemoryStream
[byte[]]$InputBytes = [Text.Encoding]::UTF8.GetBytes($InputVar)
$MemStream = New-Object -TypeName System.IO.MemoryStream -ArgumentList $BufferSize
$MemStream.Write($InputBytes, 0, $InputBytes.Length)
[Void]$MemStream.Seek(0, 'Begin')

# Setup stdin\stdout redirection for our process
$StartInfo = New-Object -TypeName System.Diagnostics.ProcessStartInfo -Property @{
                FileName = 'MyLittle.exe'
                UseShellExecute = $false
                RedirectStandardInput = $true
            }

# Create new process
$Process = New-Object -TypeName System.Diagnostics.Process

# Assign previously created StartInfo properties
$Process.StartInfo = $StartInfo
# Start process
[void]$Process.Start()

# Pipe data
$Buffer = New-Object -TypeName byte[] -ArgumentList $BufferSize
$StdinStream = $Process.StandardInput.BaseStream

try
{
    do
    {
        $ReadCount = $MemStream.Read($Buffer, 0, $Buffer.Length)
        $StdinStream.Write($Buffer, 0, $ReadCount)
        $StdinStream.Flush()
    }
    while($ReadCount -gt 0)
}
catch
{
    throw 'Houston, we have a problem!'           
}
finally
{
    # Close streams
    $StdinStream.Close()
    $MemStream.Close()
}

# Cleanup
'Process', 'StdinStream', 'MemStream' |
    ForEach-Object {
        (Get-Variable $_ -ValueOnly).Dispose()
        Remove-Variable $_ -Force
    }

1
很抱歉这么久才回复你。这似乎有效(大部分都有效,但是Remove-Variable出现了错误,不过我仅在PS5中进行了测试),但如何将其转换为可以像pipe一样方便使用的脚本呢?我猜最接近的用法应该是像这样:raw-pipe "带参数的command1" "带其他参数的command2" - Martin Ender
1
@MartinBüttner,我有一个“原始传输”功能的WIP版本:Invoke-RawPipeline。它需要PsAsync模块。您可以将字符串输入其中,并将其转换为字节并发送到目标进程的stdin中:'NoNewline' | Invoke-RawPipeline -Command @{Path = 'ruby.exe' ; Arguments = 'p ARGF.read'}。请注意,这是一个WIP版本,我已经很长时间没有研究它了,所以我不能保证任何东西。 - beatcracker
1
@beatcracker 很有趣。在管道后,您如何检测字符串是否有换行符? - Martin Ender
1
@MartinBüttner 我不需要这样做,因为当将字符串传递给函数时,PowerShell 不会添加换行符。因为这不是真正的管道,它只是一种方便的方式,将字符串对象用作函数参数。所以如果你传递 'No NewLine' - 字符串末尾就不会有换行符。如果你想要一个换行符,你应该传递 "NewLine\n"`。 - beatcracker
1
@beatcracker 如果您更新了您的答案(或将其作为单独的答案发布),并确保它可以相对可靠地工作,我很乐意为此提供悬赏(当然,前提是没有更好的答案出现)。 - Martin Ender
显示剩余2条评论

3
为了澄清一些评论中的一个基本误解:管道中的“powershell命令”是cmdlet,每个命令都在单个powershell的进程空间内运行。因此,对象作为原样在同一进程(多个线程上)内传递,除非调用外部命令。然后,通过适当的格式化cmdlet将传递的对象转换为字符串(如果不是字符串对象)。这些字符串然后被转换为字符流,每个字符串都有一个附加的\n。因此,不是“管道”添加了\n,而是将其隐式转换为文本以输入到“遗留”命令。
问题的基本问题在于提问者试图在字符(字节)流输入上获得类似对象的行为(例如没有尾随\n的字符串)。 (控制台)进程的标准输入流逐个提供字符(字节)。输入例程将这些单个字符收集到单个字符串中(通常),并在接收到\n时终止。是否将\n作为字符串的一部分返回取决于输入例程。当标准输入流被重定向到文件或管道时,输入例程大多没有这方面的知识。因此,无法确定完整字符串与没有\n的不完整字符串之间的区别以及仍需更多字符和\n。
可能的解决方案(针对字符串分隔问题,而不是powershell添加的\n问题)是在标准输入读取上设置某种超时。通过一段时间内未收到字符来表示字符串的结尾。或者,如果您具有足够低级别的管道访问权限,则可以尝试进行原子读取和写入。以这种方式,阻止的读取将返回确切写入的内容。不幸的是,这两种方法在多任务环境中运行时存在时间问题。如果延迟很长,则效率会降低,但如果延迟太短,则可能会被进程优先级调度引起的延迟欺骗。如果写入进程在读取进程读取当前行之前写入另一行,则调度优先级也会干扰原子读取和写入。它需要某种同步系统。
唯一用于表示当前行上没有更多字符的方法是关闭管道(EOF),但这是一种一次性方法,因此您只能发送一个字符串(尾随\n或不带)。 (这就是Ruby在初始示例和Invoke-RawPipeline示例中知道输入何时完成的方式。)可能这实际上是您的意图(仅发送一个带或不带尾随\n的字符串),在这种情况下,您可以简单地连接所有输入(保留或重新插入任何嵌入的\n)并丢弃最后一个\n。
对于多个字符串中powershell添加\n的问题,一个可能的解决方案是通过在每个字符串对象结尾处使用一个不合法的字符序列来重新定义“a string”的编码方式。如果您有每个字符的输入(不适用于类C行输入),则可以使用\0,否则可能使用\377(0xff)。这将允许您的遗留命令的输入程序“知道”何时结束字符串。序列\0\n(或\377\n)将是字符串的“结束”,而之前的所有内容(包括尾随的\n或不包括,可能使用多个读取)都将是该字符串。我假设您对输入程序有一定的控制权(例如,您编写了该程序),因为任何标准输入读取的现成程序通常会期望使用\n(或EOF)来分隔其输入。

0

我承认我对ruby -e "puts ARGF.read"命令没有任何经验,但我认为我可以证明管道不会添加换行符。

# check length of string without newline after pipe 
Write-Output "abc" | %{Write-Host "$_ has a length of: $($_.Length)"  }

#check of string with newline length after pipe
Write-Output "def`n" | %{Write-Host "$($_.Length) is the length of $_" -NoNewline }

#write a string without newline (suppressing newline on Write-Host)
Write-Output 'abc' | %{ Write-Host $_ -NoNewline; }

#write a string with newline (suppressing newline on Write-Host)
Write-Output "def`n" | %{ Write-Host $_ -NoNewline; }

#write a final string without newline (suppressing newline on Write-Host)
Write-Output 'ghi' | %{ Write-Host $_ -NoNewline; }

这会给我一个输出:

abc has a length of: 3
4 is the length of def
abcdef
ghi

我认为你可能需要开始查看 ruby -e "put AGRF.read" 命令,并查看它是否在每次读取后添加了换行符。


1
我已经用很多种语言和方法进行了检查。我认为这对你来说是有效的,因为你将字符串传递给另一个PowerShell脚本(或函数?)。在这种情况下,似乎PS只是传递一个字符串对象。但是一旦它被传递到另一个进程中,在该管道的结果必须转换为字节流以从STDIN读取时,换行符就会被添加进去。请参阅其他答案上的评论 - Martin Ender
1
我尝试使用*cmd /c echo|set /p=$,并且我看到它被写在两行上,但我会坚持认为管道符号不会根据下一个命令是否是powershell而添加换行符。如果我完全跳过管道符号并执行两个cmd /c echo|set /p=$*,则在powershell中响应将显示为两行 - 但是,如果我在批处理/命令文件中运行完全相同的内容,则只有一行。当然,如果我打开命令提示符并执行此命令两次,则肯定会显示为两行。因此,在powershell和批处理文件中执行命令的方式存在差异。 - Larry Dukek
1
我刚刚注意到问题中有一个错别字。我本意是使用 p 而不是 putsputs 会添加一个换行符,但是 p 只是打印传递给它的字符串表示形式,由于它打印了 "abc\n",这表明在 STDIN 上有一个 \n,即使我从未向管道写入过它。(在 cmd.exe 中运行完全相同的命令只会按预期打印出 "abc"。) - Martin Ender
1
因为在 cmd 中,管道传递字节输出,但是 powershell 必须传递对象(单行输出为一个字符串,多行输出为一个字符串数组的每个元素)。将行转换为字符串对象会吸收任何 \n(或 EOF)分隔符,因此严格来说,即使在 powershell 中,您也没有写入\n 到管道。如我解释的那样,当字符串转换为下一个遗留命令的 stdin 的字符时(如上所述),\n 会被重新生成。 - Uber Kluger

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