在一个 Bash 脚本中,$'\0' 会被解析为什么内容?为什么?

10

在各种bash脚本中,我遇到过以下内容:$'\0'

以下是一个带有一些上下文的示例:

while read -r -d $'\0' line; do
    echo "${line}"
done <<< "${some_variable}"

$'\0'返回什么值?或者稍微不同的说法,$'\0'评估为什么以及它的含义是什么?

可能已经在其他地方回答了这个问题。在发布之前我进行了搜索,但是在dollar-quote-slash-zero-quote中字符数量或有意义的单词数量非常有限,这使得从stackoverflow搜索或google中获得结果非常困难。因此,如果有其他重复的问题,请允许一些余地并将它们链接到此问题。


1
可能是unix.stackexchange.com上的重复问题。 - Ezequiel Tolnay
1
附注:在这里使用 echo 是非常次优的。请参阅 POSIX 规范中的 echo,网址为 http://pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html -- 它有大量未定义的行为。(你的字符串是否为 -n?行为未定义。你的字符串是否包含反斜杠文字?行为未定义。如果你的字符串是 -e-E,那么行为是由 POSIX 定义的,但你可能会遇到违反标准的实现 -- 除非你设置了 xpg_echo 选项。 - Charles Duffy
2
我想象你在提出这个问题时,可能没有预料到会有这么多的严谨和细节要考虑。但是,通过综合所有答案,你应该对这个问题有了非常全面的理解,或者至少有很多可以进一步研究或提出后续问题的地方。 :) - Charles Duffy
2
@CharlesDuffy 我同意。在所有的细节解释中,人们都非常友善和乐于助人。阅读每个答案和评论对我来说很有启发性。总的来说,这让我更好地理解了正在发生的事情。特别是,我学到了1)$'\0'是一个 ANSI C-quoted 字符串 2)在 Bash 中字符串以 NUL 结尾。这两点对我来说都是新的。 - John Mark Mitchell
1
@BinaryZebra,确实,我在上面有点嘲笑自己。 - Charles Duffy
显示剩余3条评论
4个回答

16
在bash中,$'\0'''完全相同:都是空字符串。在这种情况下使用特殊的Bash语法没有任何意义。
Bash字符串始终以NUL结尾,因此如果您设法将NUL插入字符串的中间,它将终止该字符串。在这种情况下,C-escape \0会转换为NUL字符,然后充当字符串终止符。 read内置命令的-d选项(定义输入行结束的字符)在其参数中期望单个字符。它不检查该字符是否为NUL字符,因此使用''的NUL终止符或$'\0'中的显式NUL(也是NUL终止符,因此可能没有任何区别)都可以让它工作。在任一情况下,效果都是读取由find-print0选项产生的以NUL结尾的数据。
read -d '' line <<< "$var'的特定情况下,$var不可能有内部NUL字符(出于上述原因),因此line将被设置为去除了前导和尾随空格的$var的整个值。(正如@mklement所指出的那样,在建议的代码片段中这不会显而易见,因为read将具有非零退出状态,即使变量已经被设置;只有在找到定界符时,read才返回成功,并且NUL不能是here-string的一部分。)
请注意,下面两个命令之间存在很大的区别:
read -d '' line

以及

read -d'' line

第一个是正确的。在第二个中,传递给read的参数单词只是-d,这意味着选项将是下一个参数(在这种情况下是line)。read -d$'\0' line将具有相同的行为;无论哪种情况,空格都是必要的。(因此,不需要C转义语法)。


1
很好的答案。这是一种代码风格问题。我写$'\0'是为了表明我的意图。 - John Kugelman
2
@JohnMarkMitchell:是的。''$'\0'之间绝对没有可检测的区别。(而且,我相信,在内部也真的没有区别。) - rici
2
@binaryzebra:你的printf示例没有使用$'\0';在这种情况下,反斜杠和0被传递给printf。我对zsh没有任何意见,正如你所说,它很有用,但是这个问题显然是关于bash的,我的答案也是如此。不存在空字节这样的东西,我也没有使用过这个短语。因此,我坚持认为在bash中,''$'\0'是无法区分的。 - rici
1
@BinaryZebra,我只会授予你这样的权利:“绝对没有意义”确实可以修改为“绝对没有非风格上的意义”,或类似的说法。 - Charles Duffy
1
@BinaryZebra:重申一下Charles所说的:是“命令替换”而不是“赋值”会去掉NUL字符。这里有一个在NUL处截断的赋值示例:printf -v var 'a\0b'; printf %s "$var" | cat -v。我也想花点时间反思一下你的好斗态度、暴躁和人身攻击,这些真的很不愉快。原本可以成为有趣而富有成果的对话(在我们其他人之间确实如此),却变得恶劣了。 - mklement0
显示剩余30条评论

