Shell 脚本中的“字典”功能:从基础到工程化的最佳实践

前言

在日常 Shell 脚本编写中,我们常常会遇到这样的场景:需要根据服务名获取对应的端口、根据环境变量加载不同的配置参数、或者根据用户名快速查找用户信息。用 Python 的话,一个字典就能轻松搞定。但在 Shell 脚本的世界里,这该如何实现?

实际上,Bash 从 4.0 版本开始,就引入了对关联数组(Associative Arrays,又称 Hash Tables 或字典)的原生支持。如果考虑跨 Shell 兼容性(如 POSIX sh 或旧版 Bash),则还有基于字符串处理和间接引用的替代方案。

本文将从最基础的关联数组入手,逐步深入到参数传递技巧、旧环境兼容方案、配置文件读取,最后总结社区公认的最佳实践。无论你使用的是 Bash 4+、Zsh,还是需要兼容老旧环境的 POSIX sh,都能在这里找到合适的方案。


一、基础知识:Bash 关联数组

1.1 版本检查

关联数组是 Bash 4.0 及以上版本引入的特性。在编写脚本之前,务必确认当前环境是否支持:

bash 复制代码
echo $BASH_VERSION   # 输出如 5.1.16(1)-release

1.2 声明与初始化

逐个赋值(推荐初学者使用):

bash 复制代码
declare -A my_dict
my_dict["name"]="Alice"
my_dict["age"]="28"
my_dict["city"]="Seattle"

声明时直接初始化

bash 复制代码
declare -A my_dict=(
    ["name"]="Alice"
    ["age"]="28"
    ["city"]="Seattle"
)

还有一种更紧凑的写法,省略键名两侧的双引号也是可行的,但从可读性角度考虑,双引号能明确区分字符串与命令,帮助阅读者快速识别这是关联数组而非索引数组。

1.3 基础操作

操作 语法
取值 ${my_dict["key"]}
获取所有键 ${!my_dict[@]}
获取所有值 ${my_dict[@]}
检查键是否存在 [[ -v my_dict["key"] ]]
删除元素 unset my_dict["key"]
删除整个数组 unset my_dict

遍历关联数组的键值对:

bash 复制代码
for key in "${!my_dict[@]}"; do
    echo "$key: ${my_dict[$key]}"
done

⚠️ 重要提醒 :关联数组在 Bash 中是无序的,遍历顺序与赋值顺序无关。如果需要保持顺序,请使用两个独立的索引数组,或者借助外部工具如 jq 维护 JSON 数据。

1.4 使用变量作为键名

这是日常脚本中最实用的技巧之一。使用变量作为键名时,必须加双引号以避免分词和特殊字符问题:

bash 复制代码
declare -A dict
key_name="user_name"
dict["$key_name"]="John"      # ✅ 正确
# dict[$key_name]="John"      # ❌ 如果键名包含空格会出错

二、进阶技巧:处理更复杂的场景

2.1 在函数中传递关联数组:local -n

Bash 4.3+ 引入了 nameref 特性,允许通过 local -n 在函数内引用外部关联数组,这是目前最优雅的传递方式:

bash 复制代码
declare -A weapons=(
    ["Straight Sword"]=75
    ["Imperial Sword"]=90
)

print_weapons() {
    local -n arr=$1                    # 创建名引用
    for key in "${!arr[@]}"; do
        printf "%s\t%d\n" "$key" "${arr[$key]}"
    done
}

print_weapons weapons

2.2 间接引用(间接变量)

对于 Bash 4.0--4.2 版本(不支持 local -n),可以通过 ${!var} 语法实现变量名引用:

bash 复制代码
dict_name="user_1"
declare "name_$dict_name=Alice"
echo "${!dict_name}"  # 输出 name_user_1

这种间接引用机制在构建"动态变量名"时非常有用,例如在处理配置文件时,可以动态地读取和设置配置项。

2.3 数组去重

利用关联数组的键唯一性,可以轻松对列表进行去重:

bash 复制代码
declare -A unique_keys
items=("apple" "banana" "apple" "orange" "banana")
for item in "${items[@]}"; do
    unique_keys["$item"]=1
done
unique_list=("${!unique_keys[@]}")   # 得到去重后的列表

2.4 删除空条目

删除关联数组中的空值条目也是一项常见需求,可以借助 unset 命令轻松实现:

bash 复制代码
declare -A arr
arr["key1"]="value1"
arr["key2"]=""
arr["key3"]="value3"

for key in "${!arr[@]}"; do
    if [ -z "${arr[$key]}" ]; then
        unset arr["$key"]
    fi
done

三、应对旧环境:兼容 Bash 3 与 POSIX sh

3.1 为什么不推荐使用 eval

Stack Overflow 上有个广为流传的观点:"Do not use eval to emulate associative arrays. You must avoid eval like the plague." eval 会将数据当作代码执行,若数据来自不可信源,存在严重的安全隐患。

3.2 间接变量组合(declare 方法)

在 Bash 3 环境中,可以通过拼接变量名来模拟键值存储:

bash 复制代码
# 模拟 setter
setValue() {
    local array_name=$1 key=$2 value=$3
    declare "${array_name}_${key}=$value"
}

# 模拟 getter
getValue() {
    local array_name=$1 key=$2
    local var_name="${array_name}_${key}"
    echo "${!var_name}"
}

setValue "config" "port" "8080"
echo "$(getValue config port)"  # 输出 8080

3.3 字符串映射法

对于必须运行在 POSIX sh 中的脚本(例如某些嵌入式环境或 Docker 容器),可以通过一个精心设计的分隔符字符串来存储所有键值对。借助 grepsed 进行高效查找:

bash 复制代码
# 存储格式: --key1=value1:SP:--key2=value2:SP:...
store() {
    dict="$1 --$2=$3"
}

get() {
    echo "$1" | sed -e "s/.*--$2=\([^ ]*\).*/\1/" -e 's/:SP:/ /g'
}

3.4 各 Shell 的关联数组支持

Shell 版本 关联数组支持 声明方式
Bash ≥ 4.0 ✅ 原生支持 declare -A
Zsh ≥ 5.0 ✅ 原生支持 typeset -A
Ksh ≥ 93 ✅ 原生支持 typeset -A
Dash / POSIX sh 任意 ❌ 无 ---
Fish --- ⚠️ 需通过字符串和列表模拟 ---

如果你的脚本需要在多种 Shell 环境下运行,最稳健的做法是使用 POSIX 兼容的子 shell 调用外部命令(如 grepawkjq)来处理数据,或直接使用 Python/Perl 等脚本语言执行逻辑。


四、配置文件管理:将字典思想延伸至文件

4.1 Shell 原生配置(Source 方案)

如果你的配置文件本身就是 config.sh,可以通过 source 直接引入:

bash 复制代码
# config/config.sh
DB_HOST="localhost"
DB_PORT="3306"
DB_USER="admin"
SERVICE_PORTS=([web]=8080 [api]=3000)

# main.sh
source "./config/config.sh"
echo "DB_HOST=$DB_HOST"

⚠️ 安全警告source 会把配置文件当作脚本执行。如果配置文件来自不可信源,会引入代码注入风险。生产环境中应确保配置文件只被信任的脚本维护,或改用解析方式读取。

4.2 JSON + jq 解析(推荐)

对于复杂数据结构,JSON + jq 是最理想的方案,它在跨语言兼容性、数据类型支持和脚本可维护性方面都有显著优势。

bash 复制代码
# config.json
{
    "database": {
        "host": "localhost",
        "port": 3306
    },
    "services": {
        "web": 8080,
        "api": 3000
    }
}

解析示例:

