PowerShell 管道添加换行符
Posted
技术标签:
【中文标题】PowerShell 管道添加换行符【英文标题】:PowerShell's pipe adds linefeed 【发布时间】:2015-04-14 19:18:48 【问题描述】:我正在尝试将字符串传送到程序的 STDIN 没有任何尾随换行符(除非该字符串本身实际上以换行符结尾)。我试着用谷歌搜索,但我只发现有人试图在没有尾随换行的情况下打印到控制台,在这种情况下Write-Host
需要一个参数-NoNewLine
。但是,要将其传输到另一个程序,我需要 Write-Output
或类似的没有这样的参数。现在看来Write-Output
甚至都不是问题:
Z:\> (Write-Output "abc").Length
3
但是一旦我将它传送到另一个程序并在那里读取字符串,我就会得到一个额外的换行符。例如,我尝试了这个 Ruby sn-p:
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 并且仍然遇到同样的问题。
【问题讨论】:
是的,这很烦人。当您使用get-content
读取文件然后使用out-file
将其写回时,也会发生这种情况。除了像上面那样通过一个单独的程序进行管道传输(修剪尾随字符的程序除外)之外,我不确定解决这个问题的方法。
您是否在connect.microsoft.com/PowerShell上发布了这种观察到的行为
@user4317867 还没有。我不会认为这是一个错误,只是一个烦人的功能。
这不是错误,只是 PowerShell 用于数据输出 Out-Default / Out-Host 的 cmdlet 的默认行为。许多人使用自己的 cmdlet 或使用自定义函数来“处理”它。它应该是一个功能,以便为更多输出做准备。我可以看到这很烦人。我敢肯定,像 Keith Hill 这样更有经验的人可能对此有话要说。
我希望可以更多地关注这一点。我想知道是否还有其他方法。
【参考方案1】:
简介
这是我的Invoke-RawPipeline
函数(从this Gist) 获取最新版本。
使用它在进程的标准输出和标准输入流之间传递二进制数据。它可以从文件/管道读取输入流并将结果输出流保存到文件中。
它需要PsAsync module 才能在多个进程中启动和管道数据。
如果出现问题,请使用-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://***.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
【讨论】:
尽管如此聪明,但在很大程度上是不必要的(除非作为解决这个特定问题的方法)。任何可以完全在 cmd 中完成的事情都可以使用cmd /c
从 powershell 调用。因此,使用字节重定向或管道 stdin、stdout 甚至 stderr 调用遗留命令很容易。此外,使用Get/Set/Add-Content -AsByteStream
(或-Encoding Byte
pre V6)可以实现powershell 获取文件的原始字节数。仅当您需要 powershell 和遗留程序(输入或输出)之间的原始字节管道,没有临时文件(此问题)时,Invoke-RawPipeline
是理想的。【参考方案2】:
蛮力方法:将二进制数据提供给进程的标准输入。我已经在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
【讨论】:
很抱歉这么久没有回过头来。这似乎有效(主要是;我在Remove-Variable
上遇到错误,但我只在 PS5 中对其进行了测试),但是我如何将它变成一个可以像管道一样方便地使用的脚本?我想最接近的用法是raw-pipe "command1 with arguments" "command2 with other arguments"
。
我建议将其设为一个函数并将该函数包含在您的 Powershell 配置文件中,以便每次打开控制台时都可以访问该函数。查看更多here
@MartinBüttner 实际上我有“原始管道”功能的 WIP 版本:Invoke-RawPipeline。它需要PsAsync module。您可以将字符串通过管道传输到其中,它们将被转换为字节并发送到目标进程的标准输入:'NoNewline' | Invoke-RawPipeline -Command @Path = 'ruby.exe' ; Arguments = 'p ARGF.read'
。请注意,它是 WIP,我很长时间没有研究它,所以我不能保证任何事情。
@beatcracker 很有趣。您如何检测 在 管道之后字符串是否有换行符?
@MartinBüttner 我不必这样做,当管道进入函数 PowerShell 时不会向字符串添加换行符。因为它不是真正的管道,所以它只是一种使用字符串对象作为函数参数的便捷方式。因此,如果您使用管道 'No NewLine'
- 字符串末尾将没有换行符。如果你想要一个,你应该使用管道"NewLine`n"
。【参考方案3】:
做一个简单的方法创建一个cmd进程并执行它
$cmdArgs = @('/c','something.exe','arg1', .. , 'arg2' , $anotherArg , '<', '$somefile.txt' )
&'cmd.exe' $cmdArgs
非常适合将信息输入到我想要的标准输入中,
【讨论】:
谢谢你,这确实按预期工作,尽管我担心这不太方便。 :)【参考方案4】:澄清一些 cmets 中的一个基本误解:管道中的“powershell 命令”是 cmdlet,每个命令都在 single powershell 的进程空间内运行。因此,对象在同一进程中(在多个线程上)按原样传递除非您调用外部命令。然后通过适当的格式化 cmdlet(如果还不是字符串对象)将传递的对象转换为字符串。然后将这些字符串转换为字符流,每个字符串都附加一个 \n。因此,添加 \n 的不是“管道”,而是隐式转换为文本以输入“遗留”命令。
问题中的基本问题是提问者试图在字符(字节)流输入上获得类似对象的行为(例如,没有尾随 \n 的字符串)。 (控制台)进程的标准输入流一次提供一个字符(字节)。输入 例程 将这些单独的字符收集到一个字符串中(通常)在收到 \n 时终止。 \n 是否作为字符串的一部分返回取决于输入例程。当标准输入流被重定向到文件或管道时,输入例程大多对此一无所知。所以没有办法确定没有 \n 的完整字符串和有更多字符的不完整字符串和 \n 之间的区别。
可能的解决方案(针对字符串定界问题,而不是 powershell 添加的 \n 问题)是在标准输入读取上设置某种超时。一个字符串的结尾可以通过在一定时间内没有接收到字符来表示。或者,如果您对管道的访问级别足够低,则可以尝试进行原子读写。通过这种方式,阻塞读取将准确返回写入的内容。不幸的是,当在多任务环境中运行时,这两种方法都存在时间问题。如果延迟太长,效率会下降,但如果延迟太短,则可能会被进程优先级调度造成的延迟愚弄。如果写入进程在读取进程读取当前行之前写入另一行,调度优先级也会干扰原子读取和写入。它需要某种同步系统。
表示当前行没有更多字符的唯一另一种方法是关闭管道(EOF),但这是一种唯一的方法,因此您只能发送一个字符串(尾随 \n 或不) . (这就是 Ruby 在初始示例和 Invoke-RawPipeline
示例中知道输入何时完成的方式。)这实际上可能是您的意图(仅发送一个带有或不带有尾随 \n 的字符串)在这种情况下您可以简单地连接所有输入(保留或重新插入任何嵌入的 \n)并丢弃最后一个 \n。
powershell 为多个字符串添加 \n 问题的一个可能解决方案是通过以其他无效的字符序列终止每个字符串对象来重新定义“字符串”的编码。如果您有每个字符输入(对于类似 C 的行输入没有好处),则可以使用 \0,否则可能会使用 \377 (0xff)。这将允许您的旧命令的输入例程“知道”字符串何时结束。序列 \0\n (或 \377\n)将是字符串的“结尾”,而在此之前的所有内容(包括尾随 \n 与否,可能使用多次读取)都将是字符串。我假设您对输入例程有一定的控制权(例如,您编写了程序),因为任何从标准输入读取的现成程序通常都需要一个 \n(或 EOF)来分隔其输入。
【讨论】:
【参考方案5】:我承认对你在管道之后使用的 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" 命令,看看它是否在每次读取后添加换行符。
【讨论】:
我已经用相当多的语言和方法检查了这一点。我认为它对您有用,因为您将字符串传送到另一个 PowerShell 脚本(或函数?)。在这种情况下,似乎 PS 只是传递了一个字符串对象。但是,一旦它通过管道传输到不同的进程,管道的结果必须转换为从 STDIN 读取的字节流,就会添加换行符。 See this comment on the other answer. 我用 cmd /c echo|set /p=$_ 试过了,我看到它写成两行,但我会坚持管道没有添加基于下一个命令是否为 powershell 的换行符。如果我一起跳过管道并执行两个 cmd /c echo|set /p=$_ 响应在 powershell 的两行 - 但是如果我在批处理/命令文件中运行那个确切的东西这是一条线。当然,如果我打开命令提示符并执行此命令两次,它肯定会在两行上。因此,命令在 powershell 和批处理文件中的执行方式似乎有所不同。 我刚刚注意到问题中有一个错字。我打算使用p
而不是puts
。 puts
确实添加了一个换行符,但 p
只是打印了您传递的任何内容的字符串表示形式,并且由于它打印了 "abc\n"
这表明在 STDIN 上有一个 \n
即使我从未在管道中写过一个 \n
. (在 cmd.exe 中运行完全相同的命令只会按预期打印 "abc"
。)
因为在 cmd 管道传递字节输出但 powershell 必须传递对象(一个字符串用于单行输出或每个元素多行的字符串数组)。将行转换为字符串对象会吸收任何 \n
(或 EOF)分隔符,因此严格来说,即使在 powershell 中,您也不会将 \n
写入管道。正如我在回答中所解释的那样,当字符串转换为下一个遗留命令的标准输入的字符时,\n
会重新生成(正如您在上面推测的那样)。以上是关于PowerShell 管道添加换行符的主要内容,如果未能解决你的问题,请参考以下文章
powershell 来自https://stackoverflow.com/questions/31051103/how-to-export-a-class-in-powershell-v5-mod
在 Windows 批处理文件中将输入管道输入到 Plink 会添加额外的换行符
在 CMD 和 PowerShell 中管道时的不同行为和输出