在上一章中,我們查看了 shell 怎樣操作字符串和數(shù)字的。目前我們所見到的數(shù)據(jù)類型在計(jì)算機(jī)科學(xué)圈里被 成為標(biāo)量變量;也就是說,只能包含一個(gè)值的變量。
在本章中,我們將看看另一種數(shù)據(jù)結(jié)構(gòu)叫做數(shù)組,數(shù)組能存放多個(gè)值。數(shù)組幾乎是所有編程語言的一個(gè)特性。 shell 也支持它們,盡管以一個(gè)相當(dāng)有限的形式。即便如此,為解決編程問題,它們是非常有用的。
數(shù)組是一次能存放多個(gè)數(shù)據(jù)的變量。數(shù)組的組織結(jié)構(gòu)就像一張表。我們拿電子表格舉例。一張電子表格就像是一個(gè) 二維數(shù)組。它既有行也有列,并且電子表格中的一個(gè)單元格,可以通過單元格所在的行和列的地址定位它的位置。 數(shù)組行為也是如此。數(shù)組有單元格,被稱為元素,而且每個(gè)元素會(huì)包含數(shù)據(jù)。 使用一個(gè)稱為索引或下標(biāo)的地址可以訪問一個(gè)單獨(dú)的數(shù)組元素。
大多數(shù)編程語言支持多維數(shù)組。一個(gè)電子表格就是一個(gè)多維數(shù)組的例子,它有兩個(gè)維度,寬度和高度。 許多語言支持任意維度的數(shù)組,雖然二維和三維數(shù)組可能是最常用的。
Bash 中的數(shù)組僅限制為單一維度。我們可以把它們看作是只有一列的電子表格。盡管有這種局限,但是有許多應(yīng)用使用它們。 對(duì)數(shù)組的支持第一次出現(xiàn)在 bash 版本2中。原來的 Unix shell 程序,sh,根本就不支持?jǐn)?shù)組。
數(shù)組變量就像其它 bash 變量一樣命名,當(dāng)被訪問的時(shí)候,它們會(huì)被自動(dòng)地創(chuàng)建。這里是一個(gè)例子:
[me@linuxbox ~]$ a[1]=foo
[me@linuxbox ~]$ echo ${a[1]}
foo
這里我們看到一個(gè)賦值并訪問數(shù)組元素的例子。通過第一個(gè)命令,把數(shù)組 a 的元素1賦值為 “foo”。 第二個(gè)命令顯示存儲(chǔ)在元素1中的值。在第二個(gè)命令中使用花括號(hào)是必需的, 以便防止 shell 試圖對(duì)數(shù)組元素名執(zhí)行路徑名展開操作。
也可以用 declare 命令創(chuàng)建一個(gè)數(shù)組:
[me@linuxbox ~]$ declare -a a
使用 -a 選項(xiàng),declare 命令的這個(gè)例子創(chuàng)建了數(shù)組 a。
有兩種方式可以給數(shù)組賦值。單個(gè)值賦值使用以下語法:
name[subscript]=value
這里的 name 是數(shù)組的名字,subscript 是一個(gè)大于或等于零的整數(shù)(或算術(shù)表達(dá)式)。注意數(shù)組第一個(gè)元素的下標(biāo)是0, 而不是1。數(shù)組元素的值可以是一個(gè)字符串或整數(shù)。
多個(gè)值賦值使用下面的語法:
name=(value1 value2 ...)
這里的 name 是數(shù)組的名字,value... 是要按照順序賦給數(shù)組的值,從元素0開始。例如,如果我們希望 把星期幾的英文簡(jiǎn)寫賦值給數(shù)組 days,我們可以這樣做:
[me@linuxbox ~]$ days=(Sun Mon Tue Wed Thu Fri Sat)
還可以通過指定下標(biāo),把值賦給數(shù)組中的特定元素:
[me@linuxbox ~]$ days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu [5]=Fri [6]=Sat)
那么數(shù)組對(duì)什么有好處呢? 就像許多數(shù)據(jù)管理任務(wù)一樣,可以用電子表格程序來完成,許多編程任務(wù)則可以用數(shù)組完成。
讓我們考慮一個(gè)簡(jiǎn)單的數(shù)據(jù)收集和展示的例子。我們將構(gòu)建一個(gè)腳本,用來檢查一個(gè)特定目錄中文件的修改次數(shù)。 從這些數(shù)據(jù)中,我們的腳本將輸出一張表,顯示這些文件最后是在一天中的哪個(gè)小時(shí)被修改的。這樣一個(gè)腳本 可以被用來確定什么時(shí)段一個(gè)系統(tǒng)最活躍。這個(gè)腳本,稱為 hours,輸出這樣的結(jié)果:
[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
當(dāng)執(zhí)行該 hours 程序時(shí),指定當(dāng)前目錄作為目標(biāo)目錄。它打印出一張表顯示一天(0-23小時(shí))每小時(shí)內(nèi), 有多少文件做了最后修改。程序代碼如下所示:
#!/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
這個(gè)腳本由一個(gè)函數(shù)(名為 usage),和一個(gè)分為四個(gè)區(qū)塊的主體組成。在第一部分,我們檢查是否有一個(gè)命令行參數(shù), 且該參數(shù)為目錄。如果不是目錄,會(huì)顯示腳本使用信息并退出。
第二部分初始化一個(gè)名為 hours 的數(shù)組。給每一個(gè)數(shù)組元素賦值一個(gè)0。雖然沒有特殊需要在使用之前準(zhǔn)備數(shù)組,但是 我們的腳本需要確保沒有元素是空值。注意這個(gè)循環(huán)構(gòu)建方式很有趣。通過使用花括號(hào)展開({0..23}),我們能 很容易為 for 命令產(chǎn)生一系列的數(shù)據(jù)(words)。
接下來的一部分收集數(shù)據(jù),對(duì)目錄中的每一個(gè)文件運(yùn)行 stat 程序。我們使用 cut 命令從結(jié)果中抽取兩位數(shù)字的小時(shí)字段。 在循環(huán)里面,我們需要把小時(shí)字段開頭的零清除掉,因?yàn)?shell 將試圖(最終會(huì)失敗)把從 “00” 到 “09” 的數(shù)值解釋為八進(jìn)制(見表35-1)。 下一步,我們以小時(shí)為數(shù)組索引,來增加其對(duì)應(yīng)的數(shù)組元素的值。最后,我們?cè)黾右粋€(gè)計(jì)數(shù)器的值(count),記錄目錄中總共的文件數(shù)目。
腳本的最后一部分顯示數(shù)組中的內(nèi)容。我們首先輸出兩行標(biāo)題,然后進(jìn)入一個(gè)循環(huán)產(chǎn)生兩欄輸出。最后,輸出總共的文件數(shù)目。
有許多常見的數(shù)組操作。比方說刪除數(shù)組,確定數(shù)組大小,排序,等等。有許多腳本應(yīng)用程序。
下標(biāo) * 和 @ 可以被用來訪問數(shù)組中的每一個(gè)元素。與位置參數(shù)一樣,@ 表示法在兩者之中更有用處。 這里是一個(gè)演示:
[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
我們創(chuàng)建了數(shù)組 animals,并把三個(gè)含有兩個(gè)字的字符串賦值給數(shù)組。然后我們執(zhí)行四個(gè)循環(huán)看一下對(duì)數(shù)組內(nèi)容進(jìn)行分詞的效果。 表示法 ${animals[*]} 和 ${animals[@]}的行為是一致的直到它們被用引號(hào)引起來。
使用參數(shù)展開,我們能夠確定數(shù)組元素的個(gè)數(shù),與計(jì)算字符串長(zhǎng)度的方式幾乎相同。這里是一個(gè)例子:
[me@linuxbox ~]$ a[100]=foo
[me@linuxbox ~]$ echo ${#a[@]} # number of array elements
1
[me@linuxbox ~]$ echo ${#a[100]} # length of element 100
3
我們創(chuàng)建了數(shù)組 a,并把字符串 “foo” 賦值給數(shù)組元素100。下一步,我們使用參數(shù)展開來檢查數(shù)組的長(zhǎng)度,使用 @ 表示法。 最后,我們查看了包含字符串 “foo” 的數(shù)組元素 100 的長(zhǎng)度。有趣的是,盡管我們把字符串賦值給數(shù)組元素100, bash 僅僅報(bào)告數(shù)組中有一個(gè)元素。這不同于一些其它語言的行為,數(shù)組中未使用的元素(元素0-99)會(huì)初始化為空值, 并把它們計(jì)入數(shù)組長(zhǎng)度。
因?yàn)?bash 允許賦值的數(shù)組下標(biāo)包含 “間隔”,有時(shí)候確定哪個(gè)元素真正存在是很有用的。為做到這一點(diǎn), 可以使用以下形式的參數(shù)展開:
${!array[*]}
${!array[@]}
這里的 array 是一個(gè)數(shù)組變量的名字。和其它使用符號(hào) * 和 @ 的展開一樣,用引號(hào)引起來的 @ 格式是最有用的, 因?yàn)樗苷归_成分離的詞。
[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
如果我們需要在數(shù)組末尾附加數(shù)據(jù),那么知道數(shù)組中元素的個(gè)數(shù)是沒用的,因?yàn)橥ㄟ^ * 和 @ 表示法返回的數(shù)值不能 告訴我們使用的最大數(shù)組索引。幸運(yùn)地是,shell 為我們提供了一種解決方案。通過使用 += 賦值運(yùn)算符, 我們能夠自動(dòng)地把值附加到數(shù)組末尾。這里,我們把三個(gè)值賦給數(shù)組 foo,然后附加另外三個(gè)。
[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
就像電子表格,經(jīng)常有必要對(duì)一列數(shù)據(jù)進(jìn)行排序。Shell 沒有這樣做的直接方法,但是通過一點(diǎn)兒代碼,并不難實(shí)現(xiàn)。
#!/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[@]}"
當(dāng)執(zhí)行之后,腳本產(chǎn)生這樣的結(jié)果:
[me@linuxbox ~]$ array-sort
Original array: f e d c b a
Sorted array:
a b c d e f
腳本運(yùn)行成功,通過使用一個(gè)復(fù)雜的命令替換把原來的數(shù)組(a)中的內(nèi)容復(fù)制到第二個(gè)數(shù)組(a_sorted)中。 通過修改管道線的設(shè)計(jì),這個(gè)基本技巧可以用來對(duì)數(shù)組執(zhí)行各種各樣的操作。
刪除一個(gè)數(shù)組,使用 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 命令刪除單個(gè)的數(shù)組元素:
[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
在這個(gè)例子中,我們刪除了數(shù)組中的第三個(gè)元素,下標(biāo)為2。記住,數(shù)組下標(biāo)開始于0,而不是1!也要注意數(shù)組元素必須 用引號(hào)引起來為的是防止 shell 執(zhí)行路徑名展開操作。
有趣地是,給一個(gè)數(shù)組賦空值不會(huì)清空數(shù)組內(nèi)容:
[me@linuxbox ~]$ foo=(a b c d e f)
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo[@]}
b c d e f
任何引用一個(gè)不帶下標(biāo)的數(shù)組變量,則指的是數(shù)組元素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
現(xiàn)在最新的 bash 版本支持關(guān)聯(lián)數(shù)組了。關(guān)聯(lián)數(shù)組使用字符串而不是整數(shù)作為數(shù)組索引。 這種功能給出了一種有趣的新方法來管理數(shù)據(jù)。例如,我們可以創(chuàng)建一個(gè)叫做 “colors” 的數(shù)組,并用顏色名字作為索引。
declare -A colors
colors["red"]="#ff0000"
colors["green"]="#00ff00"
colors["blue"]="#0000ff"
不同于整數(shù)索引的數(shù)組,僅僅引用它們就能創(chuàng)建數(shù)組,關(guān)聯(lián)數(shù)組必須用帶有 -A 選項(xiàng)的 declare 命令創(chuàng)建。
訪問關(guān)聯(lián)數(shù)組元素的方式幾乎與整數(shù)索引數(shù)組相同:
echo ${colors["blue"]}
在下一章中,我們將看一個(gè)腳本,很好地利用關(guān)聯(lián)數(shù)組,生產(chǎn)出了一個(gè)有意思的報(bào)告。
如果我們?cè)?bash 手冊(cè)頁中搜索單詞 “array”的話,我們能找到許多 bash 在哪里會(huì)使用數(shù)組變量的實(shí)例。其中大部分相當(dāng)晦澀難懂, 但是它們可能在一些特殊場(chǎng)合提供臨時(shí)的工具。事實(shí)上,在 shell 編程中,整套數(shù)組規(guī)則利用率相當(dāng)?shù)?,很大程度上歸咎于這樣的事實(shí), 傳統(tǒng) Unix shell 程序(比如說 sh)缺乏對(duì)數(shù)組的支持。這樣缺乏人氣是不幸的,因?yàn)閿?shù)組廣泛應(yīng)用于其它編程語言, 并為解決各種各樣的編程問題,提供了一個(gè)強(qiáng)大的工具。
數(shù)組和循環(huán)有一種天然的姻親關(guān)系,它們經(jīng)常被一起使用。該
for ((expr; expr; expr))
形式的循環(huán)尤其適合計(jì)算數(shù)組下標(biāo)。
Wikipedia 上面有兩篇關(guān)于在本章提到的數(shù)據(jù)結(jié)構(gòu)的文章: