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

时间:2016-04-13 02:47:20

标签: linux bash shell sh

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

有一些背景的例子:

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

$'\ 0'返回什么值?或者,略有不同,$'\ 0'的评价是什么?为什么?

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

4 个答案:

答案 0 :(得分:11)

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

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

-d builtin的read选项(定义输入的行尾字符)期望其参数中包含单个字符。它不会检查该字符是否是NUL字符,因此使用''的NUL终结符或$'\0'中的显式NUL(也是NUL终结符)也同样高兴,因此可能没有区别)。在任何一种情况下,效果都是读取由find的{​​{1}}选项生成的NUL终止数据。

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

请注意,

之间存在很大差异
read

read -d '' line

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

答案 1 :(得分:4)

补充rici's helpful answer

请注意,此答案大约是bash kshzsh也支持$'...'字符串,但其行为不同:
* zsh确实使用$'\0' 创建并保留NUL(空字节)。
相比之下,* ksh bash具有相同的限制,而另外将命令替换输出中的第一个NUL解释为字符串终结符(在第一个NUL切断,而bash 剥离这样的NUL)。 功能

$'\0'ANSI C-quoted string 技术上创建了NUL(0x0字节),但是 有效会产生空(null)字符串(与''相同),因为任何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' - ditto
  • 相反,在命令替换的上下文中, NUL被剥离

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

    • printf 'a\0b' | cat -v # -> 'a^@b' - 通过stdout和pipe
    • 保留
    • 请注意,printf通过其单引号参数生成NUL,其转义序列printf然后解释并写入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}"

这将永远不会进入while循环体,因为stdin输入是由 here-string 提供的,如上所述,不能包含NUL 请注意, read实际 会查找-d $'\0'的NUL,即使$'\0'实际上是'' 换句话说:read 按惯例将空(null)字符串解释为NUL为-d的option-argument,因为NUL本身不能指定为技术原因。

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

但是,即使没有分隔符,值也是 read ,因此要使此代码与here-string或here-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$IFS的默认值为$' \t\n')。<登记/> @rici还指出,如果不需要这样的修剪,可以使用简单的content=$some_variable

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

find -print0

注意使用while IFS= read -r -d $'\0' file; do echo "${file}" done < <(find . -print0) 来抑制前导和尾随空格的修剪,在这种情况下这是不希望的,因为输入文件名必须按原样保留。
功能

答案 2 :(得分:4)

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

然而,当我们谈论read -d ''时,会有一种非常令人困惑的错误。

read 参见是值''(空字符串)作为分隔符。

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

扩大答案。

tittle中的问题是:

  

在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

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

### 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' shoud已扩展为值0x00的字节,但shell会将其剪切,实际上变为''。 这意味着,$'\0'''都会被读取为相同的值。

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

while read -r -d '' line; do

技术上是正确的。

'实际上是什么分隔符。

这一点有两个方面,一个是读取-d选项后面的字符,另一个是此处解决的字符:如果给定分隔符为-d $'\0',将使用哪个字符读取? ?

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

第二个方面非常令人困惑,因为命令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 :(得分:1)

$'\0'将包含的转义序列\0扩展为它们所代表的实际字符\0或shell中的空字符。

这是BASH语法。根据{{​​1}}:

  

man BASH形式的单词是专门处理的。单词扩展为字符串,替换为ANSI C标准指定的反斜杠转义字符。已知的反斜杠转义序列也会被解码。

同样$'string'扩展为换行符,$'\n'将扩展为回车符。