问题描述
更准确地说,问题是:
哪些方法可以使 bash
脚本正确、安全地处理可能包含 N
的 NUL
字节?
这个问题导致了以下观察:
bash -c 'LC_ALL=C read -rN 1 </dev/zero'
- 使用 Debian 10 的
bash
版本5.0.17(1)-release
进行测试
(I tried to find out myself but found no pointer why this happens)。到目前为止我发现的是,“我的”bash
显然跳过了 NUL
上的所有 read -N
字节。
在 -N 1
的特殊情况下可能的解决方法是使用
LC_ALL=C IFS= read -rd '' -n 1
这样 NUL
作为分隔符,所以 read
返回。但是,如果您想跳过超过 1 个字节,这个技巧就会失败,因为 read
在看到第一个 NUL
后终止。
对于特殊情况,有一些变通方法,例如分叉 dd
,但是如果您想处理 bash
中的数据或需要经常跳过几个字节,分叉的弊大于利。
如果您想跳过更大的 read -d '' -n 1
区域,循环 NUL
也很麻烦,因为这是每字节一个系统调用。
注意事项:
请始终牢记,“性能”不仅仅包括原始速度。它通常包括您需要更改某些内容的时间,从头开始重写内容需要很长时间,或者插入诸如 dd
之类的东西变得极其困难。很多时候你所拥有的只是纯粹的bash
。还有一些帮手。
例如,可能有一些更大的脚本应用于 git fast-export
之类的内容。这个脚本可以完美运行,直到第一个带有 NUL
字节的二进制文件被添加到 repo 中。突然 read -N
不同步了,以至于 git fast-import
抱怨。如果代码主要用于编辑提交消息(它们被视为二进制数据),您必须复制代码:一个用于二进制,NUL 感知,一个用于提交,以在 bash 中更改。
可能没有一刀切的事情,所以我们可能需要更多的解决方案,而不仅仅是调用 dd
。
解决方法
在 bash
正在与管道通话的情况下,以下为我解决了这个问题。
我没有使用 producer | bashscript | consumer
,而是将一些转换脚本放入管道中:
producer | encoder | bashscript | decoder | consumer
-
encoder
将00
转义为01 02
,将01
转义为01 03
。 -
decoder
对00
的01 02
和01
的01 03
进行转义。
然后,在 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
字节都有第二个字节,因此我们的给定计数很短。 - 因此请阅读此额外计数并将其附加到
variable
。 - 现在,可能还会出现额外的
01
字节,因此我们也需要重新读取它们。 - 这个循环因此最多在
ld N
步结束 - 因此,与带有
O(ld N)
的O(N)
相比,此例程最多只有read -n
个系统调用。当00
- 和01
-bytes 不存在时,此例程仅执行 1 次系统调用。 - 整体悲观运行时复杂度有点像
O(N ld N)
,虽然不完美,但在使用O(N*N)
时比read -n
好得多
注意事项:
此例程不解码数据。因此,如果您读取 10 个字节并且其中有一个
NUL
,您将返回一个 11 个字节的字符串(NUL
被来自编码器的字节序列01 02
替换)。-
解码器并不总是需要的,因为
bash
非常适合用NUL
或printf '\0'
之类的东西写入printf %b '\0'
字节。但是,如果您主要将 STDIN 复制到 STDOUT 并同时更改一些内容,那么大多数情况下,不转换bash
内的数据并将其留给解码器会更方便。 -
可能没有好的方法来解码
bash
中的数据,因为bash
变量(与所有环境变量一样)不能包含NUL
。
这是一个encoder in Python3:
#!/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'))
还有 decoder in Python3 只是稍微复杂一点:
#!/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'))
complete git
repo on GitHub 还包含一个 C 代码包装器 bashnul
,其运行速度比 Python 代码快得多(C 程序也会检测编码错误等)。
(请注意,它没有经过彻底测试。)