bash 复制代码
#!/bin/bash
host=$(jq -r '.database.host' config.json)
web_port=$(jq -r '.services.web' config.json)

4.3 INI 格式的轻量解析

INI 文件适合非敏感的结构化配置,语义清晰。可以编写一个简单的解析器将其转换为关联数组:

bash 复制代码
# config.ini
[database]
host=localhost
port=3306

[services]
web=8080
bash 复制代码
declare -A config
while IFS='=' read -r key value; do
    [[ -z "$key" || "$key" =~ ^[[:space:]]*# ]] && continue
    key=$(echo "$key" | xargs)
    value=$(echo "$value" | xargs)
    config["$key"]="$value"
done < config.ini

五、最佳实践总结

5.1 Shebang 和环境

确保脚本以正确的 Bash 解释器运行:

bash 复制代码
#!/usr/bin/env bash
# 或 #!/bin/bash

避免使用 #!/bin/sh 执行 Bash 特有语法,否则关联数组会失效。

5.2 变量引用原则

  • 始终用双引号包裹键名${array["$key"]},可以避免键包含空格或特殊字符时的解析问题。
  • 始终用双引号包裹数组展开"${array[@]}",确保每个元素保持独立。
  • 使用 [[ ]] 而不是 [ ] 进行条件判断

5.3 选择策略速查

场景 推荐方案 原因
单脚本内少量键值对 declare -A 语法简洁,性能好
函数间传递 local -n (Bash 4.3+) 零拷贝,语义清晰
需保持插入顺序 两个索引数组 / 外部 JSON 关联数组本身无序
跨 Shell 兼容(含 POSIX sh) 外部工具 (jq/awk) 或 字符串映射 安全性高,但性能略降
静态配置(少改动) Source Shell 配置 简单直接,但注意安全边界
动态配置/复杂结构 JSON + jq 表达能力最强,社区生态成熟

5.4 工程化建议

  • 将配置读取逻辑封装为独立模块,实现 load_config()get_key() 等函数,便于复用和维护。
  • 敏感配置(如密钥、密码)绝不要硬编码在脚本中;应通过环境变量或密钥管理服务动态注入。
  • 在脚本开头添加 set -eu,让未定义变量和命令错误立刻退出,避免因配置缺失导致不可预期的行为。

结语

在 Shell 脚本中模拟字典功能,并不像在其他高级语言中那样"天然",但也远没有想象中复杂。Bash 4+ 的原生关联数组配合 nameref 引用传递,足以覆盖绝大多数日常场景。面对旧环境兼容性或跨 Shell 需求时,通过间接变量拼接或外部工具处理,也能优雅地解决问题。

掌握这些技巧后,你的 Shell 脚本将告别冗长的 if-elif 链和独立的零散变量,变得更加模块化、可维护。而当某天你发现 Shell 处理起来实在过于复杂时,不妨思考一下:这个任务是否更适合转用 Python 或 Ruby 来完成?毕竟,在合适的场景使用合适的工具,才是真正的专家之道

相关推荐
爱睡觉1113 小时前
在 Android 模拟器 Shell 下运行 ncnn 推理的性能排查记录
linux·shell
Bolt1 天前
Kimi code 用不了 Figma?看这里解决
shell·mcp
lunzi_08262 天前
【学习笔记】《Python编程 从入门到实践》第6章:字典创建、遍历与嵌套用法详解
python·字典·python 入门
星光不问赶路人2 天前
Shell 脚本避坑指南:从模式匹配到错误处理的实用技巧
shell
pr_note4 天前
balance_points
shell·tcl
pr_note4 天前
icc2/fc屏蔽指定warning
shell·tcl
诸神缄默不语10 天前
Linux shell脚本教程
linux·bash·shell·sh
liyoro14 天前
用 Codex + 提示词生成一个快速打开 Ghostty 的 macOS 小工具
macos·shell·ai编程
pr_note15 天前
bashrc/alias
shell·tcl