跳至正文
老丹的足迹 —— 代码写给机器,游记写给自己,感悟写给时间
老丹的足迹 老丹的足迹
老丹的足迹 老丹的足迹
  • 首页
  • 示例页面
  • 首页
  • 示例页面
老丹的足迹 老丹的足迹
老丹的足迹 老丹的足迹
  • 首页
  • 示例页面
  • 首页
  • 示例页面

Bash 数组完全指南:从设计思想到实战应用

引言

在 Bash 脚本编程中,数组是一种看似简单却蕴含深意的数据结构。许多初学者被 ${arr[@]} 和 ${arr[*]} 的语法困扰,对稀疏数组的概念感到困惑,更不理解为什么关联数组需要提前声明。

事实上,这些“怪异”的设计背后,有一套清晰自洽的设计哲学。理解这套思想,你将从“死记硬背语法”转变为“自然推导用法”,真正掌握 Bash 数组的精髓。

本文将沿着“设计思想 → 基础概念 → 高级特性 → 实战应用”的路径,系统性地剖析 Bash 数组。

第一部分:设计哲学——理解 Bash 数组为什么这样设计

1.1 核心原则:一切源于“词分割”

Bash 首先是一个命令行解释器,它的工作流程是:

  1. 读取一行命令
  2. 进行各种展开(变量、命令、通配符等)
  3. 将结果按空格/制表符/换行符分割成独立的“词”
  4. 执行这些“词”

设计洞察: 数组的本质就是“词的容器”。Bash 不是为了实现数据结构而设计数组,而是为了方便地管理命令行分割后产生的多个词。

1.2 核心矛盾:独立性 vs 整体性

设计者面临一个根本性的矛盾:

  • 作为容器:数组应该把每个元素视为独立的实体
  • 作为字符串:数组有时需要作为一个整体(比如传给 echo)

解决方案:用两种不同的语法来表示这两种需求——

  • "${arr[@]}":将数组展开为多个独立的词
  • "${arr[*]}":将数组展开为一个带分隔符的字符串

理解了这一矛盾,你就永远不会再混淆 @ 和 *。

1.3 逻辑模型:两列表格与稀疏性

Bash 数组的逻辑模型是一张两列的表格:

索引值
0apple
1banana
5cherry

设计洞察: 这是“索引→值”的映射模型,而不是传统语言中“连续内存块”的序列模型。

映射模型的特性:

  • 索引不要求连续 → 支持稀疏数组
  • 删除索引 1 只删除映射,索引 2 不会自动变成 1
  • “长度”只统计有映射的元素,不是最大索引+1

1.4 语法一致性:符号复用

Bash 的设计者非常注重语法的一致性。许多数组操作直接复用了现有符号的含义:

