在上一章中,我们研究了shell如何操纵字符串和数字。我们迄今为止研究的数据类型在计算机科学界被称为 scalar variables(标量变量);也就是说,它们是包含单个值的变量。
在本章中,我们将看看另一种称为数组的数据结构,它包含多个值。数组是几乎所有编程语言的一个特征。shell也支持它们,尽管以相当有限的方式。尽管如此,它们对于解决某些类型的编程问题非常有用。
第三十五章:数组什么是数组创建一个数组将值分配给数组访问数组元素数组操作输出数组的整个内容确定数组元素的数量查找数组使用的下标使用 read -a
分配数组元素将元素添加到数组的末尾将文件读入数组数组切片排序数组删除数组关联数组使用关联数组模拟多维总结
数组是同时包含多个值的变量。数组像表(table)一样组织。让我们以电子表格(spreadsheet)为例。电子表格就像一个二维数组。它既有行也有列,电子表格中的单个单元格可以根据其行和列地址进行定位。一个数组的行为方式相同。数组有单元格,称为元素(elements),每个元素包含数据。单个数组元素使用一个名为索引(index)或子项(subscript)的地址进行访问。
大多数编程语言都支持多维数组。电子表格是具有两个维度,宽度和高度的多维数组的一个例子。许多语言支持任意数量的维数组,尽管二维和三维数组可能是最常用的。
bash中的数组仅限于一个维度。我们可以把它们想象成一个单列的电子表格。即使有这种限制,也有很多应用。数组支持首次出现在bash版本2。最初的 Unix shell 程序 sh 完全不支持数组。
数组变量与其他 bash 变量一样命名,并在访问它们时自动创建。这里有一个例子:
xxxxxxxxxx
[me@linuxbox ~]$ a[1]=foo
[me@linuxbox ~]$ echo ${a[1]}
foo
在这里,我们看到一个数组元素的分配和访问的例子。在第一个命令中,数组 a
的元素1被赋予值“foo”。第二个命令显示元素1的存储值。在第二个命令中使用括号是必要的,以防止shell尝试在数组元素的名称上扩展路径名。
也可以使用 declare
命令创建数组。
xxxxxxxxxx
[me@linuxbox ~]$ declare -a a
使用 -a
选项,这个 declare
示例创建数组 a。
值可以以两种方式之一分配。可以使用以下语法分配单个值:
name[subscript]=value
其中 name 是数组的名称, subscript 是大于或等于零的整数(或算术表达式)。请注意,数组的第一个元素是子项零,而不是一。value 是指定给数组元素的字符串或整数。
可以使用以下语法分配多个值:
name=(value1 value2 ...)
其中 name 是数组的名称,value 占位符是按顺序分配给数组元素的值,从元素 0 开始。例如,如果我们想将一周中的缩写天分配给数组天,我们可以这样做:
xxxxxxxxxx
[me@linuxbox ~]$ days=(Sun Mon Tue Wed Thu Fri Sat)
也可以通过指定每个值的子项来为特定元素分配值。
xxxxxxxxxx
[me@linuxbox ~]$ days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu [5]=Fri [6]=Sat)
数组有什么好处?正如许多数据管理任务可以用电子表格程序执行一样,许多编程任务也可以用数组执行。
让我们来看一个简单的数据收集和演示示例。我们将构建一个脚本来检查指定目录中文件的修改时间。从这些数据中,我们的脚本将输出一个表格,显示文件最后一次修改的时间。这样的脚本可以用来确定系统何时最活跃。这个命名为小时的脚本产生了这样的结果:
xxxxxxxxxx
[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
05 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),最近修改了多少个文件。产生此代码如下:
xxxxxxxxxx
# hours: script to count files by modification time
usage () {
echo "usage: ${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..23})
,我们可以轻松地为 for
命令生成一系列单词。
下一节通过在目录中的每个文件上运行 stat
程序来收集数据。我们使用 cut
从结果中提取两位数小时。在循环中,我们需要从小时字段中删除前导零,因为shell将尝试(最终失败)将值00到09解释为八进制数字(见表34-2)。接下来,我们增加数组元素对应于一天中的时间的值。最后,我们增加一个计数器(count
)来跟踪目录中文件的总数。
脚本的最后一部分显示数组的内容。我们首先输出几行头,然后输入一个循环,产生四列输出。最后,我们输出文件的最终计数。
有许多常见的数组操作。删除数组,确定它们的大小,排序等等,在脚本中有很多应用。
字符串 *
和 @
可用于访问数组中的每个元素。与位置参数一样,@
符号是两者中最有用的。这里有一个示范:
xxxxxxxxxx
[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
,并分配三个两字字符串。然后我们执行 for
循环来查看单词分割对数组内容的影响。符号 ${animals[*]}
和 ${animals[@]}
的行为是相同的,直到它们被引用。 *
符号导致包含数组内容的单个单词,而 @
符号导致三个两个单词的字符串,匹配数组的“真实”内容。
使用参数扩展,我们可以以与查找字符串长度相同的方式确定数组中的元素数量。下面是一个例子:
xxxxxxxxxx
[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。接下来,我们使用参数扩展来检查数组的长度,使用 @
符号。最后,我们看看元素100的长度,其中包含字符串foo
。有趣的是,虽然我们将字符串分配给元素100,但bash只报告数组中的一个元素。这与其他一些语言的行为不同,其中数组中未使用的元素(元素0-99)将以空值初始化并计数。在 bash 中,数组元素只有在它们被分配一个值时才存在,而不管它们的子项如何。
subscripts —— 下标,订阅,子项
由于bash允许数组在分配子项时包含“空白”(gaps),因此有时确定哪些元素实际存在是有用的。这可以通过使用以下形式进行参数扩展来完成:
${!array[*]} ${!array[@]}
array 是数组变量的名称。像其他使用 *
和 @
的扩展一样,用引号包围的 @
形式是最有用的,因为它可以扩展为单独的单词。
xxxxxxxxxx
[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
read -a
分配数组元素内置命令 read
有一个选项(-a
)将单词放置在一个索引数组中,而不是像我们之前所做的一系列变量。这里有一个例子:
xxxxxxxxxx
[me@linuxbox ~]$ declare -a foo
[me@linuxbox ~]$ read -a foo <<< "0th 1st 2nd 3rd 4th"
[me@linuxbox ~]$ for i in "${foo[@]}"; do echo "$i"; done
0th
1st
2nd
3rd
4th
如果我们需要将值添加到数组的末尾,知道数组中的元素数量是没有帮助的,因为*和@符号返回的值不会告诉我们使用的最大数组索引。幸运的是,shell为我们提供了一个解决方案。通过使用 +=
分配运算符,我们可以自动将值添加到数组的末尾。在这里,我们为数组 foo
分配三个值,然后再添加三个。
xxxxxxxxxx
[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
最近的bash版本包括一个名为 mapfile
的新内置文件,可以直接将标准输入读取到索引数组中。它的语法看起来像这样:
mapfile -options array
mapfile
命令有一些有趣的选项,其中一些在下表中显示。
选项 | 说明 |
---|---|
-d delim | 使用 delim 结束行,而不是新行。 |
-n count | 只读取 count 行。 |
-O origin | 开始在索引 origin 而不是索引0分配数组元素。 |
-s count | 跳过文件开头的 count 行。 |
-t | 修剪每行的尾部分界符。 |
为了在动作中演示 mapfile
,我们将创建一个短脚本,生成随机的四字密码短语。这些作为传统密码的替代品很有用,因为它们很长,但很容易记住(只要你知道如何拼写;-)。
xxxxxxxxxx
# array-mapfile - demonstrate mapfile builtin
DICTIONARY=/usr/share/dict/words
WORDLIST=~/wordlist.txt
declare -a words
# create filtered word list
grep -v \' < "$DICTIONARY" \
| grep -v "[[:upper:]]" \
| shuf > "$WORDLIST"
# read WORDLIST into array
mapfile -t -n 32767 words < "$WORDLIST"
# create four word passphrase
while [[ -z $REPLY ]]; do
echo "${words[$RANDOM]}" \
"${words[$RANDOM]}" \
"${words[$RANDOM]}" \
"${words[$RANDOM]}"
echo
read -r -p "Enter to continue, q to quit > "
echo
done
此脚本使用过滤的 /usr/share/dict/words 文件来删除引号和大写字母。我们使用 shuf
命令来混合单词列表以获得一个很好的随机顺序。
接下来,我们将文件中的第一个32767个单词加载到单词数组中。为什么是32767?这是因为我们将使用 RANDOM
变量从数组中选择随机元素,每次引用 RANDOM
变量时,它都会返回一个介于 0 和 32767 之间的随机整数。
xxxxxxxxxx
[me@linuxbox ~]$ ./array-mapfile
conversions slumbers appendages metastasizing
Enter to continue, q to quit >
kettles rhinestones unused demagnetizes
Enter to continue, q to quit >
wear conveys characterizing extrusion
Enter to continue, q to quit > q
在这里,我们看到了输出。每次按下 Enter ,脚本都会显示四个随机单词。
有一种形式的参数扩展,我们可以用来从数组中提取一组称为 slice 的相邻元素。这种扩展会从原始数组的所需切片中产生数组元素,如下所示。
xxxxxxxxxx
[me@linuxbox ~]$ arr=(0th 1st 2nd 3rd 4th)
[me@linuxbox ~]$ echo "${arr[@]:2:3}"
2nd 3rd 4th
在这个例子中,我们创建一个包含五个元素的数组。
接下来,我们从索引二开始的数组中提取三个元素。通过指定负(negative)索引值,我们从数组的末尾而不是开始计数。在下面的例子中,我们提取数组的最后两个元素。注意减号前所需的前置空格。
xxxxxxxxxx
[me@linuxbox ~]$ echo "${arr[@]: -2:2}"
3rd 4th
我们还可以轻松创建包含切片元素的数组。
xxxxxxxxxx
[me@linuxbox ~]$ arr2=("${arr[@]:2:3}")
[me@linuxbox ~]$ echo "${arr2[@]}"
2nd 3rd 4th
在这里,我们创建了一个数组 arr2
,并用 arr
的三个元素填充它。
与电子表格一样,通常需要对数据列中的值进行排序。shell没有直接的方法来做到这一点,但用一点编码并不困难。
xxxxxxxxxx
# 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[@]}"
当执行时,脚本会产生以下内容:
xxxxxxxxxx
[me@linuxbox ~]$ array-sort
Original array: f e d c b a
Sorted array: a b c d e f
该脚本通过复制原始数组(a
)的内容到第二个数组(a_sorted
)进行操作,并进行复杂的命令替换。这种基本技术可以通过改变管道的设计在阵列上执行多种操作。
使用 unset
命令删除数组。
xxxxxxxxxx
[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
也可以用来删除单个数组元素。
xxxxxxxxxx
[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。记住,数组以子项零开始,而不是一!另请注意,必须引用数组元素以防止 shell 执行路径名扩展。
有趣的是,将空值分配给数组并不会清空其内容。
xxxxxxxxxx
[me@linuxbox ~]$ foo=(a b c d e f)
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo[@]}
b c d e f
任何对没有子项的数组变量的引用都是指数组中的元素零。
xxxxxxxxxx
[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 4.0 及更高版本支持关联数组(associative arrays)。关联数组使用字符串而不是整数作为数组索引,从而创建类似于字典或哈希表的关键值对。这种能力允许有趣的新方法来管理数据。例如,我们可以创建一个名为 colors
的数组,并使用颜色名称作为索引。
xxxxxxxxxx
declare -A colors
colors["red"]="#ff0000"
colors["green"]="#00ff00"
colors["blue"]="#0000ff"
与仅通过引用它们创建的整数索引数组不同,关联数组必须使用 declare
命令的 -A
选项明确创建。关联数组元素的访问方式与整数索引数组大致相同。
xxxxxxxxxx
echo ${colors["blue"]}
我们可以利用关联数组的键值对特性来执行查找任务。关联数组允许我们直接访问所需的数据,而不必像索引数组那样按顺序搜索。在下面的程序中,我们将 /usr/bin 目录中所有文件的名称及其各自的大小读取到一个数组中,然后根据用户输入执行查找。
xxxxxxxxxx
# array-lookup - demonstrate lookup using associative array
declare -A cmds
# fill array with commands and file sizes
cd /usr/bin || exit 1
echo "Loading commands..."
for i in ./*; do
cmds["$i"]=$(stat -c "%s" "$i")
done
echo "${#cmds[@]} commands loaded"
# perform lookup
while true; do
read -r -p "Enter command (empty to quit) -> "
[[ -z $REPLY ]] && break
if [[ -x $REPLY ]]; then
echo "$REPLY" "${cmds[./$REPLY]}" "bytes"
else
echo "No such command '$REPLY'."
fi
done
运行这个程序会得到下面的效果:
xxxxxxxxxx
[me@linuxbox ~]$ ./array-lookup
Loading commands...
2329 commands loaded
Enter command (empty to quit) -> ls
ls 138216 bytes
Enter command (empty to quit) -> cp
cp 141832 bytes
Enter command (empty to quit) -> mv
mv 137752 bytes
Enter command (empty to quit) -> rm
rm 59912 bytes
Enter command (empty to quit) ->
[me@linuxbox ~]$
在下一章中,我们将介绍一个脚本,该脚本充分利用关联数组来生成有趣的报告。
虽然bash确实只直接支持一维数组,但通过使用关联数组并创建看起来像多维数组地址的索引字符串来“伪造”多维数组并不难。这里有一个例子:
xxxxxxxxxx
# array-multi - simulate a multi-dimensional array
declare -A multi_array
# Load array with a sequence of numbers
counter=1
for row in {1..10}; do
for col in {1..5}; do
address="$row, $col"
multi_array["$address"]=$counter
((counter++))
done
done
# Output array contents
for row in {1..10}; do
for col in {1..5}; do
address="$row, $col"
echo -ne "${multi_array["$address"]}" "\t"
done
echo
done
运行结果如下:
xxxxxxxxxx
[me@linuxbox ~]$ ./array-multi
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25
26 27 28 29 30
31 32 33 34 35
36 37 38 39 40
41 42 43 44 45
46 47 48 49 50
如果我们在bash手册页中搜索单词 array
,我们会发现bash使用数组变量的许多实例。其中大多数都相当模糊,但在某些特殊情况下,它们可能偶尔会提供实用性。事实上,在shell编程中,数组的整个主题都没有得到充分利用,这主要是因为传统的Unix shell程序(如sh)缺乏对数组的任何支持。这种不受欢迎的情况是不幸的,因为数组在其他编程语言中被广泛使用,并为解决多种编程问题提供了强大的工具。
数组和循环具有天然的亲和力,经常一起使用。以下形式的循环特别适合计算索引数组下标:
for ((expr; expr; expr))