在Bash中将包含空字符(\0)的字符串赋值给变量

39

在尝试通过使用空字符作为分隔符正确处理文件/文件夹名称列表(请参见我的其他问题)时,我遇到了Bash的一个奇怪行为,我不理解:

当将包含一个或多个空字符的字符串赋值给变量时,空字符会丢失/被忽略/未存储。

例如,

echo -ne "n\0m\0k" | od -c   # -> 0000000   n  \0   m  \0   k

但是:
VAR1=`echo -ne "n\0m\0k"`
echo -ne "$VAR1" | od -c   # -> 0000000   n   m   k

这意味着如果不能直接使用管道传输,我需要将该字符串写入文件(例如,在 /tmp 中),然后从那里读取它。在 Z shell (zsh) 中执行这些脚本时,包含 \0 的字符串在两种情况下都会被保留,但不幸的是,我不能假设运行我的脚本的系统中存在 zsh,而 Bash 则应该是存在的。如何高效地存储或处理包含 \0 字符的字符串而不丢失任何(元)字符?

1
如果有人想知道为什么不能存储 \0,那是因为它是用来分隔变量结尾的字符。因此,存储 NUL 字符基本上就相当于将变量设置为空。 - Nick Bull
5个回答

40
在Bash中,你不能将NULL字符存储在变量中。
但是,你可以使用xxd命令存储数据的十六进制编码(并在需要时再次转换回原始数据)。
VAR1=`echo -ne "n\0m\0k" | xxd -p | tr -d '\n'`
echo -ne "$VAR1" | xxd -r -p | od -c   # -> 0000000    n  \0   m  \0   k

1
不错)我正在使用VAR1 =“ $(echo -ne' n \ 0m \ 0k' | sed 's / \ \ / g; s / \ x0 / \ 0 / g')”; echo -ne“ $ VAR1” | od -c#-> 0000000 n \ 0 m \ 0 k 顺便说一句,在输出示例中不应使用echo上的-e,而且我认为我的示例可能使用更少的内存,但我认为这是无关紧要的。 - XzKto
谢谢你的好回答!为什么不使用-e来进行echo呢?如果-E(默认)被激活,\0会被解释为2个字符(''和'0'),所以我猜使用-e应该没问题(因为\0只是NUL-char的转义)?我同意-e可能在@XzKto的解决方案中不起作用...无论如何,还是感谢你提供了这个第二种方法! - antiplex
我的意思是@jeff在他的示例中(在第二个echo中)不应该使用-e,因为它完全没有用,但我只是挑剔一下)。 - XzKto
1
@jeff 奇怪的是我之前没有意识到这一点,但我猜添加 tr -d '\n' 并不是必要的,因为 xdd -r -p 似乎会吞掉/删除自动添加的换行符,至少在我使用的 bash v4.2.24 和 zsh v4.3.17 中是这样的。如果我在这里忽略了什么,请纠正我 ;) - antiplex
printf可能更好:假设我想处理一个以null结尾的用户名和密码。此外,假设用户名和密码分别为“user”和“new\nlines”。您会发现使用echo -en "$username\0$password"\n解释为换行符而不是字符串的一部分。更好的方法是使用printf "%s\0%s" "$username" "$password"或在这个问题的情况下,VAR1=$(printf "%s\0%s\0%s" n m k | xxd -p | tr -d '\n') - b_laoshi

20

正如其他人所说,你不能存储或使用 NUL 字符

  • 在变量中
  • 在命令行的参数中

然而,你可以处理任何二进制数据(包括 NUL 字符):

  • 在管道中
  • 在文件中

因此,回答你的最后一个问题:

 

有人能给我指点一下,如何高效地存储或处理包含 \0 字符的字符串,而不会丢失任何(元)字符吗?

你可以使用文件或管道来高效地存储和处理任何带有任何元字符的字符串。

如果你计划处理数据,还应注意以下内容:

绕过限制

如果你想要使用变量,那么你必须通过编码来摆脱 NUL 字符,这里的其他各种解决方案都提供了聪明的方法来做到这一点(一个明显的方法是使用 base64 编码/解码)。

如果你关注内存或速度,你可能只想使用最小的解析器,并且只带引号 NUL 字符(和引号字符)。在这种情况下,这将对你有所帮助:

quote() { sed 's/\\/\\\\/g;s/\x0/\\x00/g'; }

然后,您可以通过将敏感数据输入quote来确保在将其存储在变量和命令行参数中之前保护您的数据,这将输出一个不含NUL字符的安全数据流。 您可以使用echo -en "$var_quoted"将原始字符串(包含NUL字符)返回,它将在标准输出上发送正确的字符串。

示例:

## Our example output generator, with NUL chars
ascii_table() { echo -en "$(echo '\'0{0..3}{0..7}{0..7} | tr -d " ")"; }
## store
myvar_quoted=$(ascii_table | quote)
## use
echo -en "$myvar_quoted"

注意:使用| hd可以以十六进制形式清晰地查看数据,并检查是否丢失了任何NUL字符。

更改工具

请记住,您可以在命令行中不使用变量或参数的情况下使用管道达到很远的效果,例如,请不要忘记<(command ...)结构,它将创建一个命名管道(一种临时文件)。

