Bash `readonly` 详解:只读变量、数组与函数

文章目录

    • [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 readonlyexport:两件事,可以叠在一起)
        • [9.4 对「尚未赋值」的名字执行 `readonly`:会锁成空](#9.4 对「尚未赋值」的名字执行 readonly:会锁成空)
        • [9.5 函数内 `readonly`:默认是全局(和 `local` 搭配则只在函数内)](#9.5 函数内 readonly:默认是全局(和 local 搭配则只在函数内))
      • [10. 小结](#10. 小结)

Bash readonly 详解:只读变量、数组与函数

readonly 是 Bash 的内建命令,用来把名字标记为只读。很多人先把它当成「常量变量」用,其实它可以作用在多种对象上,可归纳为三类:

对象 典型用法 作用
普通变量(标量) readonly NAME=valuereadonly 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 namereadonly 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=valuedeclare -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 readonlyexport:两件事,可以叠在一起
机制 解决的问题
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、没有先 localx 一般是全局只读,调用完函数后外面仍能看到。

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

相关推荐
IMPYLH1 天前
Linux 的 mktemp 命令
linux·运维·服务器·bash
拾贰_C1 天前
【Claude Code | bash | install】安装Claude Code
开发语言·bash
程序员小崔日记2 天前
一个命令救命:GitHub 爆火项目 thefuck,真把我笑服了
github·bash·开发者·宝藏项目
IMPYLH2 天前
Linux 的 mkdir 命令
linux·运维·服务器·bash
zhaoshuzhaoshu2 天前
Bash 与 Dash 的区别与联系
开发语言·bash·dash
IMPYLH2 天前
Linux 的 mkfifo 命令
linux·运维·服务器·bash
IMPYLH3 天前
Linux 的 ls 命令
linux·运维·服务器·bash
IMPYLH4 天前
Linux 的 logname 命令
linux·运维·服务器·bash
IMPYLH4 天前
Linux 的 ln 命令
linux·运维·服务器·bash