通过 CMD 和 PowerShell 管道时的不同行为和输出

Ade*_* M. 5 powershell encoding cmd pipe

我正在尝试将文件的内容通过管道传输到我制作的一个简单的 ASCII 对称加密程序。这是一个简单的程序,它从 STDIN 读取输入并对输入的每个字节添加或减去某个值 (224)。例如:如果第一个字节是 4,我们要加密,那么它变成 228。如果超过 255,程序只是执行一些模运算。

这是我用 cmd 得到的输出(test.txt 包含“这是一个测试”):

    type .\test.txt | .\Crypt.exe --encrypt | .\Crypt.exe --decrypt
    this is a test
Run Code Online (Sandbox Code Playgroud)

它也以另一种方式工作,因此它是一种对称加密算法

    type .\test.txt | .\Crypt.exe --encrypt | .\Crypt.exe --decrypt
    this is a test
Run Code Online (Sandbox Code Playgroud)

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

    type .\test.txt | .\Crypt.exe --decrypt | .\Crypt.exe --encrypt
    this is a test
Run Code Online (Sandbox Code Playgroud)

这就是我首先解密时得到的:

截屏

可能是编码问题。提前致谢。

mkl*_*nt0 10

tl;博士

如果您需要处理原始字节和/或需要防止 PowerShell 在特定情况下向文本数据添加尾随换行符,请完全避免使用PowerShell管道。
相反,cmd使用 with /c(在 Windows 上;在类 Unix 平台/类 Unix Windows 子系统上,使用shbashwith -c):

cmd /c 'type .\test.txt | .\Crypt.exe --encrypt | .\Crypt.exe --decrypt'
Run Code Online (Sandbox Code Playgroud)

请注意,如果要在 PowerShell 变量中捕获输出,则需要确保它[Console]::OutputEncoding.\Crypt.exe程序的(有效)输出编码(活动 OEM 代码页)相匹配,在这种情况下默认情况下应为 true;有关详细信息,请参阅下一节。

但是,通常最好避免对文本数据进行字节操作。


两个单独的问题,其中只有一个作为简单的解决方案:


问题1:确实存在字符编码问题,正如您所怀疑的:

PowerShell 以无形的方式将自己插入管道中,即使在向外部程序发送数据和从外部程序接收数据时也是如此:它将数据从 .NET 字符串( System.String)转换为 .NET 字符串( ),这些字符串是 UTF-16 代码单元的序列。

  • 顺便说一句:即使只使用 PowerShell 原生命令,这意味着从文件中读取输入并再次保存它们可能会导致不同的字符编码,因为一旦(字符串)数据被保存,有关原始字符编码的信息就不会保留读入内存,并在保存时使用 cmdlet 的默认字符编码;虽然此默认编码在PowerShell [Core] 6+ 中始终是无 BOM 的 UTF-8 ,但它因Windows PowerShell 中的 cmdlet 而异- 请参阅此答案

为了向外部程序发送和接收数据(例如Crypt.exe在您的情况下),您需要匹配它们的字符编码;在您的情况下,对于使用原始字节处理的 Windows 控制台应用程序,隐含的编码是系统的活动 OEM 代码页。

  • 发送数据时,PowerShell 使用$OutputEncoding首选项变量的编码编码(总是被视为文本的)数据,在 Windows PowerShell 中默认为 ASCII(!),在 PowerShell [Core] 中默认为 (BOM-less) UTF-8。

  • 接收端通过默认覆盖:PowerShell使用[Console]::OutputEncoding(其本身反映了代码页报道chcp),用于解码接收的数据,并在Windows此默认反映了活性OEM代码页,无论是在Windows PowerShell和PowerShell的[核心] [1] .

要解决您的主要问题,因此您需要设置$OutputEncoding为活动的 OEM 代码页

# Make sure that PowerShell uses the OEM code page when sending
# data to `.\Crypt.exe`
$OutputEncoding = [Console]::OutputEncoding
Run Code Online (Sandbox Code Playgroud)

问题 2PowerShell 在数据通过管道传输到外部程序时,总是在没有换行符的数据上附加一个尾随换行符

也就是说,"foo" | .\Crypt.exe不发送($OutputEncoding表示的-encoded 字节)"foo".\Crypt.exe的标准输入,它"foo`r`n"在 Windows 上发送;即,一个(适用于平台的)换行符序列(Windows 上的 CRLF)会自动且不变地附加(除非字符串已经碰巧有一个尾随换行符)。

