Bash 脚本编写教程 / 09 - 数组
09 - 数组
9.1 索引数组
声明与赋值
# 方式一:一次性赋值
fruits=("apple" "banana" "cherry" "date")
# 方式二:逐个赋值
colors=()
colors[0]="red"
colors[1]="green"
colors[2]="blue"
colors[10]="purple" # 支持不连续索引
# 方式三:用 declare
declare -a numbers=(10 20 30 40 50)
# 方式四:从命令输出填充
mapfile -t lines < /etc/hosts # Bash 4.0+
readarray -t lines < /etc/hosts # readarray 是 mapfile 的别名
# 方式五:用 read -a
read -ra words <<< "one two three four five"
# 方式六:从通配符展开
shopt -s nullglob # 无匹配时返回空
files=(/tmp/*.log)
shopt -u nullglob
读取与操作
arr=("a" "b" "c" "d" "e")
# 读取单个元素
echo "${arr[0]}" # a(第一个元素)
echo "${arr[2]}" # c(第三个元素)
echo "${arr[-1]}" # e(最后一个,Bash 4.3+)
echo "${arr[-2]}" # d(倒数第二个)
# 读取所有元素
echo "${arr[@]}" # a b c d e
echo "${arr[*]}" # a b c d e
# 数组长度
echo "${#arr[@]}" # 5
# 单个元素长度
echo "${#arr[0]}" # 1
# 所有索引
echo "${!arr[@]}" # 0 1 2 3 4
# 追加元素
arr+=("f")
arr+=("g" "h" "i") # 一次追加多个
# 删除元素
unset 'arr[2]' # 删除索引为2的元素(留下空洞)
# 注意:这不会重新编号索引
# 替换元素
arr[0]="A"
# 数组切片
echo "${arr[@]:1:3}" # 从索引1开始取3个
# 数组复制
new_arr=("${arr[@]}")
9.2 关联数组(Bash 4.0+)
# 必须先用 declare -A 声明
declare -A user
# 赋值
user[name]="张三"
user[age]=30
user[email]="zhangsan@example.com"
user[role]="工程师"
# 也可以一次性声明(Bash 不支持一次性赋值关联数组)
declare -A config=(
[host]="localhost"
[port]="5432"
[database]="myapp"
[user]="admin"
)
# 读取
echo "${config[host]}" # localhost
echo "${config[database]}" # myapp
# 所有键
echo "${!config[@]}" # host port database user
# 所有值
echo "${config[@]}"
# 长度
echo "${#config[@]}" # 4
# 判断键是否存在
if [[ -v config[host] ]]; then
echo "host 已配置"
fi
# 删除键值对
unset 'config[user]'
9.3 遍历数组
arr=("apple" "banana" "cherry")
# 遍历元素(推荐 "${arr[@]}")
for item in "${arr[@]}"; do
echo "水果: $item"
done
# 遍历索引和元素
for i in "${!arr[@]}"; do
echo "[$i] ${arr[$i]}"
done
# C 风格遍历
for ((i = 0; i < ${#arr[@]}; i++)); do
echo "[$i] ${arr[$i]}"
done
# 关联数组遍历
declare -A config=(
[host]="localhost"
[port]="5432"
)
for key in "${!config[@]}"; do
echo "$key = ${config[$key]}"
done
# 排序遍历
for key in $(echo "${!config[@]}" | tr ' ' '\n' | sort); do
echo "$key = ${config[$key]}"
done
9.4 数组传递
# ⚠️ Bash 没有真正的"传递数组"机制
# 常见方法:
# 方法一:通过全局数组(不推荐但简单)
my_array=("a" "b" "c")
print_array() {
for item in "${my_array[@]}"; do
echo "$item"
done
}
print_array
# 方法二:通过 nameref(Bash 4.3+,推荐)
process_array() {
local -n ref=$1 # 接收数组名
for item in "${ref[@]}"; do
echo "处理: $item"
done
}
data=("hello" "world")
process_array data
# 方法三:通过 "$@" 传递
join_array() {
local delimiter="$1"
shift
local result=""
for item in "$@"; do
[[ -n "$result" ]] && result+="$delimiter"
result+="$item"
done
echo "$result"
}
arr=("a" "b" "c")
result=$(join_array ", " "${arr[@]}")
echo "$result" # a, b, c
# 方法四:通过序列化(复杂场景)
serialize_array() {
local -n ref=$1
printf '%s\n' "${ref[@]}"
}
deserialize_array() {
local -n ref=$1
ref=()
while IFS= read -r line; do
ref+=("$line")
done
}
# 使用
data=("hello world" "foo bar" "baz")
serialized=$(serialize_array data)
declare -a restored
deserialize_array restored <<< "$serialized"
echo "${restored[@]}" # hello world foo bar baz
9.5 模拟多维数组
# Bash 没有原生多维数组,但可以模拟
# 方法一:嵌套索引(行*列数+列)
rows=3
cols=4
declare -a matrix
# 设置 matrix[row][col]
set_cell() {
local r=$1 c=$2 val=$3
matrix[$((r * cols + c))]="$val"
}
# 获取 matrix[row][col]
get_cell() {
local r=$1 c=$2
echo "${matrix[$((r * cols + c))]}"
}
# 填充矩阵
for ((r = 0; r < rows; r++)); do
for ((c = 0; c < cols; c++)); do
set_cell $r $c "($r,$c)"
done
done
# 打印矩阵
for ((r = 0; r < rows; r++)); do
for ((c = 0; c < cols; c++)); do
printf "%-8s" "$(get_cell $r $c)"
done
echo
done
# 方法二:关联数组 + 复合键
declare -A matrix2
matrix2["0,0"]="a"
matrix2["0,1"]="b"
matrix2["1,0"]="c"
matrix2["1,1"]="d"
echo "${matrix2["0,0"]}" # a
echo "${matrix2["1,0"]}" # c
9.6 常用数组操作
# 数组去重
dedup() {
local -A seen
local -a result=()
for item in "$@"; do
if [[ ! -v "seen[$item]" ]]; then
seen[$item]=1
result+=("$item")
fi
done
echo "${result[@]}"
}
arr=("a" "b" "a" "c" "b" "d")
echo "$(dedup "${arr[@]}")" # a b c d
# 数组排序
sort_array() {
printf '%s\n' "$@" | sort
}
sorted=$(sort_array "${arr[@]}")
echo "$sorted"
# 数组包含检查
contains() {
local needle="$1"
shift
local item
for item in "$@"; do
[[ "$item" == "$needle" ]] && return 0
done
return 1
}
if contains "c" "${arr[@]}"; then
echo "找到 'c'"
fi
# 数组交集
intersection() {
local -A set_a
local -a result=()
local item
for item in "${@:1:$1}"; do
set_a[$item]=1
done
for item in "${@:$((1+$1))}"; do
[[ -v "set_a[$item]" ]] && result+=("$item")
done
echo "${result[@]}"
}
a=("1" "2" "3" "4")
b=("3" "4" "5" "6")
echo "交集: $(intersection ${#a[@]} "${a[@]}" "${b[@]}")" # 3 4
# 数组差集
difference() {
local -A set_b
local -a result=()
local item
for item in "${@:$((1+$1))}"; do
set_b[$item]=1
done
for item in "${@:1:$1}"; do
[[ ! -v "set_b[$item]" ]] && result+=("$item")
done
echo "${result[@]}"
}
echo "差集: $(difference ${#a[@]} "${a[@]}" "${b[@]}")" # 1 2
# 数组最大值/最小值
max_array() {
local max="$1"
shift
for item in "$@"; do
((item > max)) && max=$item
done
echo "$max"
}
min_array() {
local min="$1"
shift
for item in "$@"; do
((item < min)) && min=$item
done
echo "$min"
}
nums=(3 1 4 1 5 9 2 6 5 3)
echo "最大值: $(max_array "${nums[@]}")" # 9
echo "最小值: $(min_array "${nums[@]}")" # 1
9.7 业务场景:配置文件解析器
#!/bin/bash
# config_parser.sh —— 简易 INI 配置文件解析器
set -euo pipefail
declare -A CONFIG
declare -A CONFIG_SECTION
# 解析 INI 配置文件
parse_ini() {
local file="$1"
local section="global"
CONFIG=()
while IFS= read -r line || [[ -n "$line" ]]; do
# 去除首尾空白
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
# 跳过空行和注释
[[ -z "$line" || "$line" == \#* || "$line" == \;* ]] && continue
# Section
if [[ "$line" =~ ^\[([^\]]+)\]$ ]]; then
section="${BASH_REMATCH[1]}"
continue
fi
# Key=Value
if [[ "$line" =~ ^([^=]+)=(.*)$ ]]; then
local key="${BASH_REMATCH[1]}"
local value="${BASH_REMATCH[2]}"
# 去除首尾空白
key="${key#"${key%%[![:space:]]*}"}"
key="${key%"${key##*[![:space:]]}"}"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
CONFIG["${section}.${key}"]="$value"
fi
done < "$file"
}
# 获取配置值
config_get() {
local key="$1"
local default="${2:-}"
echo "${CONFIG[$key]:-$default}"
}
# 创建示例配置文件
cat > /tmp/app.ini << 'EOF'
[server]
host = 0.0.0.0
port = 8080
workers = 4
[database]
host = localhost
port = 5432
name = myapp
user = admin
password = secret123
[logging]
level = info
file = /var/log/app.log
EOF
# 解析并使用
parse_ini /tmp/app.ini
echo "服务器配置:"
echo " 主机: $(config_get "server.host")"
echo " 端口: $(config_get "server.port")"
echo " 进程: $(config_get "server.workers")"
echo ""
echo "数据库配置:"
echo " 主机: $(config_get "database.host")"
echo " 端口: $(config_get "database.port")"
echo " 数据库: $(config_get "database.name")"
echo ""
echo "日志配置:"
echo " 级别: $(config_get "logging.level" "warn")"
echo " 文件: $(config_get "logging.file" "/dev/null")"
# 遍历所有配置
echo ""
echo "所有配置项:"
for key in $(echo "${!CONFIG[@]}" | tr ' ' '\n' | sort); do
printf " %-30s = %s\n" "$key" "${CONFIG[$key]}"
done
9.8 注意事项
| 陷阱 | 说明 | 解决方案 |
|---|
| 关联数组未声明 | declare -A 是必须的 | 否则退化为索引数组 |
| 空元素丢失 | ${arr[@]} 跳过空元素 | 使用 "${arr[@]+"${arr[@]}"}" |
| unset 留空洞 | unset arr[2] 不重编号 | 重建数组 |
| 数组复制必须引号 | new=(${arr[@]}) 会分词 | new=("${arr[@]}") |
| 传递数组需 nameref | 无法直接传递数组 | local -n ref=$1 |
9.9 扩展阅读