如何在Bash中使用空字节?


33

我读过,由于Bash中的文件路径可以包含除空字节(零值字节,$'\0')之外的任何字符,因此最好使用空字节作为分隔符。例如,如果的输出find将发送到另一个程序,则建议使用该-print0选项(对于find具有该选项的版本)。

但是,尽管类似的方法可以很好地工作(打印用换行符分隔的文件路径,请放心,这只是一个演示,我实际上并不是在真实的脚本中这样做):

find -print0 \
  | while IFS= read -r -d $'\0' ; do echo "$REPLY" ; done

这样的事情就不会工作:

for file in * ; do echo -n "$file"$'\0' ; done \
  | while IFS= read -r -d $'\0' ; do echo "$REPLY" ; done

当我只尝试for-loop部分时,我发现它只是将所有文件名打印在一起,中间没有空字节。

为什么是这样?这是怎么回事?

Answers:


43

Bash在内部使用C样式的字符串,这些字符串以空字节终止。这意味着Bash字符串(例如变量的值或命令的参数)实际上不能包含空字节。例如,此迷你脚本:

foobar=$'foo\0bar'    # foobar='foo' + null byte + 'bar'
echo "${#foobar}"     # print length of $foobar

实际上打印3,因为$foobar实际上实际上是'foo'bar字符串结尾之后的。

同样,echo $'foo\0bar'仅打印foo,因为echo不知道该\0bar部分。

如您所见,该\0序列实际上在$'...'-style字符串中具有很大的误导性。它看起来像字符串中的一个空字节,但最终无法正常工作。在第一个示例中,您的read命令具有-d $'\0'。这有效,但仅因为-d ''也有效!(这不是的明确记录的功能read,但我想它的工作原理是相同的:''是空字符串,因此它的终止空字节会立即出现。被记录为使用“ delim的第一个字符”,我想它也可以正常工作如果“第一个字符”超出了字符串的结尾!)-d delim

但是当你从你知道的find例子,它可能的命令打印出一个空字节,并为字节通过管道输送到读取它作为输入另一个命令。其中的任何部分都不依赖于在Bash内部的字符串中存储空字节。您的第二个示例的唯一问题是我们不能$'\0'在命令的参数中使用。echo "$file"$'\0'只要知道自己想要的话,就可以在结尾处愉快地输出空字节。

因此echo,可以使用而不是使用printf,它支持与$'...'-style字符串相同类型的转义序列。这样,您可以打印一个空字节,而不必在字符串中包含一个空字节。看起来像这样:

for file in * ; do printf '%s\0' "$file" ; done \
  | while IFS= read -r -d '' ; do echo "$REPLY" ; done

或者只是这样:

printf '%s\0' * \
  | while IFS= read -r -d '' ; do echo "$REPLY" ; done

(注意:echo实际上,还具有一个-e标志,可以使其处理\0并输出一个空字节;但是,它还会尝试处理文件名中的任何特殊序列。因此,这种printf方法更加健壮。)


顺便说一句,有些外壳确实允许在字符串内使用空字节。例如,您的示例在Zsh中运行良好(假设使用默认设置)。但是,无论您使用哪种Shell,类似Unix的操作系统都无法提供在程序参数中包含空字节的方法(因为程序参数是作为C样式字符串传递的),因此始终会有一些限制。(您的示例只能在Zsh中工作,因为echo它是Shell内置的,因此Zsh可以调用它而无需依赖操作系统支持来调用其他程序。如果您使用command echo而不是echo,那么它会绕过内置功能,并在上使用了独立echo程序$PATH,您将在Zsh中看到与在Bash中相同的行为。)


2
如果IFS -d ''已经定界,为什么将IFS设置为空\0?我在这里找到了一个解释:stackoverflow.com/questions/8677546/…–
CMCDragonkai
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.