在 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
的。 ksh
和 zsh
也支持 $'...'
字符串,但它们的行为不同:
* 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 中) .应该注意的是,这两个名称非常相似,这令人困惑:NUL
和 null
.
然而,当我们谈论 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",并且都包含 ab
和 cd
的正确值。下一次读取是最后一次读取(因为没有更多的零字节)并且包含值 $'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
在各种 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
的。 ksh
和 zsh
也支持 $'...'
字符串,但它们的行为不同:
* 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 中) .应该注意的是,这两个名称非常相似,这令人困惑:NUL
和 null
.
然而,当我们谈论 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",并且都包含 ab
和 cd
的正确值。下一次读取是最后一次读取(因为没有更多的零字节)并且包含值 $'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