在上一章中,我們查看了 shell 怎樣操作字符串和數字的。目前我們所見到的數據類型在計算機科學圈里被 成為標量變量;也就是說,只能包含一個值的變量。
在本章中,我們將看看另一種數據結構叫做數組,數組能存放多個值。數組幾乎是所有編程語言的一個特性。 shell 也支持它們,盡管以一個相當有限的形式。即便如此,為解決編程問題,它們是非常有用的。
## 什么是數組?
數組是一次能存放多個數據的變量。數組的組織結構就像一張表。我們拿電子表格舉例。一張電子表格就像是一個 二維數組。它既有行也有列,并且電子表格中的一個單元格,可以通過單元格所在的行和列的地址定位它的位置。 數組行為也是如此。數組有單元格,被稱為元素,而且每個元素會包含數據。 使用一個稱為索引或下標的地址可以訪問一個單獨的數組元素。
大多數編程語言支持多維數組。一個電子表格就是一個多維數組的例子,它有兩個維度,寬度和高度。 許多語言支持任意維度的數組,雖然二維和三維數組可能是最常用的。
Bash 中的數組僅限制為單一維度。我們可以把它們看作是只有一列的電子表格。盡管有這種局限,但是有許多應用使用它們。 對數組的支持第一次出現在 bash 版本2中。原來的 Unix shell 程序,sh,根本就不支持數組。
## 創建一個數組
數組變量就像其它 bash 變量一樣命名,當被訪問的時候,它們會被自動地創建。這里是一個例子:
~~~
[me@linuxbox ~]$ a[1]=foo
[me@linuxbox ~]$ echo ${a[1]}
foo
~~~
這里我們看到一個賦值并訪問數組元素的例子。通過第一個命令,把數組 a 的元素1賦值為 “foo”。 第二個命令顯示存儲在元素1中的值。在第二個命令中使用花括號是必需的, 以便防止 shell 試圖對數組元素名執行路徑名展開操作。
也可以用 declare 命令創建一個數組:
~~~
[me@linuxbox ~]$ declare -a a
~~~
使用 -a 選項,declare 命令的這個例子創建了數組 a。
## 數組賦值
有兩種方式可以給數組賦值。單個值賦值使用以下語法:
~~~
name[subscript]=value
~~~
這里的 name 是數組的名字,subscript 是一個大于或等于零的整數(或算術表達式)。注意數組第一個元素的下標是0, 而不是1。數組元素的值可以是一個字符串或整數。
多個值賦值使用下面的語法:
~~~
name=(value1 value2 ...)
~~~
這里的 name 是數組的名字,value… 是要按照順序賦給數組的值,從元素0開始。例如,如果我們希望 把星期幾的英文簡寫賦值給數組 days,我們可以這樣做:
~~~
[me@linuxbox ~]$ days=(Sun Mon Tue Wed Thu Fri Sat)
~~~
還可以通過指定下標,把值賦給數組中的特定元素:
~~~
[me@linuxbox ~]$ days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu [5]=Fri [6]=Sat)
~~~
## 訪問數組元素
那么數組對什么有好處呢? 就像許多數據管理任務一樣,可以用電子表格程序來完成,許多編程任務則可以用數組完成。
讓我們考慮一個簡單的數據收集和展示的例子。我們將構建一個腳本,用來檢查一個特定目錄中文件的修改次數。 從這些數據中,我們的腳本將輸出一張表,顯示這些文件最后是在一天中的哪個小時被修改的。這樣一個腳本 可以被用來確定什么時段一個系統最活躍。這個腳本,稱為 hours,輸出這樣的結果:
~~~
[me@linuxbox ~]$ hours .
Hour Files Hour Files
---- ----- ---- ----
00 0 12 11
01 1 13 7
02 0 14 1
03 0 15 7
04 1 16 6
04 1 17 5
06 6 18 4
07 3 19 4
08 1 20 1
09 14 21 0
10 2 22 0
11 5 23 0
Total files = 80
~~~
當執行該 hours 程序時,指定當前目錄作為目標目錄。它打印出一張表顯示一天(0-23小時)每小時內, 有多少文件做了最后修改。程序代碼如下所示:
~~~
#!/bin/bash
# hours : script to count files by modification time
usage () {
echo "usage: $(basename $0) directory" >&2
}
# Check that argument is a directory
if [[ ! -d $1 ]]; then
usage
exit 1
fi
# Initialize array
for i in {0..23}; do hours[i]=0; done
# Collect data
for i in $(stat -c %y "$1"/* | cut -c 12-13); do
j=${i/#0}
((++hours[j]))
((++count))
done
# Display data
echo -e "Hour\tFiles\tHour\tFiles"
echo -e "----\t-----\t----\t-----"
for i in {0..11}; do
j=$((i + 12))
printf "%02d\t%d\t%02d\t%d\n" $i ${hours[i]} $j ${hours[j]}
done
printf "\nTotal files = %d\n" $count
~~~
這個腳本由一個函數(名為 usage),和一個分為四個區塊的主體組成。在第一部分,我們檢查是否有一個命令行參數, 且該參數為目錄。如果不是目錄,會顯示腳本使用信息并退出。
第二部分初始化一個名為 hours 的數組。給每一個數組元素賦值一個0。雖然沒有特殊需要在使用之前準備數組,但是 我們的腳本需要確保沒有元素是空值。注意這個循環構建方式很有趣。通過使用花括號展開({0..23}),我們能 很容易為 for 命令產生一系列的數據(words)。
接下來的一部分收集數據,對目錄中的每一個文件運行 stat 程序。我們使用 cut 命令從結果中抽取兩位數字的小時字段。 在循環里面,我們需要把小時字段開頭的零清除掉,因為 shell 將試圖(最終會失敗)把從 “00” 到 “09” 的數值解釋為八進制(見表35-1)。 下一步,我們以小時為數組索引,來增加其對應的數組元素的值。最后,我們增加一個計數器的值(count),記錄目錄中總共的文件數目。
腳本的最后一部分顯示數組中的內容。我們首先輸出兩行標題,然后進入一個循環產生兩欄輸出。最后,輸出總共的文件數目。
## 數組操作
有許多常見的數組操作。比方說刪除數組,確定數組大小,排序,等等。有許多腳本應用程序。
### 輸出整個數組的內容
下標 * 和 @ 可以被用來訪問數組中的每一個元素。與位置參數一樣,@ 表示法在兩者之中更有用處。 這里是一個演示:
~~~
[me@linuxbox ~]$ animals=("a dog" "a cat" "a fish")
[me@linuxbox ~]$ for i in ${animals[*]}; do echo $i; done
a
dog
a
cat
a
fish
[me@linuxbox ~]$ for i in ${animals[@]}; do echo $i; done
a
dog
a
cat
a
fish
[me@linuxbox ~]$ for i in "${animals[*]}"; do echo $i; done
a dog a cat a fish
[me@linuxbox ~]$ for i in "${animals[@]}"; do echo $i; done
a dog
a cat
a fish
~~~
我們創建了數組 animals,并把三個含有兩個字的字符串賦值給數組。然后我們執行四個循環看一下對數組內容進行分詞的效果。 表示法 ${animals[*]} 和 ${animals[@]}的行為是一致的直到它們被用引號引起來。
### 確定數組元素個數
使用參數展開,我們能夠確定數組元素的個數,與計算字符串長度的方式幾乎相同。這里是一個例子:
~~~
[me@linuxbox ~]$ a[100]=foo
[me@linuxbox ~]$ echo ${#a[@]} # number of array elements
1
[me@linuxbox ~]$ echo ${#a[100]} # length of element 100
3
~~~
我們創建了數組 a,并把字符串 “foo” 賦值給數組元素100。下一步,我們使用參數展開來檢查數組的長度,使用 @ 表示法。 最后,我們查看了包含字符串 “foo” 的數組元素 100 的長度。有趣的是,盡管我們把字符串賦值給數組元素100, bash 僅僅報告數組中有一個元素。這不同于一些其它語言的行為,數組中未使用的元素(元素0-99)會初始化為空值, 并把它們計入數組長度。
### 找到數組使用的下標
因為 bash 允許賦值的數組下標包含 “間隔”,有時候確定哪個元素真正存在是很有用的。為做到這一點, 可以使用以下形式的參數展開:
${!array[*]}
${!array[@]}
這里的 array 是一個數組變量的名字。和其它使用符號 * 和 @ 的展開一樣,用引號引起來的 @ 格式是最有用的, 因為它能展開成分離的詞。
~~~
[me@linuxbox ~]$ foo=([2]=a [4]=b [6]=c)
[me@linuxbox ~]$ for i in "${foo[@]}"; do echo $i; done
a
b
c
[me@linuxbox ~]$ for i in "${!foo[@]}"; do echo $i; done
2
4
6
~~~
### 在數組末尾添加元素
如果我們需要在數組末尾附加數據,那么知道數組中元素的個數是沒用的,因為通過 * 和 @ 表示法返回的數值不能 告訴我們使用的最大數組索引。幸運地是,shell 為我們提供了一種解決方案。通過使用 += 賦值運算符, 我們能夠自動地把值附加到數組末尾。這里,我們把三個值賦給數組 foo,然后附加另外三個。
~~~
[me@linuxbox~]$ foo=(a b c)
[me@linuxbox~]$ echo ${foo[@]}
a b c
[me@linuxbox~]$ foo+=(d e f)
[me@linuxbox~]$ echo ${foo[@]}
a b c d e f
~~~
### 數組排序
就像電子表格,經常有必要對一列數據進行排序。Shell 沒有這樣做的直接方法,但是通過一點兒代碼,并不難實現。
~~~
#!/bin/bash
# array-sort : Sort an array
a=(f e d c b a)
echo "Original array: ${a[@]}"
a_sorted=($(for i in "${a[@]}"; do echo $i; done | sort))
echo "Sorted array: ${a_sorted[@]}"
~~~
當執行之后,腳本產生這樣的結果:
~~~
[me@linuxbox ~]$ array-sort
Original array: f e d c b a
Sorted array:
a b c d e f
~~~
腳本運行成功,通過使用一個復雜的命令替換把原來的數組(a)中的內容復制到第二個數組(a_sorted)中。 通過修改管道線的設計,這個基本技巧可以用來對數組執行各種各樣的操作。
### 刪除數組
刪除一個數組,使用 unset 命令:
~~~
[me@linuxbox ~]$ foo=(a b c d e f)
[me@linuxbox ~]$ echo ${foo[@]}
a b c d e f
[me@linuxbox ~]$ unset foo
[me@linuxbox ~]$ echo ${foo[@]}
[me@linuxbox ~]$
~~~
也可以使用 unset 命令刪除單個的數組元素:
~~~
[me@linuxbox~]$ foo=(a b c d e f)
[me@linuxbox~]$ echo ${foo[@]}
a b c d e f
[me@linuxbox~]$ unset 'foo[2]'
[me@linuxbox~]$ echo ${foo[@]}
a b d e f
~~~
在這個例子中,我們刪除了數組中的第三個元素,下標為2。記住,數組下標開始于0,而不是1!也要注意數組元素必須 用引號引起來為的是防止 shell 執行路徑名展開操作。
有趣地是,給一個數組賦空值不會清空數組內容:
~~~
[me@linuxbox ~]$ foo=(a b c d e f)
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo[@]}
b c d e f
~~~
任何引用一個不帶下標的數組變量,則指的是數組元素0:
~~~
[me@linuxbox~]$ foo=(a b c d e f)
[me@linuxbox~]$ echo ${foo[@]}
a b c d e f
[me@linuxbox~]$ foo=A
[me@linuxbox~]$ echo ${foo[@]}
A b c d e f
~~~
## 關聯數組
現在最新的 bash 版本支持關聯數組了。關聯數組使用字符串而不是整數作為數組索引。 這種功能給出了一種有趣的新方法來管理數據。例如,我們可以創建一個叫做 “colors” 的數組,并用顏色名字作為索引。
~~~
declare -A colors
colors["red"]="#ff0000"
colors["green"]="#00ff00"
colors["blue"]="#0000ff"
~~~
不同于整數索引的數組,僅僅引用它們就能創建數組,關聯數組必須用帶有 -A 選項的 declare 命令創建。
訪問關聯數組元素的方式幾乎與整數索引數組相同:
~~~
echo ${colors["blue"]}
~~~
在下一章中,我們將看一個腳本,很好地利用關聯數組,生產出了一個有意思的報告。
## 總結
如果我們在 bash 手冊頁中搜索單詞 “array”的話,我們能找到許多 bash 在哪里會使用數組變量的實例。其中大部分相當晦澀難懂, 但是它們可能在一些特殊場合提供臨時的工具。事實上,在 shell 編程中,整套數組規則利用率相當低,很大程度上歸咎于這樣的事實, 傳統 Unix shell 程序(比如說 sh)缺乏對數組的支持。這樣缺乏人氣是不幸的,因為數組廣泛應用于其它編程語言, 并為解決各種各樣的編程問題,提供了一個強大的工具。
數組和循環有一種天然的姻親關系,它們經常被一起使用。該
~~~
for ((expr; expr; expr))
~~~
形式的循環尤其適合計算數組下標。
## 拓展閱讀
* Wikipedia 上面有兩篇關于在本章提到的數據結構的文章:
[http://en.wikipedia.org/wiki/Scalar_(computing)](http://en.wikipedia.org/wiki/Scalar_(computing))
[http://en.wikipedia.org/wiki/Associative_array](http://en.wikipedia.org/wiki/Associative_array)
- 第一章:引言
- 第二章:什么是shell
- 第三章:文件系統中跳轉
- 第四章:研究操作系統
- 第五章:操作文件和目錄
- 第六章:使用命令
- 第七章:重定向
- 第八章:從shell眼中看世界
- 第九章:鍵盤高級操作技巧
- 第十章:權限
- 第十一章:進程
- 第十二章:shell環境
- 第十三章:VI簡介
- 第十四章:自定制shell提示符
- 第十五章:軟件包管理
- 第十六章:存儲媒介
- 第十七章:網絡系統
- 第十八章:查找文件
- 第十九章:歸檔和備份
- 第二十章:正則表達式
- 第二十一章:文本處理
- 第二十二章:格式化輸出
- 第二十三章:打印
- 第二十四章:編譯程序
- 第二十五章:編寫第一個shell腳本
- 第二十六章:啟動一個項目
- 第二十七章:自頂向下設計
- 第二十八章:流程控制 if分支結構
- 第二十九章:讀取鍵盤輸入
- 第三十章:流程控制 while/until 循環
- 第三十一章:疑難排解
- 第三十二章:流程控制 case分支
- 第三十三章:位置參數
- 第三十四章:流程控制 for循環
- 第三十五章:字符串和數字
- 第三十六章:數組
- 第三十七章:奇珍異寶