文章目录
-
- [Bash `readonly` 详解:只读变量、数组与函数](#Bash
readonly详解:只读变量、数组与函数) -
- [1. 语法与选项](#1. 语法与选项)
- [2. 标量变量:定义「常量」](#2. 标量变量:定义「常量」)
- [3. 查看所有只读名字:`readonly -p`](#3. 查看所有只读名字:
readonly -p) - [4. 索引数组:`readonly -a`](#4. 索引数组:
readonly -a) - [5. 关联数组:`readonly -A`](#5. 关联数组:
readonly -A) - [6. 只读函数:`readonly -f`](#6. 只读函数:
readonly -f) - [7. 与 `declare -r` 的关系](#7. 与
declare -r的关系) -
- [7.1 核心关系:等价性](#7.1 核心关系:等价性)
- [**7.2 主要区别**](#7.2 主要区别)
- [8. 实用场景示例](#8. 实用场景示例)
-
- [8.1 解析参数后「冻结」配置](#8.1 解析参数后「冻结」配置)
- [8.2 从文件加载配置后标记只读](#8.2 从文件加载配置后标记只读)
- [9. 注意点与常见错误](#9. 注意点与常见错误)
-
- [9.1 只读变量不能 `unset`,也不能再赋值](#9.1 只读变量不能
unset,也不能再赋值) - [9.2 子 Shell 里:只读不会「继承」未导出的变量](#9.2 子 Shell 里:只读不会「继承」未导出的变量)
- [9.3 `readonly` 与 `export`:两件事,可以叠在一起](#9.3
readonly与export:两件事,可以叠在一起) - [9.4 对「尚未赋值」的名字执行 `readonly`:会锁成空](#9.4 对「尚未赋值」的名字执行
readonly:会锁成空) - [9.5 函数内 `readonly`:默认是全局(和 `local` 搭配则只在函数内)](#9.5 函数内
readonly:默认是全局(和local搭配则只在函数内))
- [9.1 只读变量不能 `unset`,也不能再赋值](#9.1 只读变量不能
- [10. 小结](#10. 小结)
- [Bash `readonly` 详解:只读变量、数组与函数](#Bash
Bash readonly 详解:只读变量、数组与函数
readonly 是 Bash 的内建命令,用来把名字标记为只读。很多人先把它当成「常量变量」用,其实它可以作用在多种对象上,可归纳为三类:
| 对象 | 典型用法 | 作用 |
|---|---|---|
| 普通变量(标量) | readonly NAME=value 或 readonly NAME |
禁止再次赋值,unset 通常也会失败 |
| 数组 | 索引数组用 readonly -a,关联数组用 readonly -A(常先 declare -A) |
禁止整体重赋值及修改元素(Bash 5.x 下如此) |
| 函数 | readonly -f func_name |
禁止用新的函数定义覆盖同名函数 |
也就是说:不只变量,数组和函数都可以用 readonly 锁住 ;配合 readonly -p / readonly -pf 还能列出当前所有只读变量或只读函数。标定之后,脚本后半段误改配置、误覆盖函数的概率会明显下降。
下文先给出语法与选项,再分场景举例。
1. 语法与选项
readonly 的基本形式(摘自 Bash 内建说明):
text
readonly [-aAf] [name[=value] ...] 或 readonly -p
| 选项 | 含义 |
|---|---|
| (无选项) | 将普通变量(或带 name=value 时先赋值再标记)设为只读 |
-a |
针对索引数组变量 |
-A |
针对关联数组变量 |
-f |
将 Shell 函数 设为只读 |
-p |
列出当前所有只读变量;若与 -f 连用则列出只读函数 |
若提供 name=value,会先完成赋值,再把该名字标为只读。若变量已存在,也可以只写 readonly name,在已有值的基础上标记只读(需保证此前已赋值)。
退出状态: 选项非法或名字非法时失败;否则为成功。
2. 标量变量:定义「常量」
脚本顶部的版本号、固定路径等,常用只读变量充当常量:
bash
#!/usr/bin/env bash
readonly SCRIPT_VERSION='1.2.0'
readonly CONFIG_DIR='/etc/myapp'
# 下面一行会报错:bash: CONFIG_DIR: readonly variable
# CONFIG_DIR='/tmp/other'
一次性定义并标记:
bash
readonly MAX_RETRY=3
echo "$MAX_RETRY" # 3
只读变量仍可读、可参与参数扩展;只是不能再次赋值(除非在子 Shell 里未继承同名约束的场景下另说,见后文注意点)。
3. 查看所有只读名字:readonly -p
调试或确认环境时,可打印当前 Shell 中所有只读变量(输出形式一般为可 eval 或阅读的 declare 风格):
bash
readonly FOO=bar
readonly -p | grep -E '^declare -r FOO=' || readonly -p | head
在交互式 Shell 里,readonly -p 还会包含 Bash 启动时就已经是只读的变量(若有)。
bash-5.2$ readonly -p
declare -r BASHOPTS="checkwinsize:cmdhist:complete_fullquote:expand_aliases:extquote:force_fignore:globasciiranges:globskipdots:hostcomplete:interactive_comments:patsub_replacement:progcomp:promptvars:sourcepath"
declare -ar BASH_VERSINFO=([0]="5" [1]="2" [2]="37" [3]="1" [4]="release" [5]="aarch64-apple-darwin24.2.0")
declare -ir EUID="501"
declare -ir PPID="46710"
declare -r SHELLOPTS="braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor"
declare -ir UID="501"
bash-5.2$
每一行代表一个只读变量,格式遵循 declare [选项] 变量名="值"。前缀的选项告诉了我们变量的类型:
declare -r : 普通的字符串变量(只读)。
declare -ar: 索引数组(Array,只读)。
declare -ir: 整数变量(Integer,只读)。
4. 索引数组:readonly -a
把整个索引数组标为只读后,既不能整体重新赋值,也不能修改已有下标的元素 (在 Bash 5.x 下验证如此)。脚本里应初始化一次后当作固定列表使用。
bash
#!/usr/bin/env bash
readonly -a COLORS=('red' 'green' 'blue')
echo "${COLORS[1]}" # green
# 整数组赋值通常会失败,例如:
# COLORS=('x') # 错误:COLORS: readonly variable
readonly -p |grep -i colors
declare -ar COLORS=([0]="red" [1]="green" [2]="blue")
若需要关联数组,使用 -A
5. 关联数组:readonly -A
需先 declare -A,再 readonly -A,或一步用 declare -rA(见第 7 节与 declare 的关系):
bash
#!/usr/bin/env bash
# 定义关联数组
declare -A PORT_MAP=(
[http]=80
[https]=443
)
readonly -A PORT_MAP
echo "${PORT_MAP[https]}" # 443
6. 只读函数:readonly -f
防止函数定义被后续脚本覆盖,可用于加载「库函数」后加锁:
bash
#!/usr/bin/env bash
cleanup() {
rm -f /tmp/myapp.$$
}
readonly -f cleanup
# 下面若再定义同名函数,可能报错或无法覆盖(只读函数不可再被重新定义)
cleanup() { echo "hello wrold"; } # error: cleanup: readonly function
列出只读函数:
查看当前的只读函数
bash
readonly -pf # 或 help 所述:-p 与 -f 组合列出只读函数
实际输出以当前 Bash 为准;若需确认,可在本机执行 readonly -pf。
bash-5.2$ readonly -pf
cleanup ()
{
# $$ 当前 Shell 进程的 ID(PID)
rm -f /tmp/myapp.$$
}
declare -fr cleanup
bash-5.2$
declare: 声明命令。-f: 表示这是一个函数。-r: 表示这是一个只读对象。cleanup: 函数名。
7. 与 declare -r 的关系
declare -r name 与 readonly name 在标量变量上效果类似,都是只读。需要一次声明类型 + 只读 时,用 declare 较省事:
bash
declare -ra IDS=(1 2 3) # 只读索引数组
declare -rA META=([http]=80) # 只读关联数组
declare -ri COUNT=10 # 只读整数
习惯上:需要同时指定类型(整数 -i、数组等)时用 declare,单纯常量用 readonly 更直观。
7.1 核心关系:等价性
在 Bash 中,readonly 命令在设置变量为只读时,底层实际上调用的就是 declare 的机制。
效果相同:readonly VAR=value 和 declare -r VAR=value 执行后,变量 VAR 都会变成只读,无法修改,也无法删除(unset)。
显示相同:当你使用 readonly -p 查看只读变量时,输出格式统一都是 declare -r ...。这证明了它们在系统内部存储的属性是一致的。
7.2 主要区别
| 特性 | declare -r |
readonly |
|---|---|---|
| 出身/标准 | Bash 特有 (也存在于 Ksh, Zsh)。 非 POSIX 标准。 | POSIX 标准。 兼容 sh, dash, bash, ksh 等几乎所有 Shell。 |
| 功能扩展 | 更强。可以组合其他选项。 例如:declare -ri (只读+整数), declare -rx (只读+导出环境变量)。 |
单一。只能设置"只读"属性,不能同时指定变量类型。 |
| 作用域 (函数内) | 局部 (Local)。 在函数内使用 declare -r,变量默认是局部的,函数结束后销毁。 |
全局 (Global)。 在函数内使用 readonly,变量通常是全局的,会影响外部环境。 |
| 批量操作 | 通常需要分开写,或者结合数组语法。 | 支持一次性标记多个变量:readonly VAR1 VAR2 VAR3。 |
8. 实用场景示例
8.1 解析参数后「冻结」配置
bash
#!/usr/bin/env bash
VERBOSE=0
while getopts 'v' opt; do
case "$opt" in
v) VERBOSE=1 ;;
esac
done
readonly VERBOSE
# 后续逻辑不应再改 VERBOSE,误写会立即失败
8.2 从文件加载配置后标记只读
bash
#!/usr/bin/env bash
# 假设 source 了某个设置 DB_HOST 的文件
# shellcheck source=/dev/null
source ./config.sh
readonly DB_HOST DB_PORT
注意:若 config.sh 里没有定义某个变量,readonly DB_HOST 可能只标记空值只读;更稳妥是在 source 后检查是否已设置再 readonly。
9. 注意点与常见错误
这一节把容易踩坑的几类情况拆开说明,每条都配上可在终端里直接试的命令,方便建立直觉。
9.1 只读变量不能 unset,也不能再赋值
readonly 的本意就是「锁死这个名字」。需要临时存在、用完要删的变量(例如循环里的累加器),不要设成只读。
bash
readonly LOCKED=1
unset LOCKED # bash: unset: LOCKED: cannot unset: readonly variable
LOCKED=2 # bash: LOCKED: readonly variable
结论: 只有确定整个脚本生命周期内都不应被改掉的名字,才适合 readonly。
9.2 子 Shell 里:只读不会「继承」未导出的变量
bash -c '...'、(...) 子 Shell、$(command) 里启动的都是新的 Shell 进程 。父 Shell 里普通的只读变量没有 export 时,子 Shell 根本看不到;这和是不是只读无关,而是环境变量传递规则。
bash -c 会启动一个新的shell进程
bash
readonly PARENT_ONLY=hi
bash -c 'echo "子 Shell: ${PARENT_ONLY:-(空)}"'
# 典型输出:子 Shell: (空)
export PARENT_ONLY # 只读后仍允许 export
bash -c 'echo "子 Shell: $PARENT_ONLY"'
# 输出:子 Shell: hi
反过来:子 Shell 永远不能改写父 Shell 的变量表;父里的只读不会被「解锁」。下面两点要分清:
未 export 时: 子 Shell 里看不到 父的变量;你在子 Shell 里写 LOCKED=child 是在子进程里新建一个同名变量,与父无关。
bash
readonly LOCKED=parent
bash -c 'LOCKED=child; echo "子内: $LOCKED"' # 子内: child
echo "父仍为: $LOCKED" # 父仍为: parent
已 export 时: 环境只传递字符串值 ,不会 把 Bash 的只读属性 传给子进程。子 Shell 里该名字一般是普通变量 ,往往可以 再赋值,但改的是子进程自己的副本 ;父 Shell 里仍是只读,值也不变。
bash
readonly LOCKED=parent
export LOCKED
bash -c 'LOCKED=child; echo "子内: $LOCKED"' # 子内: child
echo "父仍为: $LOCKED" # 父仍为: parent
因此:readonly 防的是当前 Shell (及同一进程内后续代码)误改;若还要约束子脚本,需在子脚本里自行 readonly 或靠流程规范,不能指望「导出」自动带上只读语义。
9.3 readonly 与 export:两件事,可以叠在一起
| 机制 | 解决的问题 |
|---|---|
export |
变量是否进入环境 ,子进程能否读到 |
readonly |
当前 Shell(及继承该属性的环境)里是否禁止再赋值 |
常见写法:先设值并导出,再标只读------本 Shell 不能再改 APP_HOME;子进程能读到初始值。注意:子 Shell 若给自己同名变量赋值,改的是子进程副本,不会 改父进程(见 9.2)。
bash
export APP_HOME=/opt/myapp
readonly APP_HOME
# 一条命令同时「只读 + 导出」:
declare -rx CONFIG_PATH=/etc/myapp.conf
readonly -p | grep CONFIG_PATH # 往往看到 declare -rx ...
| 选项 | 含义 | 对应英文 | 作用 |
|---|---|---|---|
| -x | 导出 | export | 将该变量标记为环境变量,使其对子进程(如脚本调用的其他程序)可见。 |
| -r | 只读 | read-only | 将该变量锁定,防止后续被修改或删除。 |
等价写法
使用 declare -rx 是一种简洁的写法。如果把它拆开,等同于以下两步:
bash
# 第一步:赋值并导出
export CONFIG_PATH=/etc/myapp.conf
# 第二步:设为只读
readonly CONFIG_PATH
或者
bash
# 定义变量
CONFIG_PATH=/etc/myapp.conf
declare -x CONFIG_PATH # 导出
declare -r CONFIG_PATH # 只读
9.4 对「尚未赋值」的名字执行 readonly:会锁成空
若变量还不存在,执行 readonly VAR 会创建一个值为空的只读变量,之后想正常赋值就晚了。
bash
unset MAYBE_MISSING 2>/dev/null
readonly MAYBE_MISSING
echo "[$MAYBE_MISSING]" # 输出:[]
MAYBE_MISSING=value # bash: MAYBE_MISSING: readonly variable
更稳妥的习惯:先保证有值,再只读(尤其 source 配置文件之后)。
bash
# shellcheck source=/dev/null
source ./config.sh
if [[ -z "${DB_HOST:-}" ]]; then
echo "错误: config.sh 未设置 DB_HOST" >&2
exit 1
fi
readonly DB_HOST
9.5 函数内 readonly:默认是全局(和 local 搭配则只在函数内)
在函数里只写 readonly x=1、没有先 local,x 一般是全局只读,调用完函数后外面仍能看到。
bash
fun() {
readonly INNER=1
}
fun
echo "$INNER" # 1:泄漏到全局,容易误伤后续逻辑
若只想在函数内锁死某个名字,应先 local,再 readonly(或一条命令写完:local -r)。
bash
g() {
local INNER2=1
readonly INNER2
}
g
echo "${INNER2:-(函数外无此变量)}" # (函数外无此变量)
# Bash 4.3+ 推荐:局部只读一行搞定
h() {
local -r TMP_DIR="/tmp/$USER"
echo "$TMP_DIR"
}
实用建议: 函数内临时常量用 local / local -r;只有刻意要全局锁死 的配置,再在函数里裸用 readonly。
declare -r 在函数内默认是局部 (与函数内裸用 readonly 不同),对比见上文 7.2 表格。
10. 小结
| 需求 | 做法 |
|---|---|
| 脚本内常量 | readonly NAME=value |
| 只读数组 | readonly -a / readonly -A(配合 declare -A) |
| 防止函数被覆盖 | readonly -f func_name |
| 列出只读项 | readonly -p / readonly -pf |
readonly 是 Bash 脚本里控制可变性的简单手段:适合配置、版本号、固定表和关键函数;需要运行时删除或反复改写的名字,应使用普通变量并配合清晰的作用域(如 local)。
分享快乐,留住感动。'2026-04-09 09:22:00' --frank