5
为了补充 rici的有用答案,请注意:此答案仅适用于bashkshzsh也支持$'...'字符串,但它们的行为不同:
* zsh会创建并保留NUL(空字节)使用$'\0'字符串.
* 相反,kshbash具有相同的限制,并且另外将命令替换的输出中的第一个NUL解释为字符串终止符(在第一个NUL处截断而bash则剥除这样的NULs)。

$'\0'是一种ANSI C-quoted string技术上它创建了一个NUL(0x0字节),但实际上效果与空(null)字符串相同(与''相同),因为Bash在参数、here-doc/here-strings的上下文中将任何NUL解释为(C风格)字符串终止符

因此,使用$'\0'有点误导,因为它暗示你可以通过这种方式创建NUL,而实际上你不能:

  • 在命令参数here-doc / here-string不能创建NUL,也不能将NUL存储在变量中

    • echo $'a\0b' | cat -v # -> 'a' - 字符串在'a'后被终止
    • cat -v <<<$'a\0b' # -> 'a' - 同上
  • 相比之下,在命令替换的上下文中,NUL会被删除

    • echo "$(printf 'a\0b')" | cat -v # -> 'ab' - NUL被删除
  • 但是,您可以通过文件管道传递NUL字节。

    • printf 'a\0b' | cat -v # -> 'a^@b' - NUL通过stdout和pipe保留
    • 请注意,是printf通过其单引号参数生成NUL,然后解释并写入stdout。相比之下,如果您使用printf $'a\0b'bash将再次将NUL解释为字符串终止符,并仅传递'a'printf

如果我们检查示例代码,其意图是一次性跨行读取整个输入内容(因此我将line更改为content):

while read -r -d $'\0' content; do  # same as: `while read -r -d '' ...`
    echo "${content}"
done <<< "${some_variable}"
由于标准输入是通过here-string提供的,而here-string不能包含NULs,因此这段代码永远不会进入while循环体中
请注意,read实际上使用-d $'\0'查找NULs,即使$'\0'实际上等效于''换句话说:read按照惯例将空(null)字符串解释为NUL作为-d的选项参数,因为由于技术原因,无法指定NUL本身。

在输入中没有实际NUL的情况下,read的退出代码表示失败,因此循环永远不会执行。

但是,即使在缺少分隔符的情况下,该值也会被读取,因此为了使此代码与here-stringhere-doc一起正常工作,必须进行以下修改:

while read -r -d $'\0' content || [[ -n $content ]]; do
    echo "${content}"
done <<< "${some_variable}"

然而,正如@rici在评论中指出的那样,对于单个(多行)输入字符串,完全不需要使用while

read -r -d $'\0' content <<< "${some_variable}"

这段代码读取$some_variable的全部内容,同时删除开头和结尾的空格(这就是read函数使用默认值$' \t\n'$IFS所做的操作)。
@rici指出,如果不需要删除空格,一个简单的content=$some_variable即可。

与此相反,如果输入实际包含NUL字符,则需要while循环来处理每个以NUL分隔的标记(但没有|| [[ -n $<var> ]]子句);find -print0输出的文件名以NUL分隔:

while IFS= read -r -d $'\0' file; do
    echo "${file}"
done < <(find . -print0)

注意使用IFS= read ...来防止删除前导和尾随空格,因为在这种情况下不需要删除输入文件名中的空格。

