在CMD和PowerShell中使用管道时,行为和输出不同

14

我正在尝试将文件内容导入一个简单的ASCII对称加密程序中。这是一个简单的程序,它从STDIN读取输入,并将某个值(224)添加或减去到每个字节的输入中。

例如:如果第一个字节是4并且想要加密,则变为228。如果超过了255,程序则执行一些取模运算。

这是我在cmd中得到的输出(test.txt包含“this is a test”):

    type .\test.txt | .\Crypt.exe --encrypt | .\Crypt.exe --decrypt
    this is a test

它也可以往反方向运作,因此它是一种对称加密算法。

    type .\test.txt | .\Crypt.exe --decrypt | .\Crypt.exe --encrypt
    this is a test

但是,在 PowerShell 上的行为是不同的。 当首先进行加密时,我得到:

    type .\test.txt | .\Crypt.exe --encrypt | .\Crypt.exe --decrypt
    this is a test_*

当我第一次解密时,得到了这个:

Screen Shot

可能是一个编码问题。谢谢。

1个回答

25

简而言之:

Windows PowerShellPowerShell (Core) 直到 v7.3.x 版本中,如果你需要进行 原始字节处理 和/或需要 防止 PowerShell 在某些情况下向你的文本数据添加 尾随换行符,最好完全避免使用 PowerShell 管道,如下所示。
v7.4+ 版本中,不再需要 解决方法:在 v7.4 版本中,之前的 实验性 功能 PSNativeCommandPreserveBytePipe 成为一个 稳定 功能:当应用于 外部 (本机) 程序 时,>| 现在作为 原始字节通道,即绕过了通常的字符串解码和重新编码过程,直接传递原始数据。
然而,仍然存在一个注意事项:将 PowerShell 字符串 通过管道发送到 外部程序 仍然会导致追加一个 换行符。有关解决方法,请参阅 此答案
在Windows PowerShell和PowerShell v7.3-中处理原始字节时,使用cmd/c(在Windows上;在类Unix平台/类Unix Windows子系统上,使用shbash-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代码页。
# Make sure that PowerShell uses the OEM code page when sending
# data to `.\Crypt.exe`
$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个字符(表示往返和无意中加密的换行序列)。
# Ensure that .\Crypt.exe output is correctly decoded.
$OutputEncoding = [Console]::OutputEncoding

# Invoke the command and capture its output in variable $result.
# Note the use of the `Get-Content` cmdlet; in PowerShell, `type`
# is simply a built-in *alias* for it.
$result = Get-Content .\test.txt | .\Crypt.exe --decrypt | .\Crypt.exe --encrypt

# Remove the last 4 chars. and print the result.
$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在将数据发送到文件时都使用默认编码方式;在Windows PowerShell中,>会生成带有BOM的UTF-16LE编码文件,而PowerShell (Core)明智地默认为无BOM的UTF-8编码(在文件写入的命令中保持一致)。


在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。

2
哇!流居然不仅是字节流。很棒的信息。非常感谢。 - lit
1
https://www.powershellgallery.com/packages/Use-RawPipeline 是一个很好的替代方案,直到PowerShell添加本地处理此功能的方法。 - Vopel

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