在Shell脚本的世界里,数组就像是一个装满东西的盒子,你可以把很多相关的数据放进去,然后方便地拿出来使用。无论你是初学者还是有一定经验的Shell脚本编写者,掌握数组的使用都会让你的脚本更加灵活和强大。下面,我们就来详细了解一下Shell数组的基础知识和实用技巧。
Shell数组的基础知识 #
- 数组是什么?
数组是存储一系列值(类型可以不同)的数据结构。在Shell中,数组可以包含文本(字符串)或数字,而且每个元素都有自己的位置编号,称为索引。索引通常从0开始。
- 索引数组 vs 关联数组
索引数组:这是最传统的数组形式,每个元素按照顺序排列,并且通过数字索引来访问。
关联数组:也称为字典或哈希表,这种类型的数组允许使用非数字的键(如字符串)来标识元素,提供了更灵活的方式存储和检索信息。
创建和初始化数组 #
- 直接赋值创建数组
# 创建一个简单的索引数组
fruits=("apple" "banana" "orange")
# 创建一个有1-5共5个数字的索引数组
numbers=($(seq 5))
- 使用declare声明数组
可以使用declare -a
命令创建一个索引数组,这也是直接赋值创建数组的默认行为:
declare -a colors=("red" "green" "blue")
关联数组让我们能够以更加直观的方式组织数据,使用declare -A
创建关联数组。例如,我们可以把学生的姓名作为键,成绩作为值:
declare -A grades
grades=(["Zhang San"]=90 ["Li Si"]=85 ["Wang Wu"]=92)
访问数组元素 #
- 访问单个元素
要查看索引数组中的某一项,只需要知道它的索引即可:
echo ${fruits[0]}
# 输出: apple
使用键可以访问关联数组中的项,键区分大小写:
echo ${grades["Zhang San"]}
# 输出:90
- 获取数组长度
想知道数组中有多少项?可以使用${#array[@]}
语法。例如:
echo ${#fruits[@]}
# 输出: 3
这里的${#fruits[@]}
会给出数组中元素的数量。此方法也可以获取到关联数组的元素数量。
- 遍历数组
如果想依次处理索引数组中每个元素,可以使用循环:
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
# 每行输出一个元素
这段代码会遍历fruits数组,并打印出每种水果的名字。
对于关联数组,Bash提供了一种特殊的方式来迭代关联数组的键,即使用${!array[@]}
语法。查看关联数组所有的键:
echo "${!grades[@]}"
# 输出:Zhang San Li Si Wang Wu
使用for key in "${!array[@]}"
语法可以遍历每一个键,再通过键获取值,就可以遍历关联数组。
修改数组 #
- 添加新元素
向索引数组追加新成员非常简单:
fruits+=("grape")
# 在最后添加一个新元素
向关联数组添加新成员也非常简单:
grades+=(["Zhao Liu"]=90)
# 添加的位置不确定,取决于键的哈希值
- 更新现有元素
也可以更改已有元素的值:
fruits[1]="peach"
# 将第二个元素改为peach
同样,使用键可以定位并修改元素值:
grades["Zhao Liu"]=91
# 将Zhao Liu的成绩改为91
你可以试试使用grades["Zhao Liu"]+=1
是否符合预期?如果希望将成绩加1应该怎样做?
- 删除元素
若要移除某个特定元素:
unset fruits[2]
# 删除第三个元素
删除元素后,数组后面的元素不会前移,${fruits[2]}
为空。对于整个数组的清理,只需unset fruits
即可。
unset命令同样适用于关联数组。
数组操作高级特性 #
- 对数组进行排序
虽然Shell本身没有内置的数组排序功能,但我们可以通过其他命令实现:
sorted_fruits=($(printf "%s\n" "${fruits[@]}" | sort))
printf会将数组中的元素按行输出,sort命令可以对这些行进行排序。如果是数值数组,使用sort -n
可以按数值进行排序,或使用sort -nr
按数值倒序排序。
- 搜索特定元素
查找数组中是否存在某项:
if [[ " ${fruits[@]} " =~ " apple " ]]; then
echo "apple在数组中"
fi
=~
是一个用于正则表达式匹配的操作符,用来测试字符串是否与给定的正则表达式模式相匹配。
在使用正则表达式匹配时,需要注意字符串和正则表达式两端的空格,以确保进行完整的匹配。如果数组元素本身包含空格,则直接使用正则表达式可能会导致部分匹配的问题,从而产生不准确的结果。为了避免这种情况,建议采用遍历数组的方式逐个检查元素,以确保匹配的准确性。
- 字符串转数组
对于一串由分隔符分开的文字,可以用这个方法转换成数组:
string="apple,banana,orange"
IFS=',' read -r -a array <<< "$string"
这里IFS变量定义了分隔符,read命令将字符串分割并存入数组array中。
- 数组切片
有时候,你可能只想处理数组中的一部分元素。这时,可以使用数组切片的功能。例如,要获取fruits数组中的前两个元素,可以这样做:
echo ${fruits[@]:0:2}
# 输出:apple banana
这里的0:2
表示从索引0开始,获取2个元素。
- 数组连接与合并
可以轻松地将两个或多个数组连接起来。例如:
vegetables=("carrot" "potato" "beetroot")
all_foods=("${fruits[@]}" "${vegetables[@]}")
现在,all_foods数组包含了fruits和vegetables两个数组的所有元素。
实用示例 #
示例1:统计文件夹内不同扩展名文件的数量
#!/bin/bash
declare -A ext_counts
for file in *; do
ext="${file##*.}"
[[ "$file" == "$ext" ]] && ext="no_ext"
((ext_counts[$ext]++))
done
for ext in "${!ext_counts[@]}"; do
echo "$ext: ${ext_counts[$ext]}"
done
你可以试试用本文介绍的方法,将上述结果按键(扩展名)排序后输出。
示例2:从用户输入生成菜单
#!/bin/bash
options=("Option 1" "Option 2" "Exit")
PS3="Choose an option: "
select opt in "${options[@]}"; do
case $opt in
"Option 1")
echo "You chose Option 1"
;;
"Option 2")
echo "You chose Option 2"
;;
"Exit")
break
;;
*) echo "Invalid option $REPLY"
;;
esac
done
常见问题和注意事项 #
空格敏感:在赋值时,确保等号两边没有空格。
索引越界:尝试访问不存在的索引不会出现错误,会返回空值,所以应注意索引的范围和值的有效性,尤其是删除索引元素后。
使用括号:数组排序后的结果赋值给数组,应避免遗漏最外层的半角括号。
使用引号:在进行数组遍历、连接等操作时,获取所有数组元素的表达式应当用双引号括起来,以确保包含空格的元素值能够保持其完整性。如果不使用双引号包裹这些表达式,含有空格的元素将被视为多个独立的、由空格分隔的元素,从而可能导致意外的行为或错误的结果。
明确定义关联数组:使用直接赋值的方式会创建索引数组,虽然值是关联数组的形式,此时的赋值结果不符合预期,应使用declare -A
进行明确定义。
Bash中很多地方的双引号可以省略,例如以下语句都能获取预期的结果:
declare -a colors=(red green blue)
printf "%s\n" ${colors[@]} # 每行打印一个颜色
declare -A grades=([John]=90 [Mike]=85)
printf "%s\n" ${!grades[@]} # 每行打印一个姓名
但是,当执行以下语句后,上述printf的结果不再符合预期,空格分隔的元素值或者名称被分行输出:
colors+=("soft red")
grades+=(["Zhang San"]=90)
使用双引号可以避免出现上述问题:
printf "%s\n" "${colors[@]}"
printf "%s\n" "${!grades[@]}"
因此,不建议省略双引号,平时养成使用双引号的习惯很重要。
总结 #
Shell数组为我们的脚本编写提供了极大的便利,使得管理复杂的数据集变得更加容易。无论是简单的任务还是复杂的自动化流程,掌握数组的使用都是提高效率的关键一步。