符号普通变量的含义数组的含义
${#var}字符串长度元素个数
${var:1:3}字符串切片数组切片
var+=(x)字符串追加元素追加
!var间接引用获取索引列表

设计洞察: 这种复用不是巧合,而是一种认知减负策略——你已经知道这些符号的含义,只需要理解它们在数组上的自然延伸。

1.5 从索引到关联:逻辑泛化

索引数组:索引类型 = 整数
    ↓ 泛化
关联数组:索引类型 = 字符串

设计洞察: 关联数组不是“另一种东西”,而是索引数组在数据类型上的泛化。这就是为什么它们可以共享几乎完全相同的语法,唯一的区别是关联数组需要 declare -A 声明(告诉 Bash:“索引将不是整数,请做好准备”)。

1.6 设计思想总结

设计原则具体体现对实践的启示
服务于词分割@ 和 * 的区别遍历用 "${arr[@]}"
映射模型支持稀疏数组不要假定索引连续
语法一致性复用 # : +=举一反三学习
逻辑泛化关联数组复用语法两种数组用法相同

第二部分:基础概念与操作

2.1 数组的类型

Bash 支持两种数组:

索引数组(Indexed Array):使用整数索引,从 0 开始,支持稀疏存储。这是默认类型。

关联数组(Associative Array):使用字符串键,需要 Bash 4.0+ 并用 declare -A 声明。

2.2 创建数组

索引数组的创建方式:

# 方式一:直接赋值多个元素(最常用)
colors=(red green blue yellow)

# 方式二:逐个索引赋值
colors[0]=red
colors[1]=green

# 方式三:创建稀疏数组(索引不连续)
fruits=([0]=apple [2]=orange [5]=banana)

# 方式四:通过命令输出创建
files=($(ls *.txt))

关联数组的创建:

# 必须先声明
declare -A user

# 方式一:一次性赋值
user=([name]="Alice" [age]="25" [city]="Beijing")

# 方式二:逐个赋值
user[name]="Alice"
user[age]=25

设计意图: 关联数组需要显式声明,因为 Bash 默认将 var[string] 理解为字符串连接(这是历史兼容性原因)。declare -A 明确告诉 Bash:“这是一个映射,不要做字符串处理”。

2.3 访问数组元素

核心语法:${array[index]}

fruits=(apple banana cherry)

echo ${fruits[0]}    # 输出: apple
echo ${fruits[2]}    # 输出: cherry

获取所有元素的两种方式:

# 方式一:[@] — 每个元素作为独立词(推荐)
for fruit in "${fruits[@]}"; do
    echo "I like $fruit"
done

# 方式二:[*] — 所有元素合并为一个词
echo "${fruits[*]}"   # 输出: apple banana cherry
场景推荐用法原因
遍历元素"${arr[@]}"保持元素独立性
传递参数"${arr[@]}"每个元素成为独立参数
打印所有元素"${arr[*]}"作为单个字符串更易读
字符串拼接"${arr[*]}"与 IFS 配合灵活

设计意图: @ 和 * 的差异正是为了解决 1.2 节提到的“独立性 vs 整体性”矛盾。@ 服务于“容器”需求,* 服务于“字符串”需求。

2.4 获取数组信息

colors=(red green blue)

# 元素个数
echo ${#colors[@]}     # 输出: 3
echo ${#colors[*]}     # 输出: 3

# 稀疏数组的长度
sparse=([0]=a [2]=c)
echo ${#sparse[@]}     # 输出: 2(只计算有值的元素)

# 获取所有索引
echo ${!colors[@]}     # 输出: 0 1 2

# 稀疏数组的索引
echo ${!sparse[@]}     # 输出: 0 2

设计意图: # 统一表示“数量/长度”,! 表示“获取元数据(索引)而不是值”。这种符号复用降低了记忆负担。

2.5 获取单个元素的长度

name=("John Doe")
echo ${#name[0]}       # 输出: 8(包括空格)

2.6 数组切片

语法:${array[@]:start:length}

numbers=(0 1 2 3 4 5 6 7 8 9)

echo ${numbers[@]:3:4}    # 从索引3开始取4个: 3 4 5 6
echo ${numbers[@]:5}      # 从索引5到末尾: 5 6 7 8 9
echo ${numbers[@]: -3}    # 最后3个元素: 7 8 9(注意空格)

设计意图: 切片语法与字符串切片 ${var:1:3} 完全一致。这是“语法一致性”原则的体现——学过字符串切片的人可以立即理解数组切片。

2.7 添加和修改元素

arr=(a b c)

# 修改现有元素
arr[1]=B                  # 数组变为: a B c

# 追加新元素(方式一:+= 操作符,推荐)
arr+=(d e f)              # 数组变为: a B c d e f

# 追加新元素(方式二:计算当前长度)
arr[${#arr[@]}]=g         # 追加单个元素

# 追加新元素(方式三:重构数组)
arr=("${arr[@]}" h)       # 性能较差,不推荐频繁使用

设计意图: += 操作符在普通变量上表示字符串追加(str+="more"),在数组上自然延伸为“元素追加”。这种一致性让学习者可以迁移已有知识。

2.8 删除元素和数组

arr=(a b c d)

# 删除单个元素(留下空洞)
unset arr[2]              # 删除索引2的元素
echo ${arr[@]}            # 输出: a b d
echo ${!arr[@]}           # 输出: 0 1 3(索引2已消失)

# 删除整个数组
unset arr

设计意图: 删除索引不会导致后续索引前移,这证实了“映射模型”而非“序列模型”。删除只是移除了一个映射关系。

2.9 合并数组

arr1=(a b c)
arr2=(d e f)

merged=("${arr1[@]}" "${arr2[@]}")
echo ${merged[@]}         # 输出: a b c d e f

设计意图: 数组合并本质上就是“用 @ 展开后放在一个新数组里”。这再次证明 @ 的设计目的。

第三部分:遍历与循环

3.1 遍历值(推荐方式)

fruits=(apple banana cherry)

for fruit in "${fruits[@]}"; do
    echo "$fruit"
done

为什么必须加引号? 回忆 1.1 节的原则——Shell 会进行词分割。如果某个元素包含空格(如 “red apple”),不加引号会被分割成两个词。

3.2 遍历索引

for index in "${!fruits[@]}"; do
    echo "fruits[$index] = ${fruits[$index]}"
done

使用场景: 当你需要同时知道索引和值时,或者需要按索引顺序修改原数组时。

3.3 C 风格循环

for ((i=0; i<${#fruits[@]}; i++)); do
    echo "Element $i: ${fruits[i]}"
done

使用场景: 需要索引变量进行复杂计算时(如步长不为1、反向遍历)。

第四部分:高级操作

4.1 数组复制

original=(a b c d)

# 完全复制
copy=("${original[@]}")

# 验证独立性
original[0]=X
echo ${copy[0]}           # 输出: a(未受影响)

4.2 搜索元素

fruits=(apple banana cherry date)

# 方法一:模式匹配(简单)
if [[ " ${fruits[@]} " == *" banana "* ]]; then
    echo "Found banana"
fi

# 方法二:查找索引
search="cherry"
for i in "${!fruits[@]}"; do
    if [[ "${fruits[i]}" == "$search" ]]; then
        echo "Found $search at index $i"
        break
    fi
done

4.3 排序

numbers=(5 2 8 1 9)
fruits=(banana apple cherry date)

# 数值排序
sorted_numbers=($(printf '%s\n' "${numbers[@]}" | sort -n))

# 字符串排序
sorted_fruits=($(printf '%s\n' "${fruits[@]}" | sort))

设计意图: Bash 没有内置排序函数,因为 Unix 哲学是“做好一件事,然后与其他工具组合”。sort 命令已经完美解决了排序问题,不需要重复实现。

4.4 去重

items=(apple banana apple cherry banana)

unique=($(printf '%s\n' "${items[@]}" | sort -u))
echo ${unique[@]}         # 输出: apple banana cherry

4.5 数组与字符串转换

字符串 → 数组:

# 按空格分割
str="apple banana cherry"
arr=($str)                # 简单但有陷阱(见注意事项)

# 按自定义分隔符分割(安全)
str="apple,banana,cherry"
IFS=',' read -ra arr <<< "$str"

数组 → 字符串:

arr=(apple banana cherry)

# 使用 IFS 的第一个字符连接
str="${arr[*]}"           # 默认空格: "apple banana cherry"

# 自定义分隔符
IFS=','; str="${arr[*]}"  # "apple,banana,cherry"
IFS='|'; str="${arr[*]}"  # "apple|banana|cherry"

设计意图: ${arr[*]} 与 IFS 的结合,完美解决了“数组转字符串”的需求。你甚至可以用这个特性生成 CSV 行。

第五部分:关联数组(Bash 4.0+)

5.1 逻辑泛化

关联数组是索引数组在索引类型上的泛化:用字符串代替整数作为键。

# 声明和初始化
declare -A employee
employee=(
    [name]="John Smith"
    [id]="1001"
    [department]="Engineering"
)

# 访问(语法完全相同)
echo ${employee[name]}        # John Smith

# 添加/修改
employee[salary]=75000
employee[department]="Sales"

5.2 获取元数据

# 获取所有键(类似索引数组的 ${!arr[@]})
echo ${!employee[@]}          # name id department salary

# 获取所有值
echo ${employee[@]}           # John Smith 1001 Sales 75000

# 元素个数
echo ${#employee[@]}          # 4

5.3 遍历关联数组

for key in "${!employee[@]}"; do
    echo "$key: ${employee[$key]}"
done

设计意图: 关联数组完全重用了索引数组的语法——${!map[@]} 获取的是“键列表”而不是“索引列表”。这种语法一致性是逻辑泛化的自然结果。

5.4 检查键是否存在(Bash 4.2+)

if [[ -v employee[name] ]]; then
    echo "Name exists"
fi

5.5 删除键值对

unset employee[salary]        # 删除 salary 键

第六部分:常见陷阱与最佳实践

6.1 陷阱:引号至关重要

arr=("a b" c d)

# ❌ 错误:没有引号会错误分割
for i in ${arr[@]}; do
    echo $i    # 输出四行: a, b, c, d
done

# ✅ 正确:使用引号保持元素完整性
for i in "${arr[@]}"; do
    echo $i    # 输出三行: a b, c, d
done

核心原则: 当你想要保持数组元素的完整性时,永远使用 "${arr[@]}"。

6.2 陷阱:数组索引不连续

sparse=([0]=a [5]=b [10]=c)

# 错误假设:长度 = 最大索引 + 1
echo ${#sparse[@]}    # 3,而不是 11

# 错误假设:循环 0..(length-1) 可覆盖所有元素
for ((i=0; i<${#sparse[@]}; i++)); do
    echo ${sparse[i]} # 只输出 a,然后输出两行空
done

# 正确方式:遍历索引
for idx in "${!sparse[@]}"; do
    echo "sparse[$idx] = ${sparse[idx]}"
done

6.3 陷阱:未声明关联数组

# ❌ 错误:未声明就使用
map[key]=value        # Bash 将其解释为字符串连接,不会报错但行为错误

# ✅ 正确
declare -A map
map[key]=value        # 正常工作

6.4 陷阱:导出数组到子进程

Bash 不能直接导出数组到子进程。解决方案:

# 方法一:转换为字符串导出
export MY_ARRAY="a b c"

# 方法二:分别导出每个元素
for i in "${!arr[@]}"; do
    export "arr_$i=${arr[i]}"
done

6.5 最佳实践汇总

场景推荐做法原因
遍历数组for i in "${arr[@]}"; do保持元素完整性
获取长度${#arr[@]}语义清晰
追加元素arr+=(new)高效、简洁
复制数组copy=("${original[@]}")创建独立副本
检查元素[[ " ${arr[@]} " == *" $item "* ]]简单直接
数组转字符串IFS=','; str="${arr[*]}"灵活控制分隔符

第七部分:实战示例

7.1 批量文件处理

# 收集所有 .log 文件
log_files=(*.log)

if [[ ${#log_files[@]} -eq 0 ]]; then
    echo "No log files found"
    exit 1
fi

# 统计每个文件的行数并汇总
total_lines=0
for file in "${log_files[@]}"; do
    lines=$(wc -l < "$file")
    echo "$file: $lines lines"
    ((total_lines += lines))
done

echo "Total: $total_lines lines"

7.2 命令行参数解析器

declare -A config

while [[ $# -gt 0 ]]; do
    case $1 in
        --name)
            config[name]=$2
            shift 2
            ;;
        --port)
            config[port]=$2
            shift 2
            ;;
        --verbose)
            config[verbose]=true
            shift
            ;;
        --help)
            echo "Usage: $0 [--name NAME] [--port PORT] [--verbose]"
            exit 0
            ;;
        *)
            echo "Unknown option: $1"
            exit 1
            ;;
    esac
done

echo "Configuration:"
for key in "${!config[@]}"; do
    echo "  $key = ${config[$key]}"
done

7.3 队列实现

# 使用数组实现简单的队列
queue=()

# 入队
enqueue() {
    queue+=("$1")
}

# 出队
dequeue() {
    if [[ ${#queue[@]} -eq 0 ]]; then
        return 1
    fi
    local item="${queue[0]}"
    queue=("${queue[@]:1}")
    echo "$item"
}

# 使用示例
enqueue "task1"
enqueue "task2"
enqueue "task3"

while item=$(dequeue); do
    echo "Processing: $item"
done

7.4 彩色输出工具

declare -A colors=(
    [red]="\033[0;31m"
    [green]="\033[0;32m"
    [yellow]="\033[0;33m"
    [blue]="\033[0;34m"
    [reset]="\033[0m"
)

color_echo() {
    local color=$1
    local text=$2
    echo -e "${colors[$color]}${text}${colors[reset]}"
}

color_echo red "Error: Something went wrong"
color_echo green "Success: Task completed"
color_echo yellow "Warning: Disk space low"

7.5 处理 CSV 数据

# 解析 CSV 行
csv_line="apple,banana,cherry,date"
IFS=',' read -ra fields <<< "$csv_line"

echo "CSV has ${#fields[@]} fields:"
for i in "${!fields[@]}"; do
    echo "  Field $i: ${fields[i]}"
done

# 生成 CSV 行
data=(John Doe 30 "New York")
IFS=','; csv_line="${data[*]}"
echo "Generated CSV: $csv_line"

第八部分:性能考量

8.1 性能特征

操作时间复杂度说明
按索引访问O(1)映射模型,直接查找
追加 +=摊销 O(1)类似动态数组
删除中间元素O(n)需要重建数组
切片O(n)创建新数组
搜索O(n)线性扫描

8.2 何时使用 Bash 数组

数据规模推荐方案说明
< 100 元素✅ Bash 数组完全合适
100-1000 元素⚠️ 可以用性能尚可
1000-10000 元素❌ 不推荐考虑 awk 或 Python
> 10000 元素❌ 避免使用专用工具

8.3 性能优化建议

# ❌ 避免:频繁重建数组
for item in "${items[@]}"; do
    results=("${results[@]}" "$item")  # 每次复制整个数组
done

# ✅ 推荐:使用 += 追加
for item in "${items[@]}"; do
    results+=("$item")  # 摊销 O(1)
done

结语:设计思想如何指导实践

回顾全文,Bash 数组的设计围绕一个核心目标:在词分割的 Shell 环境下,优雅地管理一组词。理解这一原点,可以自然推导出所有实践建议:

设计原则推导出的实践
服务于词分割遍历数组时始终使用 "${arr[@]}"
映射模型不要假定索引连续,用 "${!arr[@]}" 遍历
语法一致性用类比法学习:知道字符串切片就会数组切片
逻辑泛化关联数组的用法完全复制索引数组

最重要的建议:

永远记住,Bash 数组是一个“词的容器”,不是“C 语言数组的翻版”。当你感到困惑时,问自己:“我是在处理独立的一串词,还是在处理一个整体字符串?”答案会告诉你该用 @ 还是 *。

Bash 数组可能不是最优雅的数据结构实现,但它与 Shell 生态的集成度无与伦比。掌握它,你将能够写出更简洁、更高效的脚本,让数据在命令与命令之间自由流动。

希望这份指南不仅教会了你“怎么用”,更让你理解了“为什么这么用”。这正是从“会写脚本”到“精通 Bash”的关键一跃。

作者

老丹

关注我
其他文章
上一个

Bash 变量内容操作完全指南

下一个

Bash命令行参数完全指南

关于博主

    老丹是一名C/C++后台开发工程师,信奉“无抽象不设计,无性能不生产”。

  • 技术栈:Modern C++、Linux环境编程、多线程/并发、网络编程等。
  • 信条:能用constexpr解决的问题绝不拖到运行时,能靠RAII避免的泄漏绝不写析构。
  • 正在填坑:从解封装到渲染的C++全链路实现,正在驯服FFmpeg与H.264/H.265。
  • 输出原则:这里的每一段代码都经过-Wall -Wextra -Werror -O2的洗礼。

搜索

近期文章

  • Bash命令行参数完全指南 2026年6月17日
  • Bash 数组完全指南:从设计思想到实战应用 2026年6月16日
  • Bash 变量内容操作完全指南 2026年6月16日
  • usbutils:Linux下USB设备查看与调试的完整指南 2026年6月16日
  • MQTT协议完全指南:从核心概念到实践应用 2026年6月16日
  • ICO文件格式完全解析 2026年6月15日

文章分类

  • C/C++开发 (10)
  • Linux工具包 (6)
  • Linux服务配置 (7)
  • Shell脚本 (3)
  • 计算机理论 (9)
联系我们:📍 地址:中国·广东省深圳市   |   ✉️ 邮箱:support@tanglinux.com   |   💬 QQ:870866607
版权所有:老丹的足迹粤ICP备2026061170号-1       公安备案图标 粤公网安备44030002013274号