强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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]="[email protected]"
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 扩展阅读