關於 Word Splitting 和 IFS 的三個細節

紫雲飛發表於2015-10-21

在 Bash manual 裡叫 Word Splitting,在 Posix 規範裡叫 Field Splitting,這兩者指的是同一個東西,我把它翻譯成“分詞”,下面我就說三點很多人都忽略掉(或者說從沒仔細考慮過)的分詞細節。

1. 分隔符到底是字元還是字串?

IFS 裡面可以包含多個字元,那麼在分詞的過程中,是 IFS 中的每個單獨的字元作為分隔符,還是由這些字元組合成的任意字串作為分隔符?我們寫個簡單的例子證明一下:

$ var=a12b21c IFS=12

$ printf "<%s>\n" $var

<a>

<>

<b>

<>

<c>

由於輸出了兩個空的欄位,也就證明了是 1 和 2 兩個單獨的 IFS 字元作為了分隔符,而不是 12 和 21 這兩個由 IFS 字元組成的字串作為了分割符。但結論沒這麼簡單,再看一個例子:

$ var=$'1 \t 2' IFS=$' \t' #紅色背景的是空格

$ printf "<%s>\n" $var

<1>

<2>

在這個例子中,IFS 包含兩個字元:空格符和製表符, 如果說它們倆是單獨作為分隔符的,那麼 $var 就應該被分割成四個欄位,分別是 <1> <> <> <2>,但實際的結果並不是這樣的。這是因為:空格符、製表符(\t)、換行符(\n)這三個空白符在 IFS 中會被特殊對待,Shell 會把它們按照任意順序任意數量組合成的字串作為分隔符,而不是單個字元作為分隔符。在這個例子中,是“ \t ”整體作為了一個分隔符,把 1 和 2 分成了兩個欄位。下面再演示一下 IFS 為換行符的情況:

$ var=$'1\n\n\n2\n\n\n3' IFS=$'\n'

$ printf "<%s>\n" $var

<1>

<2>

<3>

這個例子中,三個連續的換行符作為了分隔符,把 var 分成了三個欄位。

如果 IFS 既包含空白符,又包含非空白符,會怎麼樣?

看下面的例子,IFS 中既有空白符 \n 又有非空白符 2:

$ var=$'1\n\n2\n\n3' IFS=$'\n2'

$ printf "<%s>\n" $var

<1>

<3>

咦?有些同學就想問了:上面不是說,Shell 會把以任意個 IFS 包含的空白符組成的字串作為分隔符,把單個 IFS 中包含的非空白符作為分隔符嗎,那不就是有三個分隔符:“\n\n”、“2”、“\n\n”嗎?但從表現上來看,是“\n\n2\n\n”整體作為了一個分隔符,這是怎麼回事?

下面我們就再說個法則:“一個 IFS 中包含的非空白符會和它兩邊存在的由 IFS 中包含的空白符組成的字串組合成一起作為分隔符”。在上面的例子中,就是 2 和它兩邊的 “\n\n” 5個字元組合起來作為了一個分割符,所以產生了 1 和 3兩個欄位。

2. 尾部的空欄位會被丟棄

$ var=:1:2:3: IFS=:

$ printf "<%s>\n" $var

<>

<1>

<2>

<3>

四個分隔符,應該把 var 切割成 5 個欄位,但從結果上看,尾部的空欄位不見了?是的,再說一個法則:分詞之後,如果最後一個欄位是空的,那麼這個欄位會被丟棄掉。其實,一個包含空值的變數在分詞之後會被丟棄,也符合這條法則:

$ var=""

$ set -- $var

$ echo $#

0

上面的例子中,var 的值就是空,所以在分詞之後也是隻有一個空的欄位,也是最後一個欄位,符合尾部空欄位被丟棄的法則,所以 set 命令只看到了 -- 這一個引數。 

3. 首尾的空白符序列會被丟棄掉

$ var=$'\n1:2\n' IFS=$'\n:'

$ printf "<%s>\n" $var

<1>

<2>

這個例子中,分割符應該有三個,分別是 \n、:、\n,它們會把 var 分割成四個欄位 <> <1> <2> <>,尾部的欄位是空的,被丟棄,就成了 <> <1> <2>。咦?WTF,為什麼和真實的輸出不符!下面是最後一條法則:在正式分詞之前,變數兩邊的由 IFS 包含的空白符組合成的序列會被丟棄掉,然後才進行正式分詞。在上面的例子中,var 會先被切頭去尾,也就變成 “1:2”,才進行正式的分詞,也就最終被分成了 1 和 2 兩個欄位了。注意,首尾的空白符序列只包含由 IFS 中包含的空白符組成的序列,比如上面的例子改一下:

$ var=$'\n1:2\n' IFS=$'\t:'

$ printf "<%s>\n" $var

<

1>

<2

>

由於 \n 沒有包含在 IFS 中,所以 var 首尾的 \n 也就不會被去掉。 關於這點,Bash 的文件記載有 bug,我給提 bug 修復了。

最後說一句,本文中所舉的例子都是用 parameter expansion 來演示的,command substitution 和 arithmetic expansion 雖然沒有演示,但同樣適用。

相關文章