如何允许bash高效地读取包含NUL字节的数据?
更确切地说,问题是: 有哪些方法可以使如何允许bash高效地读取包含NUL字节的数据?,bash,binary,byte,nul,Bash,Binary,Byte,Nul,更确切地说,问题是: 有哪些方法可以使bash脚本正确、安全地处理N可能包含NUL的字节? 这个问题导致了以下观察结果: bash -c 'LC_ALL=C read -rN 1 </dev/zero' 这样NUL充当分隔符,因此read返回。但是,如果您想跳过超过1个字节,则此技巧将失败,因为读取将在看到第一个NUL后终止 对于特殊情况,有一些变通方法,比如分叉dd,但是如果您想在bash中处理数据,或者需要经常跳过几个字节,那么分叉会带来更大的伤害 如果您想跳过更大的NUL区域,那么
bash
脚本正确、安全地处理N
可能包含NUL
的字节?
这个问题导致了以下观察结果:
bash -c 'LC_ALL=C read -rN 1 </dev/zero'
这样NUL
充当分隔符,因此read
返回。但是,如果您想跳过超过1个字节,则此技巧将失败,因为读取将在看到第一个NUL
后终止
对于特殊情况,有一些变通方法,比如分叉dd
,但是如果您想在bash
中处理数据,或者需要经常跳过几个字节,那么分叉会带来更大的伤害
如果您想跳过更大的NUL
区域,那么在read-d'-n1
上循环也是很麻烦的,因为这是每个字节一个系统调用
注:
- 这不是一个关于哪种解决方案最好的意见的问题
- 这是一个列出处理最常见案例的方法的问题
- 答案应适用于以下用例:
- 管道,你找不到的地方
- 套接字(如
“/dev/tcp/$HOST/$PORT”
)
请始终记住,“性能”不仅仅包括原始速度。它通常包括您需要更改某些内容的时间,从零开始重写需要花费很长时间,或者插入像dd
这样的内容非常困难。通常你所拥有的只是纯粹的bash
。再加上一些帮手
例如,可能有一些更大的脚本应用于类似于git fast export
。此脚本工作正常,直到将第一个带有NUL
字节的二进制文件添加到repo。突然read-N
失去同步,以至于git fast import
抱怨。如果代码主要用于编辑提交消息(被视为二进制数据),则必须复制代码:一个用于二进制,一个用于NUL感知,一个用于提交,以便在bash中更改
这里可能没有“一刀切”这样的事情,所以我们可能需要更多的解决方案,而不仅仅是调用dd
在bash
正在与管道对话的情况下,下面的问题为我解决了
我没有使用producer | bashscript | consumer
而是将一些转换脚本放入管道中:
producer | encoder | bashscript | decoder | consumer
编码器
将00
转义为01 02
并将01
转义为01 03
decoder
unescape00
from0102
和01
from0103
然后,在bash
中,我可以使用以下例程读取N
字节:
: readbytes N variable
readbytes()
{
local -n ___var="${2:-REPLY}"
local ___esc ___tmp
LC_ALL=C read -rN "$1" ___var || return # short read
___esc="$___var"
while ___esc="${___esc//[^$'\x01']/}"
___tmp="${#___esc}"
[ 0 -lt "$___tmp" ]
do
___esc=
LC_ALL=C read -rN "$___tmp" ___esc
___tmp=$?
___var="$___var$___esc"
[ 0 = $___tmp ] || return $___tmp # short read
done
return 0
}
这个程序做什么
- 调用
readbytes N variable
首先将N
字节读入variable
- 然后计算
01
-字节(\1
)
- 每个
01
-字节都有一个第二个字节,因此我们缺少给定的计数
- 因此,读取这个额外的计数并将其附加到
变量中
- 现在,再次显示额外的
01
-字节,因此我们也需要重新读取它们
- 因此,此循环最多以
ld N
步结束
- 因此,与使用
read-N
的O(N)
相比,这个例程最多有O(ldn)
系统调用。当缺少00
-和01
-字节时,此例程仅执行1个系统调用
- 总体悲观的运行时复杂性有点像
O(nldn)
,这并不完美,但在使用read-N
注:
- 此例程不解码数据。因此,如果您读取10个字节,其中有一个
NUL
,您将返回一个11字节的字符串(编码器中的NUL
替换为字节序列01 02
)
- 解码器并不总是需要的,因为
bash
非常适合使用printf'\0'
或printf%b'\0'
之类的内容写入NUL
字节。但是,如果您在更改一些内容的同时将STDIN复制到STDOUT,那么大多数情况下,不转换bash
中的数据并将其留给解码器会更方便
- 在
bash
中可能没有解码数据的好方法,因为bash
变量(与所有环境变量一样)不能包含NUL
以下是一份:
还有一个更复杂的问题:
#!/usr/bin/env python3
import sys
dang = False
while 1:
a = sys.stdin.buffer.read(102400);
if not a: break
if dang:
a = b'\1'+a
dang = False
if a[-1] == 1:
dang = True
a = a[:-1]
sys.stdout.buffer.write(a.replace(b'\1\2', b'\0').replace(b'\1\3', b'\1'))
还包含一个C代码包装器bashnul
,它的运行速度比Python代码快得多(C程序还检测编码错误等)
(注意,它没有经过粗略测试。)
#!/usr/bin/env python3
import sys
while 1:
a = sys.stdin.buffer.read(102400);
if not a: break
sys.stdout.buffer.write(a.replace(b'\1', b'\1\3').replace(b'\0', b'\1\2'))
#!/usr/bin/env python3
import sys
dang = False
while 1:
a = sys.stdin.buffer.read(102400);
if not a: break
if dang:
a = b'\1'+a
dang = False
if a[-1] == 1:
dang = True
a = a[:-1]
sys.stdout.buffer.write(a.replace(b'\1\2', b'\0').replace(b'\1\3', b'\1'))