编辑:quote的第一个实现是不正确的,并且不会正确处理由echo -en解释的\特殊字符。感谢@xhienne发现这个问题。

编辑2:quote的第二次实现存在bug,因为仅使用\0,实际上会吃掉更多的零,因为\0\00\000\0000是等价的。所以\0被替换为\x00。感谢@MatthijsSteen发现这个问题。


1
有趣的回答,但是 quote 函数似乎有问题。它可以正确地将 \0 字符替换为 \0,但未能转义原始流中的所有转义序列,这些序列最终会被后续的 echo -en 命令解释。 - xhienne
@xhienne 真是太准确了!感谢您的评论,我已经纠正了“quote”的实现。 - vaab
1
在使用过程中,我遇到了引用函数中的一个错误。不应该使用\0,因为它会吃掉后面的3个零(echo -en '\00000' ~> 0),而是应该使用\0000\x00。所以它应该变成这样:quote() { sed 's/\\/\\\\/g;s/\x0/\\x00/g'; } - Matthijs Steen

12

使用uuencodeuudecode以实现POSIX可移植性

xxdbase64不符合POSIX 7标准,但uuencode符合

VAR="$(uuencode -m <(printf "a\0\n") /dev/stdout)"
uudecode -o /dev/stdout <(printf "$VAR") | od -tx1

输出:

0000000 61 00 0a
0000003

很遗憾,除了将其写入文件之外,我没有看到Bash进程<()替换扩展的POSIX 7替代方案,并且它们在Ubuntu 12.04中默认未安装(sharutils包)。

所以我想真正的答案是:不要使用Bash来完成这个任务,使用Python或其他更合理的解释型语言。


为了避免使用Bash进程替代<(),不如使用VAR="$(printf "a\0\n" | uuencode -m -)"来替代VAR="$(uuencode -m <(printf "a\0\n") /dev/stdout)",您觉得如何? - undefined

4

我喜欢 杰夫的回答。我会使用Base64编码而不是xxd。它可以节省一点空间,并且更容易被认为是预期的内容。

VAR=$(echo -ne "foo\0bar" | base64)
echo -n "$VAR" | base64 -d | xargs -0 ...

关于-e,它用于带有编码的空值('\0')的字面字符串的回显,尽管我也记得关于“echo -e”不安全,如果您回显任何用户输入,因为他们可以注入echo将解释并最终导致问题的转义序列。在将编码的存储字符串回显到解码时,不需要使用-e标志。

1
需要使用-e参数。执行命令echo -n 'a\0b' | xxd -p会得到十六进制值为615c3062,代表4个字节而不是3个字节。与之对比,执行命令echo -ne 'a\0b' | xxd -p会得到610062的结果。(双引号同样适用) - Martin Jambon
这是因为单引号。-e 告诉 echo 获取传递给它的字面字符串,并对转义(和其他内容)进行评估。在单引号的情况下,echo 确实获取了转义序列,然后将其解释为空值。如果改用双引号,则 shell 会在 echo 获取之前将 \0 转换为空字符。 - vontrapp
1
'\0'"\0"在posix和bash中是等效的,它们都代表一个两个字节的字符串。 echo "\0"将打印\0,就像echo '\0'一样。 - Martin Jambon
1
我谦卑地纠正了一下。是的,'a\0b'和"a\0b"甚至a\0b都需要-e标志才能使echo输出空字符。我正在更新答案。如果有任何用户输入,我确实认为-e标志会有害。无论如何,这很可能会用于捕获除echo之外生成空字符的东西的输出(例如find -print0),并且没有必要使用-e将其“echo”出来,也没有必要在回显存储的编码返回到解码步骤时使用-e。代码中文字面上的echo -e是可以的(对于示例是必要的)。 - vontrapp

0
这里有一个最大化内存效率的解决方案,它只是用 \xFF 转义了 NULL 字节。
(因为我对 base64 或类似方法不满意。 :))
esc0() { sed 's/\xFF/\xFF\xFF/g; s/\x00/\xFF0/g'; }
cse0() { sed 's/\xFF0/\xFF\x00/g; s/\xFF\(.\)/\1/g'; }

当然,它也通过将其加倍来避免任何实际的\xFF,因此它的工作方式与使用反斜杠进行转义时完全相同。这也是为什么不能使用简单映射,并且需要引用替换中的匹配。

以下是一个示例,它使用变量预渲染块和线以提高速度,在帧缓冲区上绘制渐变(在X中不起作用):

width=7680; height=1080; # Set these to your framebuffer’s size.
blocksPerLine=$(( $width / 256 ))
block="$( for i in 0 1 2 3 4 5 6 7 8 9 A B C D E F; do for j in 0 1 2 3 4 5 6 7 8 9 A B C D E F; do echo -ne "\x$i$j"; done; done | esc0 )"
line="$( for ((b=0; b < blocksPerLine; b++)); do echo -en "$block"; done )"
for ((l=0; l <= $height; l++)); do echo -en "$line"; done | cse0 > /dev/fb0

请注意,$block 包含转义的 NULL(加上 \xFF),在将所有内容写入帧缓冲区之前,cse0 会对它们进行反转义处理。

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