技术上说,扩展
$'\0'
将总是变成空字符串
''
(也称为
null字符串)传递给shell(不包括zsh)。换句话说,
$'\0'
永远不会扩展为ASCII的
NUL
(或值为零的字节),(同样不包括zsh)。需要注意的是,两个名称非常相似,这可能会让人感到困惑:
NUL
和
null
。
然而,当涉及到read -d ''
时,还有一个额外(相当令人困惑)的细节。
read
看到的是值''
(空字符串)作为分隔符。
read
执行的操作是在从标准输入读取的文本上使用字符$'\0'
(实际上是一个0x00
)进行分割。
详细说明如下:
标题中的问题是:
在bash脚本中,$'\0'
会被解析为什么,为什么?
这意味着我们需要解释$'\0'
被扩展为什么。
实际上$'\0'
被扩展为空字符串''
(在大多数shell中,不包括zsh)。
但使用的示例是:
read -r -d $'\0'
这将问题转化为:$'\0'扩展到哪个分隔符字符?
这个问题非常令人困惑。要正确回答这个问题,我们需要全面了解在shell中何时以及如何使用NUL(一个值为零或'0x00'的字节)。
流。
我们需要一些NUL来处理。可以从shell生成NUL字节:
$ echo -e 'ab\0cd' | od -An -vtx1
61 62 00 63 64 0a
$ printf 'ab\0cd' | od -An -vtx1
61 62 00 63 64
变量
在Shell中,变量不会存储NUL。
$ printf -v a 'ab\0cd'; printf '%s' "$a" | od -An -vtx1
61 62
这个示例需要在bash中执行,因为只有bash printf有-v
选项。
但是这个示例清楚地表明,包含NUL的字符串将在NUL处被截断。
简单变量将在零字节处“切割”字符串。
如果字符串是C字符串,则合理地期望它必须以NUL \0
结尾。
一旦发现NUL,字符串就必须结束。
命令替换。
当在命令替换中使用NUL时,它会有不同的作用。
以下代码应该将一个值赋给变量$a
,然后打印它:
$ a=$(printf 'ab\0cd'); printf '%s' "$a" | od -An -vtx1
而且确实如此,但在不同的 shell 中结果不同:
/bin/dash : 61 62 63 64
/bin/sh : 61 62 63 64
/bin/b43sh : 61 62 63 64
/bin/bash : 61 62 63 64
/bin/lksh : 61 62 63 64
/bin/mksh : 61 62 63 64
/bin/ksh : 61 62
/bin/ksh93 : 61 62
/bin/zsh : 61 62 00 63 64
/bin/zsh4 : 61 62 00 63 64
特别提醒的是,bash(版本4.4)警告了以下事实:
/bin/b44sh : warning: command substitution: ignored null byte in input
61 62 63 64
在命令替换中,零字节会被shell默默地忽略。但是需要非常重要的理解,这在zsh中不会发生。
现在我们已经了解了有关NUL的所有内容。我们可以看看read命令的作用。
关于NUL分隔符,这让我们回到了
read -d $'\0'
命令:
while read -r -d $'\0' line; do
$'\0'
应该被扩展为值为
0x00
的一个字节,但是shell会截断它,实际上变成了
''
。这意味着read函数将
$'\0'
和
''
视为相同的值。
话虽如此,编写等效的代码可能看起来是合理的:
while read -r -d '' line; do
这是技术上正确的。
''作为定界符的实际作用
从这个问题有两个方面需要考虑,一个是read命令中-d选项后的字符,另一个是本篇讨论的:如果给定定界符-d $'\0'
,read命令会使用哪个字符?
第一个方面已经在上面详细解答了。
对于第二个方面,这里涉及了一个非常困惑人的地方,即read
命令实际上会读取到下一个值为0x00
的字节(这也是$'\0'
所代表的意义)。
为了证明这一点,可以执行如下命令:
#!/bin/bash
printf 'ab\0cd\0ef\ngh\n' > tfile
while true ; do
read -r -d '' line; a=$?
echo "exit $a"
if [[ $a == 1 ]]; then
printf 'last %s\n' "$line"
break
else
printf 'normal %s\n' "$line"
fi
done <tfile
执行后,输出结果将为:
$ ./script.sh
exit 0
normal ab
exit 0
normal cd
exit 1
last ef
gh
第一个和第二个
exit 0
成功读取到下一个“零字节”,并且两者都包含正确的
ab
和
cd
值。下一个读取是最后一个(因为没有更多的零字节),它包含值$'ef\ngh'(是的,它还包含一个新行)。
所有这些都表明(并证明)
read -d ''
实际上读取到了下一个“零字节”,这也被ascii名称
NUL
所知道,并且应该是
$'\0'
扩展的结果。
简而言之:我们可以安全地说
read -d ''
读取到了下一个
0x00
(NUL)。
结论:
我们必须声明
read -d $'\0'
将扩展为分隔符
0x00
。
使用
$'\0'
是传达给读者这个正确含义的更好方法。
作为代码风格的一件事:我写$'\0'以明确我的意图。
只有一个字符用作分隔符:字节值为
0x00
(即使在bash中它被切割了)
注意:这些命令将打印流的十六进制值。
$ printf 'ab\0cd' | od -An -vtx1
$ printf 'ab\0cd' | xxd -p
$ printf 'ab\0cd' | hexdump -v -e '/1 "%02X "'
61 62 00 63 64
echo
是非常次优的。请参阅 POSIX 规范中的echo
,网址为 http://pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html -- 它有大量未定义的行为。(你的字符串是否为-n
?行为未定义。你的字符串是否包含反斜杠文字?行为未定义。如果你的字符串是-e
或-E
,那么行为是由 POSIX 定义的,但你可能会遇到违反标准的实现 -- 除非你设置了xpg_echo
选项。 - Charles Duffy$'\0'
是一个 ANSI C-quoted 字符串 2)在 Bash 中字符串以 NUL 结尾。这两点对我来说都是新的。 - John Mark Mitchell