我读过,由于 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 部分时,我发现它只是将所有文件名打印在一起,没有之间的空字节。
为什么是这样?这是怎么回事?
答案1
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
部分。
正如您所看到的,该序列在-style 字符串\0
中实际上非常具有误导性;$'...'
它看起来像字符串中的一个空字节,但它最终不会以这种方式工作。在您的第一个示例中,您的read
命令具有-d $'\0'
.这有效,但只是因为-d ''
也有效! (这不是 的明确记录的功能read
,但我认为它的工作原理相同:''
是空字符串,因此它的终止空字节立即出现。-d delim
记录为使用“第一个字符德利姆”,我想如果“第一个字符”超出了字符串的末尾,甚至可以工作!)
但正如你从你的find
例子中知道的那样是一个命令可以打印出一个空字节,并且该字节可以通过管道传输到另一个将其读取为输入的命令。其中没有任何部分依赖于存储空字节在 Bash 中的字符串中。第二个示例的唯一问题是我们不能$'\0'
在命令的参数中使用;echo "$file"$'\0'
如果它知道您想要的话,就可以愉快地在末尾打印空字节。
echo
因此,您可以使用来代替使用 ,它支持与-style 字符串printf
相同类型的转义序列。$'...'
这样,您就可以打印空字节,而不必在字符串中包含空字节。那看起来像这样:
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 中相同的行为。)