在PowerShell管道上输出二进制数据

22

我需要将一些数据传输到一个程序的标准输入 (stdin)中:

  1. 前四个字节是一个32位的无符号整数,表示数据的长度。这4个字节与C语言中存储无符号整数的方式完全相同,我称其为二进制数据。
  2. 剩余的字节则是数据本身。

在C语言中,这很简单:

WriteFile(h, &cb, 4);  // cb is a 4 byte integer
WriteFile(h, pData, cb);
或者
fwrite(&cb, sizeof(cb), 1, pFile);
fwrite(pData, cb, 1, pFile);

或者在 C# 中,您可以使用 BinaryWriter(我认为这段代码是正确的,我现在手头没有 C#...)

Bw.Write((int)Data.Length);
Bw.Write(Data, 0, Data.Length);
在PowerShell中我相信这是可能的,但这是我能做到的最接近的。显然,这会将大小的4个字节打印为4个可读数字:
$file = "c:\test.txt"
Set-content $file "test data" -encoding ascii
[int]$size = (Get-ChildItem $file).Length
$bytes = [System.BitConverter]::GetBytes($size)
$data = Get-content $file
$bytes
$data
11
0
0
0
test data

我需要通过管道发送的二进制数据看起来像这样(\xA 是不可打印字符的转义表示,我不希望在输出中出现 '\',而是要求输出 '\xA' 表示的字节):

\xA\x0\x0\0test data

我不知道如何以二进制格式将字节数组写出管道。我也不知道如何去掉回车符。

编辑: 我发现我可以这样做:

$file = "c:\test.txt"
Set-content $file "test data" -encoding ascii
"File: ""{0}""" -f (Get-content $file)
[int]$size = (Get-ChildItem $file).Length
"Size: " + $size
$bytes = [System.BitConverter]::GetBytes($size)
"Bytes: " + $bytes
$data = Get-content $file
$file1 = "c:\test1.txt"
Set-content $file1 $bytes -encoding byte
Add-Content $file1 $data -encoding ASCII
"File: ""{0}""" -f (Get-content $file1)
"Size: " + (Get-ChildItem $file1).Length
File: "test data"
Size: 11
Bytes: 11 0 0 0
File: "   test data"
Size: 15

但这需要我建立一个临时文件。一定有更好的方法!

编辑:上面的解决方案会破坏任何字符编码 > 127。这个管道没有“二进制”编码模式。

编辑:我最终发现了一个绕路的方法,可以将BinaryWriter与应用程序的stdin连接起来。请参见我的回答


叹气,为什么会被踩呢?我有2063个贡献点,所以我不是寄生虫。这不是一个作业问题,主要是因为我已经毕业20年了。那么是什么原因呢? - johnnycrash
第一个4个字节是什么的长度?而“二进制长度”是什么意思? - Bill_Stewart
前4个字节是接下来数据的长度。数据的长度包含在32位整数中。长度必须以二进制编码。因此,如果传输的数据长度为10,则前4个字节将是0A 00 00 00。然后是10个数据字节。 - johnnycrash
啊,那就是十六进制,而不是二进制,并且是大端序(即,它将是0A 00 00 00,而不是00 00 00 0A)? - Bill_Stewart
我所指的“二进制”是指32位整型数字的机器原始表示,而不是人类可读的版本。不知道该如何用其他方式表达呵呵。在C语言中,我们会这样做:INT32 cb = 11; fwrite(&cb, sizeof(cb), 1, pFile); 而不是使用powershell喜欢的fprintf(pFile, "%d", cb)。 - johnnycrash
显示剩余2条评论
5个回答

18

Bill_Stewart 正确指出您无法使用管道传递二进制数据。当您使用 | 运算符时,PowerShell 将使用由 $OutputEncoding 指定的编码方式。我找不到一种不会破坏数据的编码方式。

不过我找到了一种可行的方法,即使用 BinaryWriter

这是我的测试代码,从 C:\foo.exe 开始,它只是输出它接收到的数据:

#include <windows.h>
#include <stdio.h>

int main(int argc, char* argv[])
{
    HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE); 
    BYTE aBuf[0x100];
    int nRet;
    DWORD cbRead;
    if (!(nRet = ReadFile(hInput, aBuf, 256, &cbRead, NULL)))
        return printf("err: %u %d %d", cbRead, nRet, GetLastError());
    for (int i=0 ; i<256 ; ++i)
        printf("%d ", aBuf[i]);
    return 0;
}

这个 PowerShell 脚本演示了“损坏”:

$data = [Byte[]] (0..255)

$prefix = ($data | ForEach-Object {
  $_ -as [Char]
}) -join ""
"{0}" -f $prefix
$OutputEncoding = [System.Text.Encoding]::GetEncoding("us-ascii")
$prefix | c:\foo.exe

这里是输出结果。首先可以看到$prefix包含完整的字符集。其次,您可以看到传递给foo.exe的数据已经被转换。

 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
 ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 5
0 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 9
7 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 63 63 63 63 63 63 63 
63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 
63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 
63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63

使用 BinaryWriter 是可行的:

$data = [Byte[]] (0..255)

$ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo 
$ProcessInfo.FileName = "C:\foo.exe"
$ProcessInfo.RedirectStandardInput = $true 
$ProcessInfo.RedirectStandardOutput = $true 
$ProcessInfo.UseShellExecute = $false 
$Proc = New-Object System.Diagnostics.Process 
$Proc.StartInfo = $ProcessInfo 
$Proc.Start() | Out-Null 

