Bash 里的 word splitting 是很基础的一个知识点,如果没有理解透彻,很多时候会犯下不少奇奇怪怪的错误(参见 Bash Pitfalls,或者本博客翻译 Bash Pitfalls: 编程易犯的错误(一))。

一个例子,现在我们现在要一次读入文件 onefile.txt 的内容并输出,假设文件的内容是这样的:

kodango -> ~/Workspace/coding/test
$ cat onefile.txt
hello world

当我们习惯性地使用 for 循环来解决这个问题时,你会发现输出的结果与预期大相径庭:

$ for i in $(<onefile.txt); do echo "$i"; done
hello
world

给我们的脑子也打开调试开关。首先 onefile.txt 的内容一次性地输出给 for 循环,我在中间用比较形象的 tag 来描述一个空白字符:

hello<blank>world<newline><blank><newline>

这个时候,word splitting 发生了,将以上字符串按照 IFS 分隔成 helloworld。IFS 是用来分隔命令中的每一个单词的,它可以有多个字符组成,每个字符都被视作分隔符。默认情况下,它的值为 <newline><tab><whitespace,这也是为什么默认都是按空格、回车等空白字符分隔的原因。

这样一来 hello 和 world 被分隔可以很好地解释,但是那又是为什么第二行的空行没有了呢?原来,当 IFS 包含空白字符时(比如回车、空格、制表符等),在任何需要分隔单词的场景下,位于字符串开头和结尾的空白字符会被删除,另外一点是,字符串中间的连续空白会被压缩成一个。

例如:

$ str=" here is  a    string "
$ echo $str | cat -A
here is a string$

这是一个比较实用的技巧,在简洁的 Bash Programming 技巧续篇中我们也介绍过使用 tr 命令在实现这个功能。

那如果我想保留字面值,不想被 IFS 执行分隔,需要怎么做?很简单,加上引号:

$ echo "$str" | cat -A
 here is  a    string$

为了达到我们的目的,可以临时通过设置 IFS 为空,来关闭 word splitting 功能:

$ IFS=
$ for i in $(<onefile.txt); do echo "$i"; done
hello world

$ unset IFS

不过即使你理解了 IFS 的含义,你会发现通过 for 循环来读取文件内容是一件很不靠谱的事,建议使用 while 循环:

$ while IFS= read -r line; do echo "$line" ; done < onefile.txt
hello world