在 bash 脚本中,$'\0' 的计算结果是什么?为什么?

In a bash script, what would $'\0' evaluate to and why?

在各种 bash 脚本中,我遇到了以下情况:$'[=12=]'

一个有一定背景的例子:

while read -r -d $'[=11=]' line; do
    echo "${line}"
done <<< "${some_variable}"

$'\0' return 的值是多少?或者,换句话说,$'\0' 的计算结果是什么?为什么?

这个问题可能已经在其他地方得到了回答。我在发布之前进行了搜索,但是美元引号-斜线-零引号中的字符数或有意义的单词数量有限,因此很难从 Whosebug 搜索或 google 中获得结果。所以,如果还有其他重复的问题,请允许一些宽限期,并从这个问题中 link 它们。

$'[=10=]' 将包含的转义序列 [=11=] 扩展为它们代表的实际字符,即 [=11=] 或 shell.

中的空字符

这是 BASH 语法。根据 man BASH:

Words of the form $'string' are treated specially. The word expands to string, with backslash-escaped characters replaced as specified by the ANSI C standard. Known backslash escape sequences are also decoded.

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

在bash中,$'[=12=]'''完全相同:一个空字符串。在这种情况下,使用特殊的 Bash 语法完全没有意义。

Bash 字符串总是以 NUL 结尾,因此如果您设法将 NUL 插入到字符串中间,它将终止该字符串。在这种情况下,C-escape [=14=] 被转换为 NUL 字符,然后充当字符串终止符。

read 内置函数的 -d 选项(定义输入的行尾字符)在其参数中需要单个字符。它不检查该字符是否为 NUL 字符,因此使用 '' 的 NUL 终止符或 $'[=12=]' 中的显式 NUL(这也是一个 NUL 终止符,因此可能是没有什么不同)。在任何一种情况下,效果都是读取以 NUL 结尾的数据,如 find-print0 选项生成的(例如)。

read -d '' line <<< "$var'的特定情况下,$var不可能有一个内部NUL字符(由于上述原因),所以line将被设置为$var 的整个值,删除了前导和尾随的白色 space。 (正如@mklement 指出的那样,这在建议的代码片段中不会很明显,因为 read 将具有非零退出状态,即使已设置变量;read 仅 returns 如果确实找到了分隔符,则成功,并且 NUL 不能是此处字符串的一部分。)

注意

有很大区别
read -d '' line

read -d'' line

第一个是正确的。在第二个中,传递给 read 的参数词只是 -d,这意味着选项将是 下一个参数 (在这种情况下,line). read -d$'[=30=]' line 将具有相同的行为;在任何一种情况下, space 都是必需的。 (所以,再一次,不需要 C 转义语法)。

补充

注意这个答案是关于bash的。 kshzsh 也支持 $'...' 字符串,但它们的行为不同:
* zsh 使用 $'[=19=]'.
创建并保留 NUL(空字节) * ksh相比之下,与bash具有相同的限制,并且另外解释命令中的第一个NUL替换的输出作为字符串终止符( 在第一个 NUL 处切断 ,而 bash 去除 这样的 NUL。

$'[=19=]' 是一个 ANSI C-quoted string技术上 创建一个 NUL(0x0 字节) , 但 有效地 导致空 (null) 字符串(与 '') 相同,因为任何 NUL 都被解释为 (C -style) string terminator by Bash in the context of arguments and here-docs/here-strings.

因此,有点误导使用$'[=19=]',因为它建议您可以通过这种方式创建 NUL ,当你实际上不能:

  • 不能创建 NUL 作为命令的一部分参数here-doc / here-string,而你 不能 将 NUL 存储在变量:

    • echo $'a[=27=]b' | cat -v # -> 'a' - 字符串 在 'a'
    • 之后终止
    • cat -v <<<$'a[=28=]b' # -> 'a' - 同上
  • 命令替换的上下文中,相比之下,NUL被剥离:

    • echo "$(printf 'a[=29=]b')" | cat -v # -> 'ab' - NUL 被剥离
  • 但是,您可以通过文件传递NUL字节,并且管道.

    • printf 'a[=30=]b' | cat -v # -> 'a^@b' - NUL 保留,通过标准输出和管道
    • 请注意,printf 通过其 单引号 参数生成 NUL,其转义序列 printf 然后解释并写入标准输出。相比之下,如果您使用 printf $'a[=33=]b'bash 会再次将 NUL 解释为前面的字符串终止符,并且仅将 'a' 传递给 printf

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

while read -r -d $'[=10=]' content; do  # same as: `while read -r -d '' ...`
    echo "${content}"
done <<< "${some_variable}"

这将永远不会进入while循环体,因为stdin输入由提供here-string,如前所述,它不能包含 NUL。
请注意 read 实际上 确实 使用 -d $'[=41=]' 查找 NUL,即使 $'[=19=]' 实际上是 ''换句话说:read 按照惯例 将空(null)字符串解释为 NUL 作为 -d 的选项参数,因为 NUL由于技术原因无法指定本身。

如果输入中没有实际的 NUL,read 的退出代码表示失败,因此永远不会进入循环。

然而,即使没有定界符,该值也是 read,因此要使此代码与此处 - string or here-doc,必须修改如下:

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

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

read -r -d $'[=12=]' content <<< "${some_variable}"

这会读取 $some_variable 的全部内容,同时修剪前导和尾随空格(这是 read$IFS 的默认值 $' \t\n' 所做的) .
@rici 还指出,如果不需要这样的修剪,一个简单的 content=$some_variable 就可以了。

将此与实际包含 NUL 的 输入进行对比 ,在这种情况下 while 需要处理每个 NUL 分隔令牌(但没有 || [[ -n $<var> ]] 子句); find -print0 输出由 NUL 分隔的文件名):

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

