在各种bash脚本中,我遇到了以下内容:$'\0'
有一些背景的例子:
while read -r -d $'\0' line; do
echo "${line}"
done <<< "${some_variable}"
$'\ 0'返回什么值?或者,略有不同,$'\ 0'的评价是什么?为什么?
这可能已在其他地方得到解答。我在发布之前进行了搜索,但是在dollar-quote-slash-zero-quote中有限数量的字符或有意义的单词使得很难从stackoverflow搜索或谷歌获得结果。所以,如果还有其他重复的问题,请允许一些优雅并将其与此问题联系起来。
答案 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)
请注意,此答案大约是bash
。 ksh
和zsh
也支持$'...'
字符串,但其行为不同:
* 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中)。应该注意的是,两个名称非常相似令人困惑: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 ### 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
成功读取到下一个“零字节”,并且都包含正确的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
答案 3 :(得分:1)
$'\0'
将包含的转义序列\0
扩展为它们所代表的实际字符\0
或shell中的空字符。
这是BASH语法。根据{{1}}:
man BASH
形式的单词是专门处理的。单词扩展为字符串,替换为ANSI C标准指定的反斜杠转义字符。已知的反斜杠转义序列也会被解码。
同样$'string'
扩展为换行符,$'\n'
将扩展为回车符。