简而言之:
在
Windows PowerShell 和
PowerShell (Core) 直到 v7.3.x 版本中,如果你需要进行
原始字节处理 和/或需要
防止 PowerShell 在某些情况下向你的文本数据添加 尾随换行符,最好完全避免使用
PowerShell 管道,如下所示。
在
v7.4+ 版本中,不再需要
解决方法:在 v7.4 版本中,之前的
实验性 功能
PSNativeCommandPreserveBytePipe
成为一个
稳定 功能:当应用于
外部 (本机) 程序 时,
>
和
|
现在作为
原始字节通道,即绕过了通常的字符串解码和重新编码过程,直接传递原始数据。
然而,仍然存在一个注意事项:将
PowerShell 字符串 通过管道发送到
外部程序 仍然会导致追加一个
换行符。有关解决方法,请参阅
此答案。
在Windows PowerShell和PowerShell v7.3-中处理原始字节时,使用
cmd
和
/c
(在Windows上;在类Unix平台/类Unix Windows子系统上,使用
sh
或
bash
和
-c
)来执行外部命令:
cmd /c 'type .\test.txt | .\Crypt.exe --encrypt | .\Crypt.exe --decrypt'
使用类似的技术来将原始字节输出保存在一个文件中 - 不要使用PowerShell的>运算符:
cmd /c 'someexe > file.bin'
请注意,如果您想要将外部程序的文本输出捕获到PowerShell变量中或在PowerShell管道中进一步处理它,您需要确保[Console]::OutputEncoding与程序的输出字符编码(通常是活动OEM代码页)匹配,默认情况下应该是正确的;有关详细信息,请参阅下一节。
然而,一般来说,最好避免对文本数据进行字节操作。
有两个不同的问题,其中只有一个有一个简单的解决方案:
问题1:正如你所怀疑的那样,确实存在一个
字符编码问题:
PowerShell在管道中以不可见的方式作为中介,即使在与
外部程序交换数据时也是如此:它将数据
转换为.NET字符串(
System.String
),这些字符串是UTF-16代码单元的序列。
- 顺便说一下:即使只使用PowerShell本地命令,这意味着从文件读取输入并再次保存可能会导致不同的字符编码,因为一旦(字符串)数据被读入内存,原始字符编码的信息就不会被保留,保存时使用的是cmdlet的默认字符编码;而在PowerShell(核心)6+中,这个默认编码始终是无BOM的UTF-8,但在Windows PowerShell中,它因cmdlet而异 - 请参阅this answer。
为了与外部程序(例如您的情况下的Crypt.exe)进行数据发送和接收,您需要匹配它们的字符编码;在您的情况下,使用原始字节处理的Windows控制台应用程序的隐含编码是系统的活动OEM代码页。
在发送数据时,PowerShell使用$OutputEncoding首选项变量的编码来编码(始终被视为文本的)数据,默认情况下,Windows PowerShell使用ASCII(!),而PowerShell(Core)使用无BOM的UTF-8。
接收端默认情况下已经覆盖:PowerShell使用[Console]::OutputEncoding(它本身反映了chcp报告的代码页)来解码接收到的数据,在Windows上,默认情况下反映的是活动的OEM代码页,无论是在Windows PowerShell还是PowerShell [Core]中。
为了解决你的主要问题,你需要将
$OutputEncoding
设置为当前的OEM代码页。
$OutputEncoding = [Console]::OutputEncoding
问题2:PowerShell在将数据传输给外部程序时,无论如何都会在数据末尾添加一个换行符。
也就是说,当使用命令“foo | .\Crypt.exe”时,它不会将表示“foo”的编码字节(使用$OutputEncoding编码)发送到.\Crypt.exe的标准输入,而是发送“foo\r\n”(在Windows上);换句话说,会自动且无例外地添加一个平台适用的换行符序列(在Windows上是CRLF),除非字符串本身已经有一个换行符。
这个问题的讨论可以在
GitHub问题#5974和
这个答案中找到。
在您的特定情况下,隐式附加的"`r`n"也受到字节值转换的影响,这意味着第一个Crypt.exe调用将其转换为"-*",导致在数据发送到第二个Crypt.exe调用时附加了另一个"`r`n"。
结果是多了一个回车换行符(中间的"-*"),以及一个加密的换行符导致出现了"φΩ"。
简而言之:如果您的输入数据没有尾随换行符,您将需要从结果中删除最后4个字符(表示往返和无意中加密的换行序列)。
$OutputEncoding = [Console]::OutputEncoding
$result = Get-Content .\test.txt | .\Crypt.exe --decrypt | .\Crypt.exe --encrypt
$result.Substring(0, $result.Length - 4)
鉴于在答案顶部显示的调用
cmd /c
也可以工作,这似乎不值得。
PowerShell 如何处理与外部程序的管道数据:
注意:以下内容适用于 Windows PowerShell 和 PowerShell (Core),至少适用于 PowerShell 的 v7.3.x 版本 - 请参阅关于 实验性功能 PSNativeCommandPreserveBytePipe
的顶部注释,了解可能的未来变化(改进)。
与 cmd
(或类似 POSIX 的 shell,如 bash
)不同:
- PowerShell 不支持管道中的 原始字节数据。[2]
- 当与 外部程序 交互时,它只能处理 文本(而与 PowerShell 自身的命令交互时,它会传递 .NET 对象,这是它的强大之处)。
具体而言,工作原理如下:
当你通过管道(到其stdin流)将数据发送给外部程序时:
它会使用在$OutputEncoding首选项变量中指定的字符编码将其转换为文本(字符串),在Windows PowerShell中默认为ASCII(!),在PowerShell(Core)中为无BOM的UTF-8。
注意:如果你将带有BOM的编码分配给$OutputEncoding,PowerShell(截至v7.0)将在发送给外部程序的第一行输出中发出BOM。因此,例如,在Windows PowerShell中不要使用[System.Text.Encoding]::Utf8(它会发出BOM),而应改用[System.Text.Utf8Encoding]::new($false)(它不会发出BOM)。
如果数据未被PowerShell捕获或重定向,则编码问题可能不会总是显现出来,尤其是如果外部程序以使用Windows Unicode控制台API打印到显示器的方式实现。
不是文本(字符串)的东西将使用PowerShell的默认输出格式化(与打印到控制台时看到的格式相同)进行字符串化,但有一个重要的注意事项:
如果(最后的)输入对象已经是一个没有自身的尾随换行符的字符串,那么一个换行符将被无论如何附加(即使已经存在尾随换行符,如果不同,也会被替换为平台本地的换行符)。
这种行为可能会引起问题,如GitHub问题#5974和这个答案中所讨论的那样。
当你从外部程序捕获/重定向数据(从其stdout流)时,它将根据在[Console]::OutputEncoding中指定的编码,无论在Windows上的活动OEM代码页上,默认为解码为文本行(字符串)。
PowerShell内部使用.NET System.String类型来表示文本,它基于UTF-16代码单元(通常松散地,但不正确地称为"Unicode")。
上述也适用于:
在PowerShell(Core)中,鉴于
$OutputEncoding
已经默认为UTF-8,让
[Console]::OutputEncoding
也是相同的是有道理的 - 也就是说,在Windows上,活动代码页应该有效地是
65001
,正如
GitHub issue #7233中建议的那样。
[2] 通过从文件中输入,你能够处理最接近原始字节的方式是使用
Get-Content -AsByteStream
(PowerShell(Core))/
Get-Content -Encoding Byte
(Windows PowerShell)将文件读取为
.NET System.Byte
数组,但是如果你想进一步处理这样的数组,你只能将其传递给一个专门处理字节数组的
PowerShell命令,或者将其传递给一个期望字节数组的.NET类型的
方法。如果你尝试通过管道将这样的数组发送给一个
外部程序,
每个字节都会以其十进制字符串表示形式单独发送在自己的行上。
[3]
Unicode是描述“全球字母表”的抽象
标准的名称。在具体的使用中,它有各种标准的
编码方式,其中最广泛使用的是UTF-8和UTF-16。