请注意使用 IFS= read ... 来抑制前导和尾随空格的修剪,这在这种情况下是不希望的,因为输入文件名必须按原样保留。

从技术上讲,扩展$'[=21=]'将始终成为空字符串''(a.k.a。null字符串)到shell(不在 zsh 中)。或者,换句话说,$'[=21=]' 永远不会扩展为 ascii NUL(或零值字节),(同样,不在 zsh 中) .应该注意的是,这两个名称非常相似,这令人困惑:NULnull.

然而,当我们谈论 read -d '' 时,还有一个额外的(非常令人困惑的)转折。

什么read 是值''(空字符串)作为分隔符。

read所做的是将来自标准输入的输入拆分为字符$'[=21=]'(是的实际0x00)。


扩展答案。

标题中的问题是:

In a bash script, what would $'[=62=]' evaluate to and why?

这意味着我们需要解释 $'[=21=]' 扩展到什么。

$'[=21=]' 的扩展非常简单:它扩展为空字符串 ''(在大多数 shell 中,而不是在 zsh 中)。

但是使用的例子是:

read -r -d $'[=10=]'

将问题转换为:$'\0' 扩展为什么分隔符?

这是一个非常令人困惑的转折点。为了正确解决这个问题,我们需要全面了解 shells.

中何时以及如何使用 NUL(具有零值或“0x00”的字节)

串流。

我们需要一些 NUL 来处理。可以从 shell:

生成 NUL 字节
$ echo -e 'ab[=11=]cd' | od -An -vtx1
61 62 00 63 64 0a                           ### That works in bash.

$ printf 'ab[=11=]cd' | od -An -vtx1
61 62 00 63 64                              ### That works in all shells tested.

变量。

shell 中的变量不会存储 NUL。

$ printf -v a 'ab[=12=]cd'; printf '%s' "$a" | od -An -vtx1
61 62

该示例旨在在 bash 中执行,因为只有 bash printf 具有 -v 选项。 但是这个例子很清楚地表明,包含 NUL 的字符串将在 NUL 处被截断。 简单变量将 剪切 零字节处的字符串。 如果字符串是 C 字符串,则可以合理预期,它必须以 NUL [=37=] 结尾。 一旦找到 NUL,字符串就必须结束。

命令替换。

NUL 在命令替换中使用时会有所不同。 此代码应为变量 $a 赋值,然后打印它:

$ a=$(printf 'ab[=13=]cd'); printf '%s' "$a" | od -An -vtx1

确实如此,但在不同的 shells 中有不同的结果:

### 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 做了什么。

read 对 NUL 分隔符的作用。

这让我们回到命令 read -d $'[=40=]':

while read -r -d $'[=16=]' line; do

$'[=21=]' 应该被扩展为一个字节值 0x00,但是 shell 将其剪切,它实际上变成了 ''。 这意味着 $'[=21=]''' 都被读取为相同的值。

话虽如此,编写等效的构造似乎是合理的:

while read -r -d '' line; do

这在技术上是正确的。

分隔符 '' 的实际作用。

这一点有两个方面,一个是read的-d选项后面的字符,另一个是这里提到的:what character will read use if given a delimiter as -d $'[=46 =]'?.

上面已经详细回答了第一方

第二面非常令人困惑,因为命令 read 实际上会读取值 0x00 的下一个字节(这就是 $'[=21=]' 所代表的内容)。

实际证明是这样的:

#!/bin/bash

# create a test file with some zero bytes.
printf 'ab[=18=]cd[=18=]ef\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 成功读取到下一个 "zero byte",并且都包含 abcd 的正确值。下一次读取是最后一次读取(因为没有更多的零字节)并且包含值 $'ef\ngh'(是的,它还包含一个换行符)。

所有这些都表明(并证明)read -d '' 实际上读取到下一个 "zero byte",它也以 ascii 名称 NUL 为人所知,应该是$'[=21=]' 扩展的结果。

简而言之:我们可以安全地声明 read -d '' 读取到下一个 0x00 (NUL)。

结论:

我们必须声明 read -d $'[=40=]' 将扩展为 0x00 的分隔符。 使用 $'[=21=]' 是向 reader 传递此正确含义的更好方法。 作为代码风格的东西:我写 $'\0' 来明确我的意图。

一个,而且只有一个,作为分隔符的字符:0x00的字节值 (即使在bash中恰好被切掉了)


注意:此命令将打印流的十六进制值。

$ printf 'ab[=20=]cd' | od -An -vtx1
$ printf 'ab[=20=]cd' | xxd -p
$ printf 'ab[=20=]cd' | hexdump -v -e '/1 "%02X "'
61 62 00 63 64