如果不需要修剪,那么坚固的解决方案将是content=$some_variable;其余部分都是混淆。但我怀疑修剪是有意的;实际上,这是一个相当聪明的修剪实现,除了while循环外。 - rici
我更多地考虑 || [[ ... ]] 这些东西。 - rici
1
$'\0' 在 zsh 中不会被截断。这证明了 NUL 字节可以在字符串中使用(嗯,某些字符串中)。 - user2350426
2
@BinaryZebra:了解zsh很好,但是这个答案涉及到bash(标题明确写着bash,尽管一般性的标签shshell会引起歧义)。我在顶部添加了一个注释来澄清,并将bash的行为与zshksh进行了对比。 - mklement0
1
太棒了!可惜 Bash 遵循了可怕的 C 零结尾字符串约定。非常讽刺的是,这种约定在许多情况下都非常不方便... - masterxilo

5
技术上说,扩展$'\0'将总是变成空字符串''(也称为null字符串)传递给shell(不包括zsh)。换句话说,$'\0'永远不会扩展为ASCII的NUL(或值为零的字节),(同样不包括zsh)。需要注意的是,两个名称非常相似,这可能会让人感到困惑:NULnull

然而,当涉及到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                           ### That works in bash.

$ printf 'ab\0cd' | od -An -vtx1
61 62 00 63 64                              ### That works in all shells tested.

变量

在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 中结果不同:

### several shells just ignore (remove)
### a NUL in the value of the expanded command.
/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

### ksh trims the the value.
/bin/ksh        :  61 62
/bin/ksh93      :  61 62

### zsh sets the var to actually contain the NUL value.
/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

# create a test file with some zero bytes.
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成功读取到下一个“零字节”,并且两者都包含正确的abcd值。下一个读取是最后一个(因为没有更多的零字节),它包含值$'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

3
在选择bash中的$'\0'''之间,这个问题没有很好的解决方案:选择(a)使用-d $'\0'使read命令自我描述,但会错误地暗示$'\0'创建了一个NUL;或者选择(b)使用read -d ''反映技术上正在发生的情况,但这个选项会使read命令的功能变得难以理解。个人而言——这确实是一种偏好——我更喜欢(b),因为关于$'\0'可以创建一个NUL的错误信念在许多情况下可能会成为问题。 - mklement0
1
@mklement0 感谢,需要一段时间才能使它真正干净。只要理解没有无意义的禁止或隔离其他可能的用途,我不介意任何人使用 $'\0''' - user2350426

1

$'\0'会将包含的转义序列\0扩展为实际表示的字符,即在shell中为\0或空字符。

这是BASH语法。根据man BASH

形如$'string'的单词会被特殊处理。该单词会扩展为字符串,其中反斜杠转义的字符会按照ANSI C标准指定的方式替换。已知的反斜杠转义序列也会被解码。

类似地,$'\n'会扩展为换行符,$'\r'会扩展为回车符。


3
我想这是我第一次因为不准确而对Anubhava的帖子进行了downvote。($'\0'实际上并不会被解释为空字符;如果它们可以在C字符串中表示,那么它们会被解释为空字符,但事实上它们无法在C字符串中表示)。 - Charles Duffy
完全同意@CharlesDuffy和mklement0的观点。我已经删除了不准确的部分,但其他答案解释得更好。 - anubhava
1
@CharlesDuffy NUL在zsh字符串中很好地表示。如果我们在zsh中进行简单的测试:a = "$(printf'ab \ 0cd')"; echo "$ a"| xxd-p会产生61620063640a,是的,在中间存在NUL。因此:一些C创建的代码(嗯;无论如何zsh代码的66.4%)可以正确处理包含NUL的字符串。 - user2350426
1
@BinaryZebra,在bash中,来自命令替换的NUL字符会被删除。是的,在C语言编写的软件中可以使用Pascal字符串(也就是说,将长度元数据存储在带外而不是用终止符表示的字符串),但这需要额外的工作(无法使用标准库字符串处理调用),而且bash(问题中标记的shell)不支持它。 - Charles Duffy
1
@BinaryZebra,我的意思是,“C字符串”并不是指“在C语言中唯一可以表示的字符串类型”,而是指“作为一种专业术语通常被称为‘C字符串’的字符串类型”,这也恰好是C标准库字符串操作函数所处理的唯一一种字符串类型。同样,“Pascal字符串”在比Pascal语言新得多得多的语言中仍然经常使用,但它们仍然保留着那个名称。 - Charles Duffy

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