这个有问题的行为在这个 GitHub 问题这个答案中都有讨论。

在您的特定情况下,隐式附加"`r`n"也受字节值移位的影响,这意味着第一个Crypt.exe调用将其转换为-*,导致在将数据发送到第二个调用时附加另一个 调用。"`r`n"Crypt.exe

最终结果是一个额外的往返换行符(中间-*),加上一个加密的换行符,导致??)。


简而言之:如果您的输入数据没有尾随换行符,您必须从结果中截去最后 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)
Run Code Online (Sandbox Code Playgroud)

鉴于cmd /c答案顶部显示的调用也有效,这似乎不值得。


PowerShell 如何使用外部程序处理管道数据:

不同于cmd(或类似 POSIX 的 shell,例如bash):

  • PowerShell 不支持管道中的原始字节数据[2]
  • 当与外部程序交谈时,它只知道文本(而在与 PowerShell 自己的命令交谈时它传递 .NET对象,这是它的大部分功能的来源)。

具体来说,它的工作原理如下:

  • 当您通过管道(到其标准输入流)将数据发送外部程序时

    • 它被转换成文本使用在编码指定的字符(串)$OutputEncoding的偏好变量,默认为ASCII(!)中的Windows PowerShell,和(BOM-更少)UTF-8中的PowerShell [核心]

      • 警告:如果您将带有 BOM的编码分配给$OutputEncoding,PowerShell(从 v7.0 开始)将发出 BOM作为发送到外部程序的第一行输出的一部分;因此,例如,不要[System.Text.Encoding]::Utf8在 Windows PowerShell 中使用(它发出 BOM),[System.Text.Utf8Encoding]::new($false)而是使用(它不会)。

      • 如果PowerShell捕获或重定向数据,则编码问题可能并不总是很明显,即是否以使用Windows Unicode 控制台 API打印到显示器的方式实现外部程序。

    • 使用 PowerShell 的默认输出格式(与您打印到控制台时看到的格式相同)将不是文本(字符串)的内容进行字符串化,但有一个重要的警告

      • 如果(最后一个)输入对象已经一个本身没有尾随换行符的字符串,则总是附加一个(甚至现有的尾随换行符也会被平台原生的换行符替换,如果不同的话)。
  • 当您外部程序(从其标准输出流)捕获/重定向数据时,它总是根据 中指定的编码解码为文本行(字符串),[Console]::OutputEncoding默认为 Windows 上的活动 OEM 代码页(令人惊讶的是,在两个PowerShell 版本,从 v7.0-preview6 [1] 开始)。

  • PowerShell 内部文本使用 .NETSystem.String类型表示,该类型基于 UTF-16 代码单元(通常松散地,但错误地称为“Unicode” [3])。

以上也适用

  • 外部程序之间传输数据时,

  • 数据被重定向到文件时;也就是说,无论数据来源及其原始字符编码如何,PowerShell在将数据发送到文件时都使用默认编码;在Windows PowerShell 中>生成 UTF-16LE 编码的文件(带有 BOM),而 PowerShell [Core] 明智地默认为无 BOM 的 UTF-8(始终如一,跨文件写入 cmdlet)。

添加对在外部程序和文件重定向之间传递原始数据的支持是此 GitHub 问题的主题。


[1] 在 PowerShell [Core] 中,鉴于$OutputEncoding已经默认为 UTF-8,值得称赞的[Console]::OutputEncoding是,保持相同是有意义的 - 即,活动代码页65001在 Windows上有效,如此 GitHub 问题中所建议的。

[2] 使用来自文件的输入,最接近原始字节处理的是使用(PowerShell [Core]) / (Windows PowerShell)将文件作为.NETSystem.Byte数组读取,但您可以进一步处理此类文件的唯一方法因为数组是通过管道传输到旨在处理字节数组的PowerShell命令,或者通过将其传递给需要字节数组的 .NET 类型的方法。如果您尝试通过管道将这样的数组发送到外部程序,则每个字节都将作为其十进制字符串表示形式在其自己的行上发送Get-Content -AsByteStreamGet-Content -Encoding Byte

[3] Unicode是描述“全球字母表”的抽象标准的名称。在具体使用中,它有多种标准编码,其中使用最广泛的是UTF-8和UTF-16。

  • 哇!对于字节流来说就这么多了。很棒的信息。非常感谢。 (2认同)