在一个bash脚本中,$'\ 0'会评估什么?为什么?

Joh*_*ell 6 linux bash shell sh

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

一些上下文的示例:

while read -r -d $'\0' line; do
    echo "${line}"
done <<< "${some_variable}"
Run Code Online (Sandbox Code Playgroud)

$'\ 0'作为其值返回什么?或者,略有不同,$'\ 0'评估为什么?为什么?

这有可能在其他地方得到解答.我在发布之前进行了搜索,但是在dollar-quote-slash-zero-quote中有限数量的字符或有意义的单词使得很难从stackoverflow搜索或谷歌获得结果.因此,如果还有其他重复问题,请允许一些优雅并将其与此问题联系起来.

ric*_*ici 12

在bash中,$'\0'恰好相同'':一个空字符串.在这种情况下,使用特殊的Bash语法绝对没有意义.

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

所述-d的的选项read内置(其限定了线端字符输入)预计其参数的单个字符.它不检查该字符是否是NUL字符,因此使用NUL终结符''或显式NUL $'\0'(它也是NUL终止符,因此它可能没有什么不同)同样高兴.在任何一种情况下,效果都是读取NUL终止的数据,例如由find(s)-print0选项产生的数据.

在特定情况下read -d '' line <<< "$var',不可能$var有一个内部NUL字符(由于上述原因),因此line将被设置为$var删除了前导和尾随空格的整个值.(正如@mklement所说,这在建议的代码片段中不会显而易见,因为read即使变量已经设置,也会有非零退出状态; read只有在实际找到分隔符时才返回成功,并且NUL不能here-string的一部分.)

请注意,两者之间存在很大差异

read -d '' line
Run Code Online (Sandbox Code Playgroud)

read -d'' line
Run Code Online (Sandbox Code Playgroud)

第一个是正确的.在第二个中,传递给的参数单词read是just -d,这意味着该选项将是下一个参数(在本例中line).read -d$'\0' line会有相同的行为; 在任何一种情况下,空间都是必要的.(所以,再次,不需要C-escape语法).

  • @JohnMarkMitchell:是的.'''和'$'\ 0'`之间绝对没有可检测到的区别.(而且,我相信,内部也没有任何区别.) (2认同)
  • @binaryzebra:你的printf示例不使用`$'\ 0'`; 在这种情况下,反斜杠和0被传递给printf.我没有反对zsh,正如你所说的那样有用,但这个问题显然是关于bash的,我的答案也是如此.没有空字节这样的东西,我没有使用那个短语.所以我坚持认为,在bash中,'''和'$'\ 0'`是无法区分的. (2认同)

mkl*_*nt0 5

补充rici的有用答案

请注意,这个答案是关于bash. ksh并且zsh还支持$'...'字符串,但它们的行为不同:
*zsh确实使用$'\0'. 相比之下,
*ksh具有与 相同的限制bash,并且另外将命令替换输出中的第一个 NUL 解释为字符串终止符(在第一个 NUL 处切断,而bash 剥离此类NUL )。

