local 命令详解与参数实测(Ubuntu 22.04)
以 Ubuntu 22.04(bash 5.1.16)实测逐用法讲解 bash 内建 local:函数外报错原文、局部遮蔽与返回后恢复、动态作用域、local -i/-a/-r/-n 属性、函数内 declare 等价性、经典坑 local rc=$(false) 掩盖退出码、readonly 冲突与 set -e 叠加行为,附 local 与全局变量/declare 对比。
local 命令详解与参数实测(Ubuntu 22.04)
目标:全面掌握 bash 内建命令 local 的每一种用法——在函数内声明局部变量、局部遮蔽全局并在返回后自动恢复、动态作用域规则、
local -i/-a/-r/-n属性变体、函数内 declare 与 local 的等价关系,以及最经典的坑local rc=$(false)把命令退出码掩盖成 0、与 readonly 全局变量冲突、叠加set -e的真实行为,全部命令与回显已在 Ubuntu 22.04(bash 5.1.16)实测通过。
一、环境信息
本文所有命令均在以下环境实测,回显逐字照抄:
- 操作系统:Ubuntu 22.04.5 LTS(
PRETTY_NAME="Ubuntu 22.04.5 LTS") - 内核架构:x86_64
- Shell:GNU bash 5.1.16(1)-release(软件包 bash 5.1-6ubuntu1.1)
- 执行身份:root
- 演示脚本目录:
/root/demo/local/(loc1.sh~loc4.sh,函数与变量均在脚本内定义执行)
二、命令概览
2.1 语法与测试数据
local 用于在函数内声明局部变量,函数返回后变量自动销毁、被遮蔽的全局值自动恢复。语法形态(来自实测 help local 首行):
local [option] [name[=value] ...]
local var=value:声明局部变量并赋初值;local var1 var2:一次声明多个(可分别带值);local -i/-a/-r/-n:带整数/数组/只读/命名引用属性;- 只能在函数内使用,函数外直接报错(3.1 实测)。
本文测试数据约定:所有演示在 /root/demo/local/ 目录下的独立脚本中逐条执行;全局变量用 name、counter 等直接赋值构造,函数用 show()、bump() 等短名。每个小节均可独立执行复现。
💡 说明:bash 默认所有变量都是全局的,函数里不写 local 的赋值会直接改写全局——这是脚本被「顺便」污染的头号来源。local 只影响可见性与生命周期,不影响是否导出给子进程(export 是另一个维度)。
2.2 help 全量输出(逐行照抄)
以下为 help local 的完整实测输出:
$ help local
local: local [option] name[=value] ...
Define local variables.
Create a local variable called NAME, and give it VALUE. OPTION can
be any option accepted by `declare'.
Local variables can only be used within a function; they are visible
only to the function where they are defined and its children.
Exit Status:
Returns success unless an invalid option is supplied, a variable
assignment error occurs, or the shell is not executing a function.
关键信息解读:帮助正文很短,但两句原文都对应实测——「visible only to the function where they are defined and its children」即局部变量对子函数同样可见,这就是动态作用域(3.4 实测);退出状态原文「a variable assignment error occurs, or the shell is not executing a function」对应 3.1 的函数外报错与 3.11 的 local 自身返回值语义。属性选项(-i/-a/-r/-n 等)原文只说「OPTION can be any option accepted by declare」,即与 declare 同族——这正是 3.6~3.8 逐项实测的属性来源。
2.3 版本信息与内建归属
local 是纯 shell 内建命令,版本随 bash 发布:
$ type -a local
local is a shell builtin
$ bash --version | head -1
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)
$ dpkg -l bash | tail -1
ii bash 5.1-6ubuntu1.1 amd64 GNU Bourne Again SHell
⚠️ 注意:系统里没有 /usr/bin/local 独立程序(type -a 只返回 shell builtin)。local 与 declare 同族(help 末行明说),在函数内使用 declare 效果与 local 一致(3.9 实测)。
三、参数逐项实测
3.1 函数外使用报错:can only be used in a function
local 是函数专属内建,顶层直接调用会失败:
local outside_var=1
echo 退出码=$?
实测回显:
$ local outside_var=1
/root/demo/local/loc1.sh: line 5: local: can only be used in a function
$ echo 退出码=$?
退出码=1
💡 要点:报错原文固定为 local: can only be used in a function,退出码 1,变量没有被创建。排查脚本报这个错时直接找函数体外的 local 行,常见于复制函数片段时把声明留在了外面。
3.2 局部遮蔽全局:函数内外互不干扰
函数内声明同名局部变量后,读取到的是局部值;全局值原样保留:
name="GLOBAL"
show() { local name="LOCAL"; echo "函数内: $name"; }
show
echo "函数外: $name"
实测回显:
$ name="GLOBAL"
$ show() { local name="LOCAL"; echo "函数内: $name"; }
$ show
函数内: LOCAL
$ echo "函数外: $name"
函数外: GLOBAL
💡 要点:函数内的 local name="LOCAL" 把全局 name 暂时盖住,函数看自己声明的,函数外看全局的。注意局部变量改变量本身不改变全局——函数内若还想改全局,可用 name=$name 的取值时机差或起不同名字。
3.3 函数返回后局部变量消失、全局值恢复
局部变量的生命周期严格等于函数执行期;函数返回后即使函数内改过,全局也不受影响:
counter=100
bump() { local counter=0; counter=$((counter+5)); echo "函数内 counter=$counter"; }
bump
echo "函数外 counter=$counter"
unset -v counter; type -t counter; echo "变量已不存在, type 查询退出码=$?"
实测回显:
$ counter=100
$ bump() { local counter=0; counter=$((counter+5)); echo "函数内 counter=$counter"; }
$ bump
函数内 counter=5
$ echo "函数外 counter=$counter"
函数外 counter=100
$ unset -v counter
$ type -t counter
$ echo "变量已不存在, type 查询退出码=$?"
变量已不存在, type 查询退出码=1
💡 要点:函数内从 0 加到 5,函数外仍是 100——局部副本与全局互不污染;函数返回后局部 counter 整个销毁。最后用 unset + type -t 验证了全局 counter 被清理后查询退出码为 1(变量不存在)。这套「遮蔽→恢复」机制让函数可以放心使用通用临时名(如 i、rc、tmp)。
3.4 动态作用域:子函数可见调用者的 local 变量
help 原文「restricted to that function and its children」的含义:local 变量对当前函数调用的子函数可见。实测:
inner() { echo "inner 看到 secret=$secret"; }
outer() { local secret="from-outer"; inner; }
outer
inner
echo "全局: [$secret]"
实测回显:
$ inner() { echo "inner 看到 secret=$secret"; }
$ outer() { local secret="from-outer"; inner; }
$ outer
inner 看到 secret=from-outer
$ inner
inner 看到 secret=
$ echo "全局: [$secret]"
全局: []
💡 要点:outer 调用 inner 时,inner 能看到 outer 的局部 secret;直接调用 inner 时 secret 为空。这就是 bash 的动态作用域(按调用链可见),与 C/Python 的静态作用域完全不同。实用推论:子函数可以读调用者的局部变量,但不要依赖——重命名调用者的局部名会让子函数静默读到空值。
3.5 一次声明多个变量与缺省赋值
一条 local 声明多个名字,赋值在声明时求值,未赋值的初始为空:
x=1
demo2() { local a=10 b=$x c; echo "a=$a b=$b c=[${c:-空}]"; }
demo2
实测回显:
$ x=1
$ demo2() { local a=10 b=$x c; echo "a=$a b=$b c=[${c:-空}]"; }
$ demo2
a=10 b=1 c=[空]
💡 要点:local a=10 b=$x c 一行声明三个局部变量——a 常量、b 取全局现值、c 只声明为空。b=$x 在声明行展开,拿到的是声明那一刻的全局值。养成习惯:能赋初值就赋初值,未初始化的局部变量参与 set -u 环境会直接报 unbound variable。
3.6 local -i:整数属性
-i 让变量按算术表达式求值,赋值字符串自动当算式算:
calc() { local -i total=$1+$2; echo "total=$total"; }
calc 3 4
calc 7 8
实测回显:
$ calc() { local -i total=$1+$2; echo "total=$total"; }
$ calc 3 4; echo 退出码=$?
total=7
退出码=0
$ calc 7 8
total=15
💡 要点:total=$1+$2 对普通变量是字符串拼接,对 -i 变量是算术求值。省掉 $(( )) 一层书写;代价是任何赋值都会走算术解析,传入非数字非变量名的内容会得到意外结果,慎用于处理外部输入。
3.7 local -a:局部数组
数组也能局部化,函数内完整可操作,返回后不泄漏:
collect() { local -a parts=("web" "db" "cache"); echo "第一个=${parts[0]} 个数=${#parts[@]}"; }
collect
echo "函数外: ${parts[0]:-未定义(数组没有泄漏到全局)}"
实测回显:
$ collect() { local -a parts=("web" "db" "cache"); echo "第一个=${parts[0]} 个数=${#parts[@]}"; }
$ collect
第一个=web 个数=3
$ echo "函数外: ${parts[0]:-未定义(数组没有泄漏到全局)}"
函数外: 未定义(数组没有泄漏到全局)
💡 要点:local -a parts=(...) 声明并初始化局部索引数组;函数外 ${parts[0]:-未定义} 输出兜底文案,证明数组没有泄漏。循环里累积结果的数组务必 local,否则多次调用间会残留上次的元素。
3.8 local -r:局部只读
-r 在函数执行期锁定变量,二次赋值直接报错:
lock() { local -r token="abc"; token="xyz"; echo "不会走到这里"; }
lock
echo 退出码=$?
实测回显:
$ lock() { local -r token="abc"; token="xyz"; echo "不会走到这里"; }
$ lock; echo 退出码=$?
/root/demo/local/loc2.sh: line 19: token: readonly variable
退出码=1
💡 要点:报错 token: readonly variable,赋值行失败、函数以最后执行状态返回(此处赋值失败为 1,后续 echo 没执行)。注意只读的是函数期的局部实例,函数返回后全局同名变量不受影响;只读属性也没有 local +r 的解锁手段,声明前想清楚。
3.9 函数内 declare 等价于 local
在函数体内使用 declare,作用域行为与 local 一致(help local 末行也指向 declare):
f() { declare dv="inside"; echo "dv=$dv"; declare -p dv; }
f
echo "函数外: ${dv:-未定义}"
实测回显:
$ f() { declare dv="inside"; echo "dv=$dv"; declare -p dv; }
$ f
dv=inside
declare -- dv="inside"
$ echo "函数外: ${dv:-未定义}"
函数外: 未定义
💡 要点:declare -p dv 输出 declare -- dv="inside"——没有 -g 标记,说明是局部实例;函数外查询为空。团队规范二选一即可(local 更明确),但要知道 declare 在函数内默认就是局部、加了 -g 才是全局,别按「declare=全局」的印象误写。
3.10 local -n:命名引用(间接操作另一个变量)
bash 4.3+ 支持命名引用,local 声明的引用变量是另一个变量的别名:
mydata="hello-nameref"
wrap() { local -n ref=$1; echo "通过 ref 读到 $1 的值: $ref"; }
wrap mydata
实测回显:
$ mydata="hello-nameref"
$ wrap() { local -n ref=$1; echo "通过 ref 读到 $1 的值: $ref"; }
$ wrap mydata
通过 ref 读到 mydata 的值: hello-nameref
💡 要点:local -n ref=$1 把 ref 绑定到参数指定名字的变量上,读 ref 等价读 mydata,写 ref=value 也会写进 mydata。这是 ${!name} 间接展开之外更可读的方案,且支持写操作;比 eval 拼接变量名安全得多(没有解析注入面)。注意引用目标变量在引用期间重命名或 unset 会产生悬空引用。
3.11 经典坑:local rc=$(false) 掩盖命令退出码
local 与命令替换写在同一行时,$? 是 local 自身的返回值(恒 0),不是命令替换的:
run_false() { local rc=$(false); echo "rc=[$rc] 这一行拿到 \$?=$?"; return 0; }
run_false
实测回显:
$ run_false() { local rc=$(false); echo "rc=[$rc] 这一行拿到 \$?=$?"; return 0; }
$ run_false
rc=[] 这一行拿到 $?=0
正确写法——先声明再赋值,两步分离:
run_ok() { local rc; rc=$(false); echo "rc=[$rc] 命令替换的 \$?=$?"; return 0; }
run_ok
实测回显:
$ run_ok() { local rc; rc=$(false); echo "rc=[$rc] 命令替换的 \$?=$?"; return 0; }
$ run_ok
rc=[] 命令替换的 $?=1
💡 要点:两种写法 rc 都是空串(false 无输出),但第一步拿到的 $? 一个是 0、一个是 1——local rc=$(false) 的退出状态属于 local 内建本身,命令替换的 1 被吞掉。脚本里凡是 local x=$(cmd) 后紧跟 if [ $? -ne 0 ] 的错误判断全部失效。修正公式:local x 与 x=$(cmd) 拆成两行。
3.12 与 readonly 全局变量冲突:报错但不中断
local 遇到同名 readonly 全局变量会失败,但不会终止脚本:
ro_global=1; readonly ro_global
trylock() { local ro_global=2; echo "local 的 \$?=$?, ro_global=$ro_global"; }
trylock
echo "函数返回=$?, 全局 ro_global=$ro_global"
实测回显:
$ ro_global=1
$ readonly ro_global
$ trylock() { local ro_global=2; echo "local 的 \$?=$?, ro_global=$ro_global"; }
$ trylock
/root/demo/local/loc3.sh: line 25: local: ro_global: readonly variable
local 的 $?=1, ro_global=1
$ echo "函数返回=$?, 全局 ro_global=$ro_global"
函数返回=0, 全局 ro_global=1
💡 要点:local: ro_global: readonly variable 报错后 local 返回 1,但脚本继续执行——函数内读到的仍是全局值 1,函数返回值 0(被最后一条 echo 覆盖)。两个推论:其一,想给系统 readonly 变量(如 UID、EUID)起局部名要避开;其二,local 失败没有 set -e 时只是「静默降级为读全局」,排查时要看 stderr。
3.13 叠加 set -e:local 吃掉失败导致 -e 不触发
3.11 的坑在 set -e 下后果更严重——本该退出的失败被 local 的 0 掩盖:
guard() { local out=$(false); echo "set -e 没触发, 继续执行了"; }
set -e
guard
echo "脚本仍在运行"
set +e
实测回显:
$ guard() { local out=$(false); echo "set -e 没触发, 继续执行了"; }
$ set -e
$ guard
set -e 没触发, 继续执行了
$ echo "脚本仍在运行"
脚本仍在运行
💡 要点:set -e 的判断依据是简单命令的退出码,这一行退出码属于 local(0),false 的 1 根本没到 -e 手里,错误被静默放过。这是「local + 命令替换」在严格模式脚本里最隐蔽的失效模式;严格模式下所有 local x=$(cmd) 都应拆成两行(3.11 修正写法)。
3.14 技术拓展:local 与全局变量/declare 对比
三种变量声明方式横向对比(差异点均来自本文 3.2~3.13 实测):
| 维度 | 全局赋值(不声明) | local(本文主角) | 函数内 declare |
|---|---|---|---|
| 可用位置 | 任意处 | 仅函数内,否则报错(3.1) | 任意处;函数内默认局部 |
| 生命周期 | 到 unset/进程结束 | 函数返回即销毁(3.3) | 同 local |
| 对全局同名的影响 | 直接改写 | 遮蔽,返回后恢复(3.2/3.3) | 同 local |
| 子函数可见性 | 可见 | 可见(动态作用域,3.4) | 同 local |
| 属性支持 | 无 | -i/-a/-A/-r/-n/-x 等(help 全量) | 全量属性,另支持 -g 提升为全局 |
| 退出码语义 | 普通赋值恒 0 | 自身 0,与同行命令替换无关(3.11) | 同 local |
| 适用场景 | 真正的配置/状态 | 函数内一切临时变量 | 需要属性或 -g 控制时 |
关键解读:函数内临时变量的默认选择就是 local;需要数组/整数/只读等属性或 -g 精细控制时用 declare;只有「刻意要在函数外生效」的写结果型变量才用全局赋值,且建议集中命名(如 RESULT_*)避免误伤。
四、实战示例
4.1 递归阶乘:local 保证每层变量独立
递归函数每层调用必须有独立的参数与中间值,靠 local 实现:
factorial() {
local n=$1
if [ "$n" -le 1 ]; then
echo 1
else
local sub=$(( n - 1 ))
local result=$( factorial $sub )
echo $(( n * result ))
fi
}
factorial 5
factorial 1
factorial 10
实测回显:
$ factorial 5
120
$ factorial 1
1
$ factorial 10
3628800
验证闭环:5!=120、10!=3628800 结果正确。每层的 n、sub、result 都是局部实例,递归深度再大也互不串号——这正是 4.2 反例想要省掉却省出 bug 的东西。
4.2 反例对照:递归不用 local 会踩踏全局变量
同一个递归形态,一个用 local 一个不用,观察全局同名变量的下场:
rec_local() { local level=$1; [ "$level" -ge 1 ] && rec_local $((level-1)); }
rec_global() { level=$1; [ "$level" -ge 1 ] && rec_global $((level-1)); }
level=100
rec_local 3; echo "用 local 后全局 level=$level"
rec_global 3; echo "不用 local 后全局 level=$level"
实测回显:
$ level=100
$ rec_local 3; echo "用 local 后全局 level=$level"
用 local 后全局 level=100
$ rec_global 3; echo "不用 local 后全局 level=$level"
不用 local 后全局 level=0
💡 要点:rec_global 递归直接把全局 level 从 100 写成了 0;用 local 的版本全局安然无恙。若这个全局变量后面还承担别的职责(如循环计数),就是典型的「幽灵 bug」。凡是函数内出现赋值,先问一句「这个变量该不该 local」。
五、注意事项与常见问题
Q1:脚本报 local: can only be used in a function 怎么回事?
local 写在了函数体外面(3.1 实测报错原文与退出码 1)。常见于把函数里的声明行复制出来单独执行、或粘贴代码时括号丢了。修正:把声明挪回函数内,或改成普通赋值/declare。
Q2:local rc=$(false) 后判断 $? 为什么永远是 0?
那一行的退出码属于 local 内建本身(help 原文:仅变量名非法或 readonly 时非 0),命令替换的失败状态被掩盖(3.11 实测 0 与 1 的对比)。修正写法两行拆开:local rc + rc=$(false)。叠加 set -e 时这个坑会让失败静默放行(3.13 实测)。
Q3:函数里明明没写 local,变量怎么把外面的值改了?
bash 变量默认全局,函数内不声明的赋值就是全局赋值。要隔离必须显式 local;4.2 实测展示了递归场景的全局踩踏。可用 set -u + 命名规范(临时变量统一前缀)辅助暴露问题。
Q4:local 声明的变量能被子进程/子命令看到吗?
默认不能导出;加了 local -x 的变量对子进程可见,但导出状态随函数返回一并销毁。注意动态作用域与导出是两码事:同一脚本内子函数可见(3.4)靠的是作用域链,$(...) 子 shell 里看到的是复制副本,改它不影响函数内原值。
Q5:local -r 报 readonly variable 还有救吗?
没有。-r 在声明时立刻生效且局部实例无法解锁(3.8 实测),只能改脚本去掉 -r 或换名。同理 3.12 的 readonly 全局变量冲突也是声明期失败——给变量起局部名前避开系统 readonly 名(UID/EUID/PPID/BASH_VERSINFO 等)。
Q6:动态作用域(3.4)到底是好是坏?
好处是子函数能直接复用调用者的局部上下文(比如统一的 run_with_prefix() 包装器);坏处是子函数依赖了调用者的局部名就会脆弱。实践准则:子函数只把动态作用域当只读的上下文用,核心参数一律显式传参。
六、参考链接
- bash(1) 手册(Ubuntu 22.04 jammy,local/declare 内建与 Shell Parameters 作用域一节):https://manpages.ubuntu.com/manpages/jammy/en/man1/bash.1.html
- GNU Bash Reference Manual(Bash Builtins → local;Arrays;Local Variables):https://www.gnu.org/software/bash/manual/
- 本地自省:
help local、help declare(bash 内建帮助,2.2 节全量原文) - ShellCheck 静态检查工具(local 误用检查 SC2317/SC2155 等):https://www.shellcheck.net/
- bash 源码仓库(builtins/declare.def,local 与 declare 同源实现):https://git.savannah.gnu.org/cgit/bash.git