在 CMD 和 PowerShell 中管道时的不同行为和输出

Posted

技术标签:

【中文标题】在 CMD 和 PowerShell 中管道时的不同行为和输出【英文标题】:Different behaviour and output when piping in CMD and PowerShell 【发布时间】:2022-01-24 04:36:24 【问题描述】:

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

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

    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_*

这就是我第一次解密时得到的:

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

【问题讨论】:

【参考方案1】:

tl;dr

从 PowerShell 7.2 开始,如果您需要 原始字节处理 和/或需要防止 PowerShell 在特定情况下向您的文本数据添加尾随换行符,请避免PowerShell 管道。

未来支持在外部程序之间传递原始字节数据和文件重定向是GitHub issue #1908 的主题。

对于原始字节处理,使用/c 输出到cmd(在Windows 上;在类Unix 平台/类Unix Windows 子系统上,使用sh 或@ 987654333@ 和-c):

cmd /c 'type .\test.txt | .\Crypt.exe --encrypt | .\Crypt.exe --decrypt'

使用类似的技术将原始字节输出保存在 文件 - 不要使用 PowerShell >运营商:

cmd /c 'someexe > file.bin'

请注意,如果您想在 PowerShell 变量中捕获外部程序的文本输出,您需要确保[Console]::OutputEncoding匹配程序的输出字符编码(通常是活动的 OEM 代码页),在这种情况下默认情况下应该是 true;有关详细信息,请参阅下一节。

一般来说,最好避免字节操作文本数据。


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


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

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

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

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

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

接收默认被覆盖:PowerShell使用[Console]::OutputEncoding(它本身反映了chcp报告的代码页)用于解码接收到的数据,在 Windows 上,这默认反映活动的 OEM 代码页,包括 Windows PowerShell 和 PowerShell [Core][1]

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

# Make sure that PowerShell uses the OEM code page when sending
# data to `.\Crypt.exe`
$OutputEncoding = [Console]::OutputEncoding

问题 2PowerShell在将数据传送到外部程序时,总是将尾随换行符附加到没有新行符的数据上:

"foo" | .\Crypt.exe不发送($OutputEncoding-encoded bytes表示)"foo".\Crypt.exe的stdin,它在Windows上发送"foo`r`n";即,(适用于平台的)换行符序列(Windows 上的 CRLF)会自动且始终附加(除非字符串已经恰好有一个尾随换行符)。

GitHub issue #5974 和this answer 中讨论了这种有问题的行为。

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

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


简而言之:如果您的输入数据有 no 尾随换行符,则您必须从结果中删除 最后 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 如何使用外部程序处理管道数据:

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

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

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

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

使用 $OutputEncoding 首选项变量中指定的字符编码转换为文本(字符串),默认为ASCII( !) 在 Windows PowerShell 中,以及在 PowerShell (Core) 中的 (BOM-less) UTF-8。

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

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

使用 PowerShell 的默认输出格式(与打印到控制台时看到的格式相同)对尚未成为文本的内容(字符串)进行字符串化,并带有一个重要警告

如果(最后一个)输入对象已经一个本身没有尾随换行符的字符串,则总是附加一个 (甚至现有的尾随换行符也被替换为平台原生的换行符,如果不同的话)。 此行为可能会导致问题,如 GitHub issue #5974 和 this answer 中所述。

当您从外部程序(从其标准输出流)捕获/重定向数据时,它总是解码为文本行 em>(字符串),基于 [Console]::OutputEncoding 中指定的编码,默认为 Windows 上的活动 OEM 代码页(令人惊讶的是,在 both PowerShell版本,截至 v7.0-preview6[1])。

PowerShell 内部文本使用 .NET System.String type 表示,它基于 UTF-16 代码单元(通常松散但不正确地称为“Unicode”[3])。 p>

上述也适用

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

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


[1] 在 PowerShell (Core) 中,鉴于 $OutputEncoding 已经默认为 UTF-8,值得称赞的是,让 [Console]::OutputEncoding 相同 - 即,活动代码页是正如GitHub issue #7233 中所建议的那样,在Windows 上有效地65001

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

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

【讨论】:

哇!对于作为字节流的流来说就这么多了。很棒的信息。非常感谢。 powershellgallery.com/packages/Use-RawPipeline 是一个不错的选择,直到 PowerShell 添加本地方式来处理此问题。

以上是关于在 CMD 和 PowerShell 中管道时的不同行为和输出的主要内容,如果未能解决你的问题,请参考以下文章

从CMD管道到PowerShell

使用 cmd.exe 或 PowerShell 或 Python 从 Windows 命名管道中读取

通过管道将 CMD 输出传递给 Powershell [重复]

popen

想要将 PSList 包装在 Powershell 函数中以使用管道值

是否可以从过滤器中终止或停止 PowerShell 管道