$'\0'是一个ANSI C 引用的字符串,它在技术上创建了一个 NUL(0x0字节),但实际上导致了空(空)字符串(与 相同'',因为任何 NUL 都被 Bash 在上下文中解释为(C 风格)字符串终止符参数和here-docs/here-strings。

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

  • 不能创建 NUL作为命令参数here-doc / here-string 的一部分也不能将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
    • 请注意,它是printf通过其单引号参数生成 NUL,printf然后将其转义序列解释并写入标准输出。相比之下,如果您使用printf $'a\0b',bash将再次将 NUL 解释为前面的字符串终止符并仅传递'a'printf.

如果我们考察示例代码,其目的是读取整个输入一次,跨线(因此我改变了linecontent):

while read -r -d $'\0' content; do  # same as: `while read -r -d '' ...`
    echo "${content}"
done <<< "${some_variable}"
Run Code Online (Sandbox Code Playgroud)

永远不会进入while循环体,因为 stdin 输入是由here-string提供的,正如解释的那样,它不能包含 NUL。
请注意,read实际上确实会使用寻找 NUL -d $'\0',即使$'\0'是有效的''. 换句话说:read 按照惯例,将空(空)字符串解释为 NUL 作为-d选项参数,因为出于技术原因不能指定 NUL 本身。

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

但是,即使没有分隔符,该值也会被读取,因此要使此代码与 here-string 或 here-doc 一起使用,必须进行如下修改:

while read -r -d $'\0' content || [[ -n $content ]]; do
    echo "${content}"
done <<< "${some_variable}"
Run Code Online (Sandbox Code Playgroud)

然而,作为一个评论@rici笔记,用(多线)输入字符串,就没有必要使用while在所有

read -r -d $'\0' content <<< "${some_variable}"
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

  • @BinaryZebra:很高兴了解`zsh`,但这个答案是关于`bash`(标题清楚地表示`bash`,尽管通用标签`sh` 和`shell` 会产生歧义)。我在顶部添加了一个注释来澄清,这也将 `bash` 的行为与 `zsh` 和 `ksh` 的行为进行了对比。 (2认同)

小智 5

从技术上讲,扩展$'\0'将始终成为shell(不在 zsh 中)的空字符串''(又名字符串)。或者,反过来说, a$'\0'永远不会扩展为 ascii NUL(或具有零值的字节),(同样,不在 zsh 中)。应该指出的是,这两个名称非常相似令人困惑:NULnull

然而,当我们谈论read -d ''.

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

什么read 确实是从标准上的字符分割输入$'\0'(是一个实际的0x00)。


扩展答案。

标题中的问题是:

在 bash 脚本中, $'\0' 会评估什么,为什么?

这意味着我们需要解释$'\0'扩展到什么。

$'\0'扩展为什么非常简单:它扩展为空字符串''(在大多数 shell 中,而不是在 zsh 中)。

但是使用的例子是:

read -r -d $'\0'
Run Code Online (Sandbox Code Playgroud)

这将问题转换为: $'\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.
Run Code Online (Sandbox Code Playgroud)

多变的。

shell 中的变量不会存储 NUL。

$ printf -v a 'ab\0cd'; printf '%s' "$a" | od -An -vtx1
61 62
Run Code Online (Sandbox Code Playgroud)

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

命令替换。

在命令替换中使用 NUL 时,其工作方式会有所不同。此代码应为变量分配一个值$a,然后打印它:

$ a=$(printf 'ab\0cd'); printf '%s' "$a" | od -An -vtx1
Run Code Online (Sandbox Code Playgroud)

确实如此,但在不同的 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
Run Code Online (Sandbox Code Playgroud)

特别值得一提的是,bash(4.4 版)警告了以下事实:

/bin/b44sh      :  warning: command substitution: ignored null byte in input
61 62 63 64
Run Code Online (Sandbox Code Playgroud)

在命令替换中,shell 默默地忽略了零字节。
了解 zsh 中不会发生这种情况非常重要。

现在我们有了关于 NUL 的所有内容。我们可以看看 read 做了什么。

read对 NUL 分隔符做什么。

这让我们回到命令read -d $'\0'

while read -r -d $'\0' line; do
Run Code Online (Sandbox Code Playgroud)

$'\0'shoud 已扩展为一个字节的值0x00,但外壳将其切割并实际上变成了''. 这意味着这两个 $'\0'''通过读取接收为相同的值。

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

while read -r -d '' line; do
Run Code Online (Sandbox Code Playgroud)

它在技术上是正确的。

'' 的分隔符实际上做了什么。

这一点有两个方面,一个是 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
Run Code Online (Sandbox Code Playgroud)

执行时,输出将是:

$ ./script.sh
exit 0
normal ab
exit 0
normal cd
exit 1
last ef
gh
Run Code Online (Sandbox Code Playgroud)

前两个exit 0是成功读取进行到下一个“零字节”,并且都包含了正确的价值观abcd。下一次读取是最后一次(因为没有更多的零字节)并包含值 $'ef\ngh' (是的,它还包含一个新行)。

所有这一切都表明(并证明)read -d ''实际上读取到下一个“零字节”,这也以 ascii 名称已知NUL并且应该是$'\0'扩展的结果。

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

结论:

我们必须声明 aread -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
Run Code Online (Sandbox Code Playgroud)

  • ++ 进行了大大改进的重写。在`bash`中重新选择`$'\0'`和`''`:这个问题没有好的解决方案:选择是在(a)使用`-d $'\0'`来制作` read` 命令自记录,其代价是错误地暗示 `$'\0'` 创建了一个 NUL,并且 (b) 使用 `read -d ''` 来反映正在_技术上_发生的事情,其代价是混淆 `read` 命令的功能。就我个人而言——这确实是_偏好_的问题——我更喜欢 (b),因为错误地认为 `$'\0'` 创建了一个 NUL 在很多情况下都会成为一个问题。 (3认同)