1. 程式人生 > >詳細解析Shell中的IFS變數

詳細解析Shell中的IFS變數

image

題圖:Photo by Jacob Postuma on Unsplash
本文原創釋出於微信公眾號“洛奇看世界”,一個大齡2b碼農的世界

這裡的Shell主要指bash,學習bash的前前後後在IFS變數上吃了不少苦頭,雖然花了不少時間,也知道大概如何使用,但並沒有深入理解。翻了幾本Shell相關的書,對IFS也都是一帶而過,並沒有做詳細的闡述(IFS本身在Shell裡面就是很小很小的一個知識點而已,也不值得這些書花大篇幅去解釋);嘗試百度“Shell IFS”,大多數結果也不甚滿意。終於決定要自己完整的瞭解下IFS了。嚴格來說,本文是對IFS文件描述和使用的考證說明。

本文主要有以下幾個話題:

  • 如果想了解我是如何找到介紹IFS資料的,請跳轉到第1節,如何找到介紹IFS的資料?
  • 如果想知道Bash手冊是如何介紹IFS的,請跳轉到第2節, Bash手冊中關於IFS的介紹
  • 如果想看一些IFS使用相關的例子,請跳轉到第3節, IFS使用的一些例子
  • 如果想看一些IFS的結論,請跳轉到第4節;
  • 如果想了解本文附帶有哪些福利,請跳轉到第5節;

1. 如何找到介紹IFS的資料?

這個章節挺廢話的,但為什麼還會有這個章節呢?我只是希望通過這個章節向有些朋友展示我是如何思考,找到解決這個問題的方法的。拿到一個問題,並不是所有朋友都一下子能拿出很好的解決辦法,這其中必然有個思考嘗試的過程,而這一節,就是想向你展示我是如何思考的。這個過程可能走了很多彎路,並非一下就能找到正確的答案,但仍希望我的思考能對你有一絲的借鑑意義。

想了解IFS,必然需要找到詳細的資料才行,可是,如果從來沒了解過IFS,從哪裡找到介紹IFS的資料呢?

查詢資料的第一反應是搜尋,但直接以關鍵字”IFS”/”shell IFS”/”bash IFS”進行百度,得到一大堆告訴你如何使用”IFS”的文章,但這並不是我想要的。

平時習慣了命令列的”man”/”help”方式,但是使用”man IFS”/”help IFS”/”info IFS”都無果啊。

後來想了想,IFS只是Shell裡面的一個環境變數,使用”man shell”或”man bash”應該能找到介紹。果不其然,”man bash”找到了關於IFS的介紹。

以下是在”man bash”結果中“Shell Variables”一節關於“IFS”的介紹:
image

在”Word Spliting”一節找到更多關於IFS的介紹。
image

命令列執行”man bash”顯示的內容太多,不方便閱讀,後來想到通過”man bash | grep IFS”過濾只與IFS相關的內容,無奈,這個操作沒有任何結果。
為什麼沒有任何結果呢?試著將”man bash”的結果輸出到文字檔案看看就知道了,裡面插入太多顯示格式字元,導致連一個完整的IFS字串都找不到。

因此想到的辦法是搜尋並下載bash的手冊(manual)看看,這樣通過bash手冊查詢IFS相關內容就方便多了。

Bash Reference Manual

2. Bash手冊中關於IFS的介紹

這一節看起來也挺廢話的,因為其中好多都是直接應用官方文件。我一直認為做技術同做學問一樣,都應該是嚴肅的。這裡借用脫襪子大神(torvalds)的一句很有名的話,“Talk is cheap, show me your code”,我想說得是“Talk is cheap, show me your evidence”。所以這一節從官方文件出發,介紹IFS為什麼會有這些特性。

This is Edition 4.4, last updated 7 September 2016, of The GNU Bash Reference Manual, for Bash, Version 4.4.

儘管Bash 4.4版本的手冊可能跟你執行的bash不匹配,但bash總檯上是很穩定的,各版本間差異不會太大,儘管是不同的版本,但也不會影響對IFS內容的理解。

