1. 数组基础与索引数组
一句话总结: 索引数组用数字作下标,
${arr[0]}取单个元素,${arr[@]}取全部,${#arr[@]}取长度,声明用arr=(...)。
Bash 的索引数组用整数下标访问,下标从 0 开始。定义数组最常用的方式是圆括号空格分隔列表。
# 定义与读取
fruits=("apple" "banana" "cherry")
echo "${fruits[0]}" # apple
echo "${fruits[2]}" # cherry
# 追加元素
fruits+=("date")
echo "${#fruits[@]}" # 4,元素个数
# 一次读取全部
echo "${fruits[@]}" # apple banana cherry date
# 带下标打印
echo "${fruits[*]}" # 等价,但作为单个字符串
1.1 空数组与未定义下标
${arr[@]} 在数组为空或未定义时,在 set -u 下会直接报错,这是新手最容易踩的坑。
# 空数组安全展开:加默认值
empty=()
echo "${empty[@]-}" # 空数组安全,输出空
# set -u 下直接展开空数组会报错
set -u
echo "${empty[@]}" # bash: empty[@]: unbound variable
一句话总结:
set -u下展开未定义或空数组会报 unbound variable,写成"${arr[@]-}"可安全兜底。
1.2 从命令输出构建数组
mapfile(别名 readarray)把命令或文件输出按行读入数组,比 arr=$(...) 逐字更稳。
# 按行读入数组
mapfile -t lines < /etc/hosts
echo "共 ${#lines[@]} 行"
# 从管道读入
mapfile -t ips <<< "$(awk '{print $1}' access.log)"
printf '%s\n' "${ips[@]}"
# 指定起始下标与行数
mapfile -t -s 1 -n 3 header < file.txt # 跳过首行,只读 3 行
2. 关联数组
一句话总结: 关联数组用字符串作下标,声明必须
declare -A,适合按 key 做聚合统计、缓存查找与配置映射。
索引数组的整数下标写不进"用户名→邮箱"这类映射,关联数组正是为此而生。
# 声明关联数组并赋值
declare -A users
users[alice]="alice@example.com"
users[bob]="bob@example.com"
echo "${users[alice]}" # alice@example.com
# 遍历 key 与 value
for name in "${!users[@]}"; do
echo "$name → ${users[$name]}"
done
2.1 关联数组做计数统计
awk 能按字段聚合,纯 Bash 场景下关联数组是同样的角色——统计每个状态码出现次数。
declare -A code_count
while read -r _ _ _ code; do
((code_count[$code]++))
done < access.log
for c in "${!code_count[@]}"; do
printf '%s %d\n' "$c" "${code_count[$c]}"
done | sort -rn | head -10
一句话总结: 关联数组把「状态码、IP、用户名」这类离散 key 变成可聚合的桶,
((map[$key]++))一行完成计数。
2.2 合并与删除
declare -A a=( [x]=1 [y]=2 ) b=( [y]=3 [z]=4 )
# 合并:遍历 b 覆盖进 a
for k in "${!b[@]}"; do a[$k]=${b[$k]}; done
# 删除单个 key 或清空
unset a[y]
unset a # 删除整个数组
3. 数组遍历与常用操作
一句话总结: 遍历用
for x in "${arr[@]}",下标遍历用"${!arr[@]}";切片、拼接、去重、排序是高频操作。
# 遍历元素(推荐加引号防词分裂)
for f in "${files[@]}"; do
stat -c '%n %s' "$f"
done
# 遍历下标
for i in "${!files[@]}"; do
echo "index=$i value=${files[$i]}"
done
3.1 切片与拼接
arr=(a b c d e)
# 切片:从下标 1 取 2 个
sub=("${arr[@]:1:2}")
echo "${sub[@]}" # b c
# 拼接成字符串
joined=$(IFS=,; echo "${arr[*]}")
echo "$joined" # a,b,c,d,e
一句话总结:
${arr[@]:off:len}切片、"${arr[*]}"配合 IFS 拼接成单字符串,是数组转 CSV 的两行写法。
4. 函数定义与参数
一句话总结: 函数用
name() { ... }定义,$1$2是位置参数,local声明局部变量,函数体内还能嵌套定义函数。
函数让脚本从"一串命令"进化为"可复用的程序单元"。
#!/usr/bin/env bash
greet() {
local name="${1:-world}"
echo "Hello, $name"
}
greet "shell" # Hello, shell
greet # Hello, world
4.1 参数处理模式
与主脚本的参数解析共用同一套技巧:$# 个数、$@ 全部、shift 逐位消费。
log() {
local level="$1"; shift
printf '[%s] %s\n' "$level" "$*"
}
log INFO "build start"
log ERROR "compile failed"
一句话总结: 函数参数与脚本参数同构,
local+ shift + 默认值${1:-}是函数参数处理的标准三件套。
4.2 传引用:让函数修改外部变量
Bash 没有指针,但可以用 nameref(declare -n)让函数直接读写外部数组或变量。
append_to() {
local -n _arr="$1"
_arr+=("${@:2}")
}
items=(a b)
append_to items c d
echo "${items[@]}" # a b c d
5. 返回值与输出捕获
一句话总结:
return N返回 0~255 的整数状态码,文本输出靠echo打印后用$(...)捕获;两者混用时小心 stdout 被污染。
函数有两种"返回值":return 的状态码,和打印到 stdout 的文本。
# 状态码返回
is_root() { [[ "$EUID" -eq 0 ]]; }
if is_root; then
echo "管理员"
fi
# 文本捕获
get_name() { echo "Alice"; }
name=$(get_name)
5.1 stdout 污染陷阱
函数内部的所有 echo 都会进入捕获结果,连调试输出也不例外——把调试打印改到 stderr。
compute() {
echo "debug: 开始计算" >&2 # 调试信息走 stderr
echo $(( ${1:-0} * 2 ))
}
result=$(compute 21)
echo "result=$result" # result=42,不含调试行
一句话总结: 需要捕获文本的函数,内部普通输出一律
echo,调试/日志一律>&2,否则$(...)会捞到脏数据。
5.2 返回值与输出并用
lookup() {
local key="$1"
declare -A map=( [a]=1 [b]=2 )
[[ -n "${map[$key]}" ]] || return 1
echo "${map[$key]}"
}
if val=$(lookup a); then
echo "命中: $val"
else
echo "未命中"
fi
6. 作用域与递归
一句话总结: 函数内
local变量是局部的,未声明即全局;递归需要小心全局状态污染,常用local保存中间结果。
#!/usr/bin/env bash
counter=0
bump() {
local x=10
counter=$((counter + 1)) # 修改全局
x=$((x + 1))
}
bump
echo "$counter" # 1
6.1 递归阶乘
fact() {
local n="$1"
if (( n <= 1 )); then
echo 1
else
echo $(( n * $(fact $((n - 1))) ))
fi
}
echo "$(fact 5)" # 120
一句话总结: 递归靠
local保住每一层的 n 不被覆盖;递归深度别太大,Bash 函数栈有限,超过几千层会爆。
6.2 目录递归遍历
walk() {
local dir="$1"
for entry in "$dir"/*; do
if [[ -d "$entry" ]]; then
walk "$entry"
else
echo "$entry"
fi
done
}
walk /tmp/project | head
7. 声明式风格实战
一句话总结: 把「数据放数组、逻辑放函数」,脚本就从命令堆砌变成「配置驱动」——加一个元素等于加一个功能。
命令式脚本把数据和流程揉在一起;声明式把数据分离出来,逻辑复用同一套函数。
#!/usr/bin/env bash
set -euo pipefail
# 数据:任务清单
tasks=(
"build:make build"
"test:make test"
"deploy:./deploy.sh prod"
)
run_task() {
local name="$1" cmd="$2"
echo "==> $name"
if $cmd >/dev/null 2>&1; then
echo " OK"
else
echo " FAIL"
return 1
fi
}
for t in "${tasks[@]}"; do
run_task "${t%%:*}" "${t#*:}"
done
7.1 函数式 map 与 filter
把"对每个元素做什么"提成函数,循环只剩一行。
# 对数组每个元素执行函数(map)
map() {
local fn="$1"; shift
local out=()
for x in "$@"; do
out+=("$($fn "$x")")
done
echo "${out[@]}"
}
shout() { echo "${1^^}"; }
map shout hello world # HELLO WORLD
# 过滤(filter)
filter_even() {
local out=()
for x in "$@"; do
(( x % 2 == 0 )) && out+=("$x")
done
echo "${out[@]}"
}
filter_even 1 2 3 4 5 # 2 4
一句话总结: map 与 filter 函数把循环细节封装一次,之后改逻辑只改传入的函数,代码面大幅收窄。
7.2 用关联数组做配置表
declare -A PORT=( [web]=8080 [api]=9000 [db]=5432 )
start_service() {
local svc="$1"
local port="${PORT[$svc]:-0}"
[[ "$port" != 0 ]] || { echo "未知服务 $svc"; return 1; }
echo "启动 $svc @ $port"
}
for s in web api db; do start_service "$s"; done
8. 总结
| 环节 | 要点 |
|---|---|
| 索引数组 | arr=(...) 声明,${arr[i]} 取值,${#arr[@]} 长度 |
| 关联数组 | declare -A 声明,字符串下标,适合聚合与映射 |
| 遍历 | "${arr[@]}" 元素遍历,"${!arr[@]}" 下标遍历 |
| 切片拼接 | "${arr[@]:off:len}" 切片,IFS + "${arr[*]}" 拼接 |
| 函数参数 | $1~$9 位置参数,local 局部变量,${1:-default} 默认值 |
| 返回值 | return N 状态码,echo + $(...) 捕获文本,日志走 stderr |
| 作用域递归 | 未 local 即全局,递归靠 local 保现场,深度有限 |
| 声明式 | 数据放数组、逻辑放函数,map/filter 封装循环 |
数组和函数是 Bash 从"脚本语言"走向"编程语言"的两块基石。数组管数据、函数管逻辑,再用关联数组做配置表,中等复杂度的自动化任务都能写得结构清晰。下一步进入 xargs 与并行处理,把单机脚本扩展到并发批量任务。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。