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 对比。

Linux命令Linux系统UbuntuUbuntu 22.04命令行Shell2026/10/16

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() 包装器);坏处是子函数依赖了调用者的局部名就会脆弱。实践准则:子函数只把动态作用域当只读的上下文用,核心参数一律显式传参。

六、参考链接