在PDF版的bash手冊中搜索“IFS”, 總共在13個章節找到33個結果,其中最重要的地方有4個,包括:

  • 第71頁,第5.1節(“5.1 Bourne Shell Variables”)對shell變數IFS的定義
  • 第30頁,第3.5.7節(“3.5.7 Word Splitting”)對字元分割中使用IFS的介紹
  • 第20頁,第3.4.2節(“3.4.2 Special Parameters”)對特殊引數使用IFS的介紹
  • 第92頁,第6.7節(“6.7 Arrays”)對陣列引用中使用IFS的介紹

    以上列舉的4點並非按照先後順序,而是按照個人理解的重要程度排列


下面詳細介紹這4點。

2.1 變數IFS的定義

Bash手冊第71頁,第5.1節“Bourne Shell Variables”簡單說了什麼是IFS變數,如下: ![image](https://github.com/guyongqiangx/blog/blob/dev/shell/images/manual-5.1-ifs-definition.png?raw=true) 這裡提到IFS作為Shell的內建變數,是一個用於分割欄位的字元列表(注意,這裡是字元列表,說明其中可以包含不止一個字元)。

2.2 使用IFS進行單詞分割

Bash手冊的第30頁,第3.5.7節“Word Splitting”描述了基於IFS進行分割的細節,如下: ![image](https://github.com/guyongqiangx/blog/blob/dev/shell/images/manual-3.5.7-word-spliting.png?raw=true) 這裡說得比較詳細,是對IFS工作描述的重中之重,主要有以下幾點:
  • Shell把變數IFS內的每一個字元都當做是一個分割符(delimeter),用這些字元作為每一個欄位的結束符來進行分割。
  • 如果IFS沒有設定,或者IFS的值被設定為“ \t\n”(space, tab和 newline),那麼操作物件的開始和結束處的所有space, tab和newline序列都將被忽略,但是操作物件中間的space, tab和newline序列會作為界定符工作。
  • 如果IFS值不是預設值(例如程式中對IFS進行設定過),只有出現在IFS內的空白字元(可能是space, tab或newline中的一個或幾個)才會在單詞開始和結束處被忽略,這裡說的是單詞,而不是整個操作物件。
  • IFS內的非空白字元多個連續出現時,每個非空白字元會被當做單獨的分隔符看待,但是多個連續的空白字元會被當做一個分隔符看待。
  • 如果IFS為空(“null”),則不會進行單詞分割。

2.3 特殊引數$*中使用IFS

Bash手冊的第20頁,第3.4.2節“Special Parameters”介紹了特殊引數$*包含在雙引號中時,組合的新字串使用IFS的第1個字元進行連線,由於預設情況下IFS的第1個字元是空格,這就是為什麼我們看到"$*"的結果是使用空格進行分隔,如下:
image

《Linux命令列與Shell指令碼程式設計大全》第2版的第276頁是這樣描述$*和$@變數的:
$*和$@變數提供了對所有引數的快速訪問,這兩個都能夠在單個變數中儲存所有的命令列引數。
$*變數會將命令列上提供的所有引數當做單個單詞儲存。每個詞是指命令列上出現的每個值。基本上,$*變數會將這些都當做一個引數,而不是多個物件。

反過來說,$@變數會將命令列上提供的所有引數當做同一字串中的多個獨立的單詞。它允許你便利所有的值,將提供的每個引數分割開來。這通常通過for命令完成。

這裡特別說了IFS對變數$*的擴充套件的影響,主要有3點:

  • 當用雙引號(“double quotes”)來引用特殊變數$*時,會使用IFS變數的第1個字元來連線$*引數的每一個部分,即"$*"相當於"$1c$2c...",其中c是IFS變數的第一個字元。
  • 如果沒有設定IFS,則c為空格字元(space),實際上預設情況下IFS變數的第1個字元就是空格字元。
  • 如果IFS為空(null),則$*內各引數會直接連線在一起。

2.4 陣列引用中使用IFS

Bash手冊的第92頁,第6.7節“Arrays”介紹了IFS對陣列元素引用的影響,如下:
image

這裡強調引用陣列元素時,還可以使用*和@下標,哈哈,沒想到吧,跟命令列引數一樣。
通常情況下是使用帶下標的${name[subscript]}方式引用,但現在還可以使用*@來引用,如${name[*]}${name[@]}
如果${name[*]}被包含在雙引號內,則其將會用IFS的第1個字元連線陣列的各個元素進行擴充套件,跟上1節使用雙引號引用特殊引數$*一樣。

3. IFS使用的一些例子

關於IFS的重點:
IFS是shell的內建變數,IFS是一個字元列表,裡面的每一個字元都會用來作為分隔符進行單詞分割。

以下是使用IFS設定和分割的一些例子。

3.1 檢查IFS的預設值

mbp:~ rocky$ echo -n "$IFS" | hexdump
0000000 20 09 0a
0000003

十六進位制值0x20, 0x09和0x0a分別對應於空格(space), 水平製表符(tab)和換行符(newline)的值。

這裡給echo使用“-n”引數避免在echo時在行位新增換行符。如下是不帶“-n”的輸出:

mbp:~ rocky$ echo "$IFS" | hexdump
0000000 20 09 0a 0a
0000004

跟前面的結果比較,這裡最後多了一個字元0x0a。

3.2 IFS的修改和恢復

因為IFS是系統級變數,修改使用後記得要恢復原樣,否則後續程式就會出現一些奇奇怪怪的異常,別怪我沒告訴你啊,我自己曾經因為這個問題踩了個大坑。

這裡以一個處理帶空格的檔名來展示對IFS變數的修改。

操作目錄下有一個名為”a b c.txt”的檔案(字母a,b,c中間有兩個空格):

mbp:shell rocky$ ls -lh
total 24
-rw-r--r--  1 rocky  admin     0B  5  6 01:21 a b c.txt
drwxr-xr-x  3 rocky  admin   442B  5  6 02:03 images
-rw-r--r--  1 rocky  admin   115B  5  6 02:04 test1.sh
-rw-r--r--  1 rocky  admin   202B  5  6 02:02 test2.sh
-rw-r--r--  1 rocky  admin   228B  5  6 02:03 test3.sh
  • 預設情況,無法處理檔名中的空格
mbp:shell rocky$ cat test1.sh
#!/bin/bash

echo "1.Test with default IFS:"
echo -n "$IFS" | hexdump
for item in `ls`
do
    echo "file: $item"
done
mbp:shell rocky$
mbp:shell rocky$ bash test1.sh
1.Test with default IFS:
0000000 20 09 0a
0000003
file: a          <-- 錯誤的檔名
file: b          <-- 錯誤的檔名
file: c.txt      <-- 錯誤的檔名
file: images
file: test1.sh
file: test2.sh
file: test3.sh
  • 第1種方式:使用一箇中間變數儲存原始值,然後修改IFS,操作完成後再使用中間變數恢復IFS。
mbp:shell rocky$ cat test2.sh
#!/bin/bash

echo "2.Test with new IFS:"

# 先列印預設的IFS
echo -n "$IFS" | hexdump

# 使用變數IFS_SAVE臨時儲存IFS
IFS_SAVE=$IFS
IFS=$'\n'
echo -n "$IFS" | hexdump

for item in `ls`
do
    echo "file: $item"
done

# 從IFS_SAVE中恢復IFS
IFS=$IFS_SAVE
echo -n "$IFS" | hexdump

mbp:shell rocky$
mbp:shell rocky$ bash test2.sh
2.Test with new IFS:
0000000 20 09 0a  <-- 這裡是原來預設的IFS
0000003
0000000 0a        <-- 這裡是修改後的IFS,後面就使用'\n'來分割檔名
0000001
file: a b c.txt
file: images
file: test1.sh
file: test2.sh
file: test3.sh
0000000 20 09 0a  <-- 操作完後恢復預設的IFS
0000003
  • 第2種方式:使用local來宣告要使用的IFS變數來覆蓋全域性變數,由於local變數只在區域性有效,所以操作完不需要恢復IFS。
mbp:shell rocky$ cat test3.sh
#!/bin/bash

echo "3.Test with local IFS:"
function show_filename {
    # 使用local變數IFS來儲存臨時設定,僅在函式內有效
    local IFS=$'\n'
    echo -n "$IFS" | hexdump
    for item in `ls`
    do
        echo "file: $item"
    done
}

# 先列印預設的IFS
echo -n "$IFS" | hexdump

# 函式內會更改IFS並進行操作,但函式內並不會進行恢復
show_filename

# 退出函式後再列印IFS看看
echo -n "$IFS" | hexdump

mbp:shell rocky$
mbp:shell rocky$ bash test3.sh
3.Test with local IFS:
0000000 20 09 0a  <-- 這裡是原來預設的IFS
0000003
0000000 0a        <-- 這裡是函式內local變數設定的IFS
0000001
file: a b c.txt
file: images
file: test1.sh
file: test2.sh
file: test3.sh
0000000 20 09 0a  <-- 退出函式後IFS沒有被修改
0000003

3.3 IFS使用單個字元進行分割

IFS是一個字元列表,即使待分割字串中有碰巧有多個分隔符在一起,他大爺的還是按單個字元分割。

親,再次說明IFS是一個字元列表啊,我以前好長一段時間都不明白將IFS=$’ \t\n’這樣是什麼意思。這裡是說將space, tab, newline這3個字元作為分隔符。

假如有一個語句是這樣的:var=abc12345 IFS=12,你猜這裡的“IFS=12”是什麼意思?他丫的就是將字元“1”和“2”這兩個字元設定為分隔符啊,驗證如下:

mbp:shell rocky$ var=abc12345 IFS=12
mbp:shell rocky$ echo -n "$IFS" | hexdump -C
00000000  31 32                                             |12|
00000002
mbp:shell rocky$ for item in $var; \
> do \
>   echo "<$item>";\
> done;
<abc>
<>
<345>

這裡先定義了一個字串var=abc12345,然後設定IFS=12,通過後面的hexdump我們看到IFS的實際內容已經變成了“1”和“2”兩個字元。

然後用新的IFS來分割字串“abc12345”,顯然前面“abc”和後面的“345”都被分割為單獨的字串了。
從輸出可見,中間還有一個空字串,這個空串就是從1和2兩個字元中間分割得到的。

所以,即使多個分隔符挨在一起,仍然是按照單個分隔符進行分割,沒有你想的那麼智慧呢。

但有一種情況特殊,預設情況下IFS的值為空白分隔符” \t\n”(即space, tab和newline),按照手冊3.5.7節中的說法,會將挨在一起的多個空白分隔符看做一個分隔符。

mbp:shell rocky$ var=$'abc \n45'
mbp:shell rocky$ echo -n "$IFS" | hexdump
0000000 20 09 0a
0000003
mbp:shell rocky$ for item in $var; \
> do \
>   echo "<$item>"; \
> done;
<abc>
<45>

這裡字串var的內部有兩個分隔符(空格和換行符)挨在一起,但最後var被當做一個分割符進行分割得到了兩個子串。

空格符、製表符(\t)、換行符(\n)這三個空白符在 IFS 中會被特殊對待,Shell 會把它們按照任意順序任意數量組合成的字串作為分隔符,而不是單個字元作為分隔符。

前面的例子提到的都是字串的分割受IFS設定的影響,下面兩個例子講多個數據元素合併為一個時也受IFS設定的影響。

3.4 特殊引數$*受IFS影響

手冊的3.4.2節講引數$*被雙引號包含時,其結果受IFS第一個字元的影響。下面列舉一個例子來驗證下:

mbp:shell rocky$ cat test5.sh
#!/bin/bash

# 1. 使用預設的IFS

# 列印當前的IFS
echo -n "$IFS" | hexdump
# 以非雙引號的方式引用$*
echo \$*=$*
# 以雙引號的方式引用$*
echo "\"\$*\"=$*"

# 2. 修改IFS為'-'進行測試

# 修改IFS並打印出來
IFS=$'-'
echo -n "$IFS" | hexdump
# 以非雙引號的方式引用$*
echo \$*=$*
# 以雙引號的方式引用$*
echo "\"\$*\"=$*"
mbp:shell rocky$

# 這裡傳入1,2,3,4,5共計5個引數
mbp:shell rocky$ bash test5.sh 1 2 3 4 5
0000000 20 09 0a  <-- 預設的IFS值
0000003
$*=1 2 3 4 5      <-- 以非雙引號的方式($*)
"$*"=1 2 3 4 5    <-- 以雙引號的方式("$*")
0000000 2d        <-- 修改後的IFS
0000001
$*=1 2 3 4 5      <-- 以非雙引號的方式($*)
"$*"=1-2-3-4-5    <-- 以雙引號的方式("$*")

可見,當修改IFS以後,對使("*”)會影響到合成的結果。

3.5 陣列元素${array[*]}受IFS影響

mbp:shell rocky$ cat test6.sh
#!/bin/bash

# 定義陣列var,包含1,2,3,4,5共5個元素
var=(1 2 3 4 5)

# 1. 使用預設的IFS
echo -n "$IFS" | hexdump
# 以非雙引號的方式引用
echo \${var[*]}=${var[*]}
# 以雙引號的方式引用
echo "\"\${var[*]}\"=${var[*]}"

# 修改IFS
IFS=$'-'

# 使用需改的IFS
echo -n "$IFS" | hexdump
# 以非雙引號的方式引用
echo \${var[*]}=${var[*]}
# 以雙引號的方式引用
echo "\"\${var[*]}\"=${var[*]}"
mbp:shell rocky$
mbp:shell rocky$ bash test6.sh
0000000 20 09 0a       <-- 預設的IFS值
0000003
${var[*]}=1 2 3 4 5    <-- 以非雙引號的方式(${var[*]})
"${var[*]}"=1 2 3 4 5  <-- 以雙引號的方式("${var[*]}")
0000000 2d             <-- 修改後的IFS
0000001
${var[*]}=1 2 3 4 5    <-- 以非雙引號的方式(${var[*]})
"${var[*]}"=1-2-3-4-5  <-- 以雙引號的方式("${var[*]}")

3.6 建議以類似IFS=string’的方式來設定IFS

上面的各個例子中都是使用IFS=$'string'(例如:IFS=$' \t\n')的奇怪的方式來設定IFS,既然'\n'是常量,為什麼前面還要使用$符號呢?

這裡跟$'string'的特殊引用方式有關,詳細解釋參考Bash手冊的第3.1.2.4節“ANSI-C Quoting”,這一節提到$'string'的引用方式會被當做特別對待,使用這種方式的值會使用反斜槓轉義的字元。
對於IFS=$' \t\n'就包含了對“\t”和“\n”兩個轉義。

因此你能看到如下的兩種方式是有區別的:

# 使用"string"的方式,無法使反斜槓轉義後續字元
mbp:shell rocky$ IFS=" \t\n"
mbp:shell rocky$ echo -n "$IFS" | hexdump
0000000 20 5c 74 5c 6e
0000005

# 使用'string'的方式,無法使反斜槓轉義後續字元
mbp:shell rocky$ IFS=' \t\n'
mbp:shell rocky$ echo -n "$IFS" | hexdump
0000000 20 5c 74 5c 6e
0000005

# 使用$'string'的方式,成功使用反斜槓轉義
mbp:shell rocky$ IFS=$' \t\n'
mbp:shell rocky$ echo -n "$IFS" | hexdump
0000000 20 09 0a
0000003

從以上驗證的結果可見,只有第三種方式,IFS才成功包含了轉義字元,其結果為期望的space,tab和newline三個字元;二前面兩種方式都原樣包含了字串中的5個字元。

4. IFS的一些結論

以下是我對使用IFS的一些結論:

  • IFS本身是一個包含1個或多個字元的列表
  • 不確定IFS內容時使用“echo -n "$IFS" | hexdump”將其以十六進位制的方式打印出來看看
  • 多個IFS內的非空白分割字元出現在一起時,每個分割符單獨起作用;但如果IFS內的空白字元多個連續出現時,會將多個連續空白字元整體當做一個分隔符
  • IFS既可以用於單個元素分割,也和以用於多個元素組合為單個元素。對於使用*作為下標引用陣列類元素(包括特殊引數$*和陣列, 都是陣列類元素)時,雙引號包含的引用會將多個元素擴充套件組合生成單個元素,但這個新元素的內部是使用IFS的第1個字元進行連線。

5. 其它

  • 本文原創釋出於微信公眾號“洛奇看世界”,一個大齡2b碼農的世界。

image

  • 個人微訊號,新增請備註“微信公眾號”。

image

  • 關注微信公眾號“洛奇看世界”
    • 回覆關鍵詞“0506”,下載本文提到的Bash手冊和本文的PDF版本。
    • 回覆關鍵詞“Android電子書”,獲取超過150本Android相關的電子書和文件。電子書包含了Android開發相關的方方面面,好不好,你說了算。