$Writer = New-Object System.IO.BinaryWriter($proc.StandardInput.BaseStream);
$Writer.Write($data, 0, $data.length)
$Writer.Flush()
$Writer.Close()

$Proc.WaitForExit()
$Proc.StandardOutput.ReadToEnd()
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 5
0 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 9
7 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 1
33 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 16
8 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255

所以,将长度以二进制形式写入数据文件的最终脚本,看起来会像这样:

$data = [Byte[]] (0..255)

$ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo 
$ProcessInfo.FileName = "C:\foo.exe"
$ProcessInfo.RedirectStandardInput = $true 
$ProcessInfo.RedirectStandardOutput = $true 
$ProcessInfo.UseShellExecute = $false 
$Proc = New-Object System.Diagnostics.Process 
$Proc.StartInfo = $ProcessInfo 
$Proc.Start() | Out-Null 

$Writer = New-Object System.IO.BinaryWriter($proc.StandardInput.BaseStream);
$Writer.Write([Int32]$data.length)
$Writer.Write($data, 0, $data.length)
$Writer.Flush()
$Writer.Close()

$Proc.WaitForExit()
$Proc.StandardOutput.ReadToEnd()

您可以看到第一个字节0 1 0 0是等于256[Int32]的原始二进制表示:

0 1 0 0 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 1
31 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 16
6 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251

1
谢谢,这让我感到很烦恼。无论我采用哪种方法,二进制数据都会被搞乱(而且是一致的,所以显然遵循某种逻辑)。 - Kruft

9

我需要将一些数据通过stdin输入到一个程序中。

当使用不同的编码时,确实会引起许多问题。这里有一个不使用Get-/Set-Content应用任何编码的不同方法。

您实际上可以使用Start-Process命令来将二进制数据传输到外部程序:

Start-Process my.exe -RedirectStandardInput my.bin

自PowerShell 2.0以来就可用。


正如这里只创建了一个进程一样,-RedirectStandardInput 没有传输任何内容;文件 my.bin 被提供给新的 my.exe 进程的 stdin。如果您已经有或不介意创建一个文件作为新进程的 stdin 使用,那么这可能是最好的解决方案。如果像问题中一样,您希望一个进程向另一个进程的 stdin 提供数据而没有中间文件参与,则此方法无法实现。johnnycrash 的答案 显示了如何使用重定向来将任意数据提供给子进程的 stdin - Lance U. Matthews

2
这个对你有用吗?
$fileName = "C:\test.txt"
$data = [IO.File]::ReadAllText($fileName)
$prefix = ([BitConverter]::GetBytes($data.Length) | foreach-object {
  "\x{0:X2}" -f $_
}) -join ""
"{0}{1}" -f $prefix,$data

如果您希望$prefix包含字节的原始数据表示,可以使用$_ -as [Char]替换"\x{0:X2}" -f $_


1
不好意思,因为我想要流的前4个字节是32位数字的原始机器表示。2的补码。这可以通过使用Set-Content $file $bytearray -encoding byte来实现。 - johnnycrash
你如何将其导入到另一个程序的标准输入?假设数据长度为10(0A),这是一个换行符,用于命令解释器。 - Bill_Stewart
请查看更新的答案 - 虽然我不知道你将如何处理命令解释器不允许你向另一个程序的标准输入发送某些控制字符的事实。 - Bill_Stewart
1
我无法控制正在传输数据的程序所期望的内容。我只是试图自动化一个大规模的测试。这是接收程序正在执行的操作:ReadFile(m_hInput,&ulMsgLen,sizeof(ulMsgLen),&cbBytes,NULL); m_strMsg.PszAllocate(ulMsgLen+1) ; ReadFile(m_hInput,m_strMsg,m_strMsg.Cch(),&cbBytes,NULL)) - johnnycrash
1
“我无法控制我正在传输数据的程序期望什么” - 这就是我试图表达的意思。如果您尝试将数据传输到另一个程序的标准输入,并且该数据包含控制字符(从shell的角度来看),则shell将对这些控制字符进行操作(例如,我的示例中的换行符)而不是将它们传递给您程序的标准输入。” - Bill_Stewart
1
我给了你的解决方案和评论一个+1,因为它们指引了我正确的方向,即“|”符号使用编码来处理输出。由于没有“原始”编码,因此似乎无法使用“|”符号在管道上输出二进制数据。它似乎不受控制字符的影响。 - johnnycrash

2
[System.Console]::OpenStandardOutput().Write($bytes, 0, $bytes.Length)

6
请提供一些关于你的解决方案的信息,例如代码如何工作等。 - Tox

1

使用BinaryWriter的简短示例:

$file = 'c:\temp\test.txt'
$test = [byte[]](0..255)
$mode = [System.IO.FileMode]::Create
$stream = [System.IO.File]::Open($file, $mode)
$bw = [System.IO.BinaryWriter]::new($stream)
$bw.Write($test)
$bw.Flush()
$bw.Dispose()
$stream.Dispose()

你正在将测试数组写入 test.txt(即使它是二进制数据,而不是文本)。 然后呢? 它如何被管道传输到下一个进程中? 该问题明确要求避免使用临时文件。 - Lance U. Matthews
1
这个例子回答了我另外一个问题:如何使用Powershell在原地修改二进制文件(使用这些确切的关键词,因为这正是我在搜索的)。$mode必须是::Open,并且必须使用$bw.Seek($Offset,0)。结果:文件按照我想要的方式被修改,但不需要加载到内存中,因为文件的大小是可用内存的数倍。 - undefined

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