为什么$'\0'或$'\x0'是空字符串?应该是空字符,不是吗?

23

允许$'字符串'扩展。我的man bash说:

形如$'string'的单词被特殊处理。该单词扩展为string,其中反斜杠转义字符按照ANSI C标准指定的方式替换。如果存在反斜杠转义序列,则按以下方式解码:
\a 警报(响铃)
\b 退格
\e
\E 转义字符
\f 换页符
\n 换行符
\r 回车符
\t 水平制表符
\v 垂直制表符
\ 反斜杠
\' 单引号
\" 双引号
\nnn 八位字符,其值为八进制值nnn(一到三个数字)
\xHH 八位字符,其值为十六进制值HH(一个或两个十六进制数字)
\cx 控制字符x
扩展后的结果是单引号括起来的,就像美元符号不存在一样。

但是,为什么不能将$'\0'$'\x0'转换成空字符?
这是否有文档说明?是否有原因?(它是一个特性还是一个限制甚至是一个错误?)

$ hexdump -c <<< _$'\0'$'\x1\x2\x3\x4_'
0000000   _ 001 002 003 004   _  \n
0000007

echo给出了预期的结果:

> hexdump -c < <( echo -e '_\x0\x1\x2\x3_' )
0000000   _  \0 001 002 003   _  \n
0000007

我的bash版本

$ bash --version | head -n 1
GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)

为什么echo $'foo\0bar'的行为与echo -e 'foo\0bar'不同?


3
好问题!也许这是一个Posix的事情?祝你好运。 - shellter
感谢所有的回答。我在使用netcat测试服务器的SGCI接口时遇到了同样的问题。SCGI头部有NUL字符。在这里阅读后,特别是使用管道的建议后,我开发了一个解决方法。我在需要NUL字符的地方使用八进制377(ASCII 255),然后将字符串通过tr管道传递到netcat之前进行处理。xmlreq='<?xml version="1.0" encoding="UTF-8"?><methodCall><methodName>system.client_version</methodName><params></params></methodCall>' scgihdr=CONTENT_LENGTH$'\377'${#xmlreq}$'\377'SCGI$'\377'1$'\377' echo -n ${#scgihdr} - Dinh S. Roon
3个回答

27

这是一种限制。 bash 不允许字符串值包含内部 NUL 字节。

Posix(和 C)的字符串中不能包含内部 NUL。例如,参见Posix 定义的字符串(重点添加):

3.92 字符串

一个由字符构成的连续序列以及包括第一个空字节

同样,标准的 C 对字符串中的 NUL 字符有明确的规定:

§5.2.1p2 … 一个所有位都为 0 的字节,称为空字符,在基本执行字符集中存在;用于终止一个字符字符串。

Posix 明确禁止在文件名(XBD 3.170)或环境变量(XBD 8.1 "... 被认为以空字节结尾")中使用 NUL(和 /)。

在这个背景下,包括 bash 在内的 shell 命令语言通常使用相同的字符串定义,即由单个 NUL 终止的非 NUL 字符序列。

当然,你可以在 bash 管道中自由传递 NUL,而且没有任何限制阻止你将 shell 变量分配给输出 NUL 字节的程序的输出。但是,根据 Posix 的规定,后果是“未指定的”(XSH 2.6.3“如果输出包含任何空字节,则行为是未指定的。”)。在 bash 中,NUL 会被删除,除非你使用 bash 的 C 转义语法($'\0')将 NUL 插入字符串中,在这种情况下,NUL 将最终终止该值。

在实践中,请考虑尝试向实用程序的 stdin 中插入 NUL 的以下两种方式之间的区别:

$ # Prefer printf to echo -n
$ printf $'foo\0bar' | wc -c
3
$ printf 'foo\0bar' | wc -c
7
$ # Bash extension which is better for strings which might contain %
$ printf %b 'foo\0bar' | wc -c
7

1
很好的信息。关于“没有什么可以阻止您将shell变量分配给输出NUL的程序”的问题,值得指出的是,该变量的值将不可避免地在遇到第一个NUL时被截断。关于“如果您使用bash的反斜杠转义序列之一($'\0')将NUL插入字符串中,则会终止该值。”的问题-澄清一下:将$'\0'插入_另一个_字符串将不会终止整个字符串,而只是忽略$'\0';例如:a$'\0'b-> ab;但是,在$'...'内部的\0将会在那里切断该字符串;例如:$'a\0b'-> a - mklement0
1
@mklement0:是的,我在两年前犯了个错误。谢谢提醒,现在已经修复了。 - rici
@mklement0:但停在NUL处是因为您已使用“-d''”告诉read这样做。如果read遇到NUL并且记录终止符是换行符(或任何其他不是NUL的字符),则行为与命令替换一致:NUL被剥离但字符串未终止。换句话说,除非您指定了NUL作为终止符,否则read将读取超过NUL。 - rici
谢谢你指出这个问题:原来在Bash 4.3.3中引入了read去除NUL字符的功能(详情请参考链接)。在Bash 4.3.2及以下版本中,read永远不会读取超过NUL字符的部分。举个例子,在Bash 3.2.57中执行read -r var < <(printf 'a\0b'),变量$var只会被赋值为a,NUL字符后面的内容会被丢弃。 - mklement0
@mklement0:啊,有趣。我想我会在这个回答中坚持使用“未指定”;read并不是非常相关的。(如果是,就必须看看printf -v和其他一些完全不相关的东西了,这与OP没有关系。) - rici
显示剩余2条评论

5
但是为什么Bash不会将$'\0'$'\x0'转换成空字符呢?
因为空字符会终止一个字符串。
$ echo $'hey\0you'
hey

4

这是一个空字符,但它的含义取决于你的意图。

空字符代表一个空字符串,当你扩展它时,得到的结果也是空字符串。它是一个特殊情况,我认为文档中暗示了这一点,但并没有明确说明。

在C语言中,二进制零'\0'终止一个字符串,并且它本身也表示一个空字符串。Bash是用C语言编写的,所以它很可能遵循这个规则。

编辑: POSIX在许多地方都提到了空字符串。在“基本定义”中,它将一个空字符串定义为:

3.146 空字符串(或 Null 字符串)
其第一个字节为空字节的字符串。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接