set 命令详解与参数实测(Ubuntu 22.04)
详解 bash 内置 set 命令:无参数列出变量与函数,-e/-u/-x/-v/-n/-f/-C/-a 与 -o errexit/nounset/xtrace/pipefail 逐项实测,含 -e 豁免清单、set -- 重置位置参数、set -euxo pipefail 惯用法与 set/shopt 对比。
set 命令详解与参数实测(Ubuntu 22.04)
目标:全面掌握 bash 内置 set 命令的每一个常用参数——无参数查看变量与函数、-e/-u/-x/-v/-n/-f/-C/-a 单字母选项、-o 长选项(errexit、nounset、xtrace、pipefail、noglob 等)、
--重置位置参数与 set -euxo pipefail 组合惯用法,全部命令与回显已在 Ubuntu 22.04(bash 5.1.16)实测通过。
一、环境信息
本节记录实测环境的系统与 Shell 版本,后续所有命令与回显均来自该环境,可直接复现。
$ head -2 /etc/os-release
PRETTY_NAME="Ubuntu 22.04.5 LTS"
NAME="Ubuntu"
$ uname -m
x86_64
- 操作系统:Ubuntu 22.04.5 LTS(x86_64)
- Shell 版本:GNU bash 5.1.16(1)-release
- 命令类型:set 为 bash 内置命令(builtin),没有独立的可执行文件
- 演示目录:/root/demo/set/(下文各小节的演示脚本均保存在此目录)
二、命令概览
2.1 语法与测试数据
set 的语法(摘自 help 首行):
set [-abefhkmnptuvxBCHP] [-o option-name] [--] [arg ...]
set 承担三类职责,理解这三类职责就能把所有参数归类记忆:
- 开关 shell 选项:
set -e打开 errexit,set +e关闭(+一律表示关闭); - 重置位置参数:
set -- a b c把 $1/$2/$3 重新赋值为 a/b/c; - 无参数时列出全部 shell 变量与函数。
创建演示目录:
$ mkdir -p /root/demo/set
2.2 help 全量输出(逐行照抄)
help set 共 81 行,是单字母选项与 -o 长选项的权威速查表,全文照抄如下:
$ help set
set: set [-abefhkmnptuvxBCHP] [-o option-name] [--] [arg ...]
Set or unset values of shell options and positional parameters.
Change the value of shell attributes and positional parameters, or
display the names and values of shell variables.
Options:
-a Mark variables which are modified or created for export.
-b Notify of job termination immediately.
-e Exit immediately if a command exits with a non-zero status.
-f Disable file name generation (globbing).
-h Remember the location of commands as they are looked up.
-k All assignment arguments are placed in the environment for a
command, not just those that precede the command name.
-m Job control is enabled.
-n Read commands but do not execute them.
-o option-name
Set the variable corresponding to option-name:
allexport same as -a
braceexpand same as -B
emacs use an emacs-style line editing interface
errexit same as -e
errtrace same as -E
functrace same as -T
hashall same as -h
histexpand same as -H
history enable command history
ignoreeof the shell will not exit upon reading EOF
interactive-comments
allow comments to appear in interactive commands
keyword same as -k
monitor same as -m
noclobber same as -C
noexec same as -n
noglob same as -f
nolog currently accepted but ignored
notify same as -b
nounset same as -u
onecmd same as -t
physical same as -P
pipefail the return value of a pipeline is the status of
the last command to exit with a non-zero status,
or zero if no command exited with a non-zero status
posix change the behavior of bash where the default
operation differs from the Posix standard to
match the standard
privileged same as -p
verbose same as -v
vi use a vi-style line editing interface
xtrace same as -x
-p Turned on whenever the real and effective user ids do not match.
Disables processing of the $ENV file and importing of shell
functions. Turning this option off causes the effective uid and
gid to be set to the real uid and gid.
-t Exit after reading and executing one command.
-u Treat unset variables as an error when substituting.
-v Print shell input lines as they are read.
-x Print commands and their arguments as they are executed.
-B the shell will perform brace expansion
-C If set, disallow existing regular files to be overwritten
by redirection of output.
-E If set, the ERR trap is inherited by shell functions.
-H Enable ! style history substitution. This flag is on
by default when the shell is interactive.
-P If set, do not resolve symbolic links when executing commands
such as cd which change the current directory.
-T If set, the DEBUG and RETURN traps are inherited by shell functions.
-- Assign any remaining arguments to the positional parameters.
If there are no remaining arguments, the positional parameters
are unset.
- Assign any remaining arguments to the positional parameters.
The -x and -v options are turned off.
Using + rather than - causes these flags to be turned off. The
flags can also be used upon invocation of the shell. The current
set of flags may be found in $-. The remaining n ARGs are positional
parameters and are assigned, in order, to $1, $2, .. $n. If no
ARGs are given, all shell variables are printed.
Exit Status:
Returns success unless an invalid option is given.
重点信息:Using + rather than - causes these flags to be turned off 说明 + 方向一律表示关闭选项;The current set of flags may be found in $- 说明当前标志可用 $- 查看;If no ARGs are given, all shell variables are printed 就是无参数用法的出处。
2.3 版本信息
$ bash --version | head -1
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)
$ echo $BASH_VERSION
5.1.16(1)-release
$ type set
set is a shell builtin
type set 确认 set 是 shell 内置命令,不存在独立二进制文件,其行为随 bash 版本而定,本文所有结论基于 bash 5.1.16。
三、参数逐项实测
3.1 无参数:列出全部 shell 变量与函数
不带任何参数执行 set,会打印当前 shell 的全部变量(按字母序)与全部函数定义。演示脚本先定义一个函数和一个变量,再分段观察 set 的输出:
#!/usr/bin/env bash
# /root/demo/set/show-set.sh
greet() { echo "hello from greet"; }
app_version="1.0.0"
echo "== 变量部分(head -12,按字母序) =="
set | head -12
echo "== 常用变量(grep 节选) =="
set | grep -E "^(HOME|PATH|PWD|SHELL|SHLVL|UID)="
echo "== 函数部分 =="
set | grep -A3 "^greet "
echo "== 自定义变量 =="
set | grep "^app_version"
echo "== 总行数 =="
set | wc -l
实测输出:
$ bash /root/demo/set/show-set.sh
== 变量部分(head -12,按字母序) ==
BASH=/usr/bin/bash
BASHOPTS=checkwinsize:cmdhist:complete_fullquote:extquote:force_fignore:globasciiranges:hostcomplete:interactive_comments:progcomp:promptvars:sourcepath
BASH_ALIASES=()
BASH_ARGC=()
BASH_ARGV=()
BASH_CMDS=()
BASH_LINENO=([0]="0")
BASH_SOURCE=([0]="/root/demo/set/show-set.sh")
BASH_VERSINFO=([0]="5" [1]="1" [2]="16" [3]="1" [4]="release" [5]="x86_64-pc-linux-gnu")
BASH_VERSION='5.1.16(1)-release'
DIRSTACK=()
EUID=0
== 常用变量(grep 节选) ==
HOME=/root
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
SHELL=/bin/bash
SHLVL=2
UID=0
== 函数部分 ==
greet ()
{
echo "hello from greet"
}
== 自定义变量 ==
app_version=1.0.0
== 总行数 ==
37
说明:
- 输出前半段是变量(含 bash 自动维护的 BASH、BASHOPTS、EUID 等),后半段是函数定义(greet 以
greet ()开头的完整定义形态出现),自定义变量 app_version 也混在变量区按字母序列出; - 本演示脚本上下文共 37 行;真实登录环境会加载大量配置与补全脚本,set 输出动辄数百上千行,所以排查时通常配合
head、grep截取,而不是整屏翻阅。
💡 要点:set(无参)与 env 的区别——set 列出所有 shell 变量(含未导出的局部变量)加函数;env 只列出已导出到环境的变量,不含函数。找"变量到底存不存在"用 set,看"子进程能继承什么"用 env。
3.2 set -e(errexit):命令失败立即退出
-e 让脚本在任何一条命令返回非零时立即终止。做一组对照实验:同样的两条失败命令,一个脚本不开 -e,一个开 -e。
#!/usr/bin/env bash
# /root/demo/set/no-e.sh(无 -e)
echo "第1步:开始"
ls /root/demo/set/not-exist.txt
echo "第3步:没有 -e,失败后我照样执行"
#!/usr/bin/env bash
# /root/demo/set/with-e.sh(有 -e)
set -e
echo "第1步:开始"
ls /root/demo/set/not-exist.txt
echo "第3步:有 -e,我永远执行不到"
实测输出:
$ bash /root/demo/set/no-e.sh; echo rc=$?
第1步:开始
ls: cannot access '/root/demo/set/not-exist.txt': No such file or directory
第3步:没有 -e,失败后我照样执行
rc=0
$ bash /root/demo/set/with-e.sh; echo rc=$?
第1步:开始
ls: cannot access '/root/demo/set/not-exist.txt': No such file or directory
rc=2
对照结论:
- 无 -e:ls 失败只打印错误信息,第 3 步照常执行,脚本最终 rc=0(最后一条 echo 的退出码),失败被"静默吞掉";
- 有 -e:ls 失败(rc=2)后脚本立即终止,第 3 步没有执行,脚本退出码就是触发退出的 ls 的 rc=2。
💡 要点:脚本的退出码很有诊断价值——有 -e 时 rc=2 直接告诉你"是 ls 失败退出的",而不是 rc=0 的假成功。
3.3 set -e 的豁免场景:if 条件、&&、|| 不触发
-e 并非"任何失败都退",bash 规定了几类豁免上下文。用一组实验验证:
#!/usr/bin/env bash
# /root/demo/set/e-exempt.sh
set -e
echo "A: if 条件中失败不退出"
if ls /no/such/file 2>/dev/null; then
echo "if 成立"
else
echo "if 条件失败,但脚本继续"
fi
echo "B: && 左侧失败不退出"
false && echo "不会打印"
echo "&& 之后脚本继续"
echo "C: || 右侧兜底不退出"
false || echo "false 被 || 兜住"
echo "|| 之后脚本继续"
echo "D: 独立失败的命令会退出"
false
echo "独立 false 之后,我执行不到"
实测输出:
$ bash /root/demo/set/e-exempt.sh; echo rc=$?
A: if 条件中失败不退出
if 条件失败,但脚本继续
B: && 左侧失败不退出
&& 之后脚本继续
C: || 右侧兜底不退出
false 被 || 兜住
|| 之后脚本继续
D: 独立失败的命令会退出
rc=1
- A:if/elif 的条件命令失败属于正常分支判断,不触发 errexit,走 else 分支后继续;
- B:&& 左侧失败导致整条链短路,整条链的返回值非零,但它是"被 && 连接的命令列表",不触发退出,后面的 echo 照常执行;
- C:|| 右侧兜底后整条链返回零,更不会退出,这是
cmd || true压制失败的原理; - D:独立一行的 false 触发 errexit,脚本以 rc=1 退出,最后的 echo 没有执行。
💡 要点:想让
命令1 && 命令2失败时也退出,可以在链尾补|| exit 1;反之想豁免某条独立命令的失败,写成cmd || true即可。
3.4 errexit 与函数返回值
函数内部的失败命令同样受 -e 约束:函数内触发 errexit 后函数立即返回非零,主流程随后终止。
#!/usr/bin/env bash
# /root/demo/set/e-func.sh
set -e
myfunc() {
echo "函数内第1行"
false
echo "函数内第3行:执行不到"
}
myfunc
echo "函数返回非零后,主流程执行不到"
实测输出:
$ bash /root/demo/set/e-func.sh; echo rc=$?
函数内第1行
rc=1
函数内的 false 触发 errexit,"函数内第3行"和主流程的收尾 echo 都没有执行,脚本 rc=1(false 的退出码)。
💡 要点:递归理解——errexit 对函数体和主流程一视同仁,函数内任何一条未豁免的失败命令都会"击穿"整个脚本;这正是 set -e 能保护复杂脚本的机制。
3.5 set -u(nounset):未定义变量报错
先看不加 -u 时引用未定义变量的表现,再加 -u 对照:
$ echo "未定义变量的值: [$UNDEFINED_XYZ]"
未定义变量的值: []
不加 -u 时未定义变量静默展开为空串,脚本继续跑——这是空文件、空目录等"静默事故"的常见源头。开启 -u 后:
#!/usr/bin/env bash
# /root/demo/set/with-u.sh
set -u
echo "APP_NAME=$APP_NAME"
echo "这行执行不到"
$ bash /root/demo/set/with-u.sh; echo rc=$?
/root/demo/set/with-u.sh: line 3: APP_NAME: unbound variable
rc=1
报错形态为 变量名: unbound variable,脚本以 rc=1 终止。配合 ${VAR:-默认值} 可以给可选变量兜底,不再误伤:
#!/usr/bin/env bash
# /root/demo/set/u-default.sh
set -u
echo "USER_NAME=${USER_NAME:-guest}"
echo "LOG_LEVEL=${LOG_LEVEL:-info}"
$ bash /root/demo/set/u-default.sh
USER_NAME=guest
LOG_LEVEL=info
必填变量则用 ${VAR:?提示}——未定义时带提示直接报错终止:
$ ( set -u; echo "${MISSING:?缺少必填变量 MISSING}" ); echo rc=$?
/root/set-test2.sh: line 3: MISSING: 缺少必填变量 MISSING
rc=1
$ ( set -u; MISSING=ok; echo "${MISSING:?缺少必填变量 MISSING}" ); echo rc=$?
ok
rc=0
💡 要点:
set -u+${VAR:-默认}(可选变量)+${VAR:?提示}(必填变量)三者组合,是生产脚本防"静默空值"的标准配置。
3.6 set -x(xtrace):调试跟踪与 set +x 关闭
-x 把每条命令展开后的形态打印到 stderr,前缀默认是 + 。关闭用 set +x:
#!/usr/bin/env bash
# /root/demo/set/with-x.sh
set -x
name=world
echo "hello $name"
set +x
echo "关闭跟踪后恢复普通输出"
$ bash /root/demo/set/with-x.sh; echo rc=$?
+ name=world
+ echo 'hello world'
hello world
+ set +x
关闭跟踪后恢复普通输出
rc=0
注意 + echo 'hello world':变量已展开、引号形态也被还原显示,能直接看出命令实际接收到的参数。前缀可通过 PS4 变量定制:
$ ( PS4="trace> "; set -x; cd /tmp; pwd; set +x )
ttrace> cd /tmp
ttrace> pwd
/tmp
ttrace> set +x
实测出现 ttrace> 而不是 trace> :PS4 的首字符会按命令嵌套深度重复,本例经过一层 eval 嵌套所以多打了一个 t。默认 PS4 为 + ,在函数调用、source 等多层场景下会看到 ++、+++,这代表调用深度,是排查调用链的线索。
💡 要点:不改脚本也能跟踪——直接
bash -x /path/to/script.sh等价于在脚本头加 set -x;-x 的跟踪输出走 stderr,bash -x script.sh 2>trace.log可单独留存日志。
3.7 set -o pipefail:管道中间命令失败可见
bash 默认只取管道最后一个命令的退出码,中间命令失败会被掩盖。逐条对照:
$ false | true; echo rc=$?
rc=0
$ ls /no/such-dir 2>/dev/null | wc -l; echo rc=$?
0
rc=0
$ set -o pipefail
$ false | true; echo rc=$?
rc=1
$ ls /no/such-dir 2>/dev/null | wc -l; echo rc=$?
0
rc=2
$ set +o pipefail
$ false | true; echo rc=$?
rc=0
- 开启前:
false | true返回 rc=0,ls 失败 | wc -l也返回 rc=0,中间的失败完全不可见; - 开启后:管道返回最后一个非零退出码,
false | true变成 rc=1,ls | wc -l变成 rc=2; set +o pipefail关闭后恢复默认行为。
💡 要点:pipefail 通常与 -e 组合使用才有完整效果——单开 pipefail 只让 rc 变对,配合 errexit 才能让"管道中间失败"真正中断脚本(见 3.15)。
3.8 set -f / set -o noglob:禁用通配符展开
-f(长名 noglob)关闭文件名通配展开,* 不再被替换为文件列表:
$ cd /root/demo/set && touch a.txt b.txt
$ echo *
a.txt b.txt e-exempt.sh e-func.sh no-e.sh u-default.sh with-e.sh with-u.sh with-x.sh
$ set -f
$ echo *
*
$ set +f
$ echo *
a.txt b.txt e-exempt.sh e-func.sh no-e.sh u-default.sh with-e.sh with-u.sh with-x.sh
$ set -o noglob
$ echo *.txt
*.txt
$ set +o noglob
- 默认状态:
echo *展开为目录下文件名列表; set -f后:*、*.txt原样输出,glob 关闭;set +f(或set +o noglob)恢复。
💡 要点:处理内容里含字面
*的字符串(如某些配置模板)时,临时set -f可以避免误展开;日常脚本极少全局关闭 glob,多为局部临时使用。
3.9 set --:重置位置参数
-- 之后的参数被依次赋给 $1、$2……若 -- 后没有参数,位置参数被清空。这是脚本内改写位置参数的唯一正规手段:
#!/usr/bin/env bash
# /root/demo/set/pos.sh
echo "进入脚本: \$1=$1 \$2=$2 \$#=$#"
set -- a b c
echo "set -- a b c 之后: \$1=$1 \$2=$2 \$3=$3 \$#=$#"
set --
echo "set -- 清空之后: \$1=[$1] \$#=$#"
实测输出:
$ bash /root/demo/set/pos.sh x y
进入脚本: $1=x $2=y $#=2
set -- a b c 之后: $1=a $2=b $3=c $#=3
set -- 清空之后: $1=[] $#=0
set -- a b c把原本的 x/y 整体替换为 a/b/c,$# 同步变为 3;set --不带参数则全部清空,$# 变为 0。
💡 要点:shift 是"丢掉 $1、其余前移",set -- 是"整体重建",两者配合可完成选项预处理(见 4.4 实战)。
3.10 set -o / set +o:列出全部选项开关与 $- 标志
set -o 按名称列出全部选项的 on/off 状态(本环境非交互 shell 下实测,28 项全量照抄):
$ set -o
allexport off
braceexpand on
emacs off
errexit off
errtrace off
functrace off
hashall on
histexpand off
history off
ignoreeof off
interactive-comments on
keyword off
monitor off
noclobber off
noexec off
noglob off
nolog off
notify off
nounset off
onecmd off
physical off
pipefail off
posix off
privileged off
verbose off
vi off
xtrace off
解读:默认开启的只有 braceexpand(花括号展开)、hashall(-h,命令路径缓存)、interactive-comments 三项;emacs、vi、history、monitor 等交互特性在脚本(非交互 shell)里均为 off。set +o 则以"可直接执行的 set 命令"形式输出,便于把当前选项状态保存成快照:
$ set +o | head -8
set +o allexport
set -o braceexpand
set +o emacs
set +o errexit
set +o errtrace
set +o functrace
set -o hashall
set +o histexpand
单字母标志还可用 $- 快速查看:
$ echo $-
hB
$ ( set -e; echo $- )
ehB
$ ( set -eu; echo $- )
ehuB
h 即 hashall、B 即 braceexpand,set -e 后多出 e,再开 -u 多出 u。
💡 技术拓展:管 bash 行为开关的命令不止 set 一个,还有 shopt。两者对比——
维度 set shopt 选项集合 POSIX 传统单字母 + -o 长名(28 项) bash 扩展选项(本环境实测 53 项) 查看 set -o / set +o / echo $- shopt / shopt -p 开关 set -x / set +x(+ 为关) shopt -s xxx / shopt -u xxx(-s 开、-u 关) 典型选项 errexit、nounset、xtrace、pipefail、noglob autocd、globstar、lastpipe、cdspell 兼容性 其他 POSIX shell(dash 等)大多支持 bash 特有,dash 不支持 本文主题是 set;涉及 autocd、globstar 等 bash 新选项时用 shopt 管理。set -o 表里出现而 help set 未单列的选项,本质都是单字母选项的长名别名。
3.11 set -v(verbose):打印读入的原始行
-v 在读入每行时原样打印该行,注意它与 -x 的区别:-v 打印"读到的原始文本",-x 打印"展开后的执行命令"。
#!/usr/bin/env bash
# /root/demo/set/with-v.sh
set -v
echo "看看 -v 打印什么"
$ bash /root/demo/set/with-v.sh
echo "看看 -v 打印什么"
看看 -v 打印什么
第一行是 -v 打印的原始读入行(带引号原样),第二行才是真正的执行输出。
💡 要点:-v 输出信息量比 -x 少(不展开变量),实际排障中 -x 的使用频率远高于 -v,两者可同时开启对照观察。
3.12 set -n(noexec):只读命令不执行,做语法检查
-n 让 bash 读入命令但完全不执行,用于语法校验:
#!/usr/bin/env bash
# /root/demo/set/syntax-ok.sh
set -n
echo "这行不会被执行"
$ bash -n /root/demo/set/syntax-ok.sh; echo rc=$?
rc=0
$ ( set -n; echo "会被打印吗" ) ; echo rc=$?
rc=0
bash -n 脚本 等价于脚本内 set -n:语法正确时 rc=0 且无任何输出;echo 没有执行,说明命令确实只被解析。若脚本有语法错误,bash -n 会指出具体行号并以非零退出。
💡 要点:脚本上线前先跑一遍
bash -n script.sh是零成本的语法门禁;注意 set -n 开着的时候当前 shell 不再执行任何命令,交互终端误开 set -n 会"假死",用 set +n 恢复。
3.13 set -C(noclobber):禁止重定向覆盖已有文件
-C 让 > 不能覆盖已存在的常规文件:
$ echo first > /root/demo/set/clobber.txt
$ set -C
$ echo second > /root/demo/set/clobber.txt; echo rc=$?
/root/set-test2.sh: line 3: /root/demo/set/clobber.txt: cannot overwrite existing file
rc=1
$ set +C
$ echo second >| /root/demo/set/clobber.txt; cat /root/demo/set/clobber.txt
second
开启 -C 后覆盖写被拒绝并报 cannot overwrite existing file,rc=1;>| 是绕过 noclobber 的强制覆盖写法,set +C 恢复默认。
💡 要点:-C 防的是"手滑用
>覆盖重要文件",需要强制覆盖时显式写>|比关闭选项更安全,意图一目了然。
3.14 set -a(allexport):新赋值变量自动导出
-a 打开后,之后所有新建/修改的变量自动标记为导出(等同隐式 export):
$ ( set -a; DEMO_VAR=hello; bash -c "echo 子进程中 DEMO_VAR=\$DEMO_VAR" )
子进程中 DEMO_VAR=hello
$ ( DEMO_VAR2=hello; bash -c "echo 子进程中 DEMO_VAR2=\$DEMO_VAR2" )
子进程中 DEMO_VAR2=
开 -a 时 DEMO_VAR 传进了子进程;不开时 DEMO_VAR2 是空——未导出的变量子进程看不到。
💡 要点:
set -a; source xxx.env; set +a是批量加载环境变量文件的惯用法,比逐行 export 省事且不易漏。
3.15 组合惯用法:set -euxo pipefail
四个选项组合成生产脚本的"严格模式"头:命令失败即退(-e)、未定义变量报错(-u)、打印执行跟踪(-x)、管道中间失败可见(-o pipefail)。先看失败路径:
#!/usr/bin/env bash
# /root/demo/set/strict.sh
set -euxo pipefail
mkdir -p /root/demo/set/out
ls /root/demo/set/not-exist-dir | wc -l
echo "严格模式下执行不到"
$ bash /root/demo/set/strict.sh; echo rc=$?
+ mkdir -p /root/demo/set/out
+ ls /root/demo/set/not-exist-dir
+ wc -l
ls: cannot access '/root/demo/set/not-exist-dir': No such file or directory
0
rc=2
$ ls -d /root/demo/set/out
/root/demo/set/out
管道里 ls 失败(rc=2),在 pipefail 的加持下整条管道返回 2, errexit 立即终止脚本——0 是 wc -l 打出来的,随后 echo 没有执行。再看成功路径的完整跟踪:
#!/usr/bin/env bash
# /root/demo/set/deploy.sh
set -euxo pipefail
export LANG=C.UTF-8
DEST=/root/demo/set/deploy
rm -rf "$DEST"
mkdir -p "$DEST/conf"
printf "mode=standalone\n" > "$DEST/conf/app.conf"
cat "$DEST/conf/app.conf"
grep -q "mode=standalone" "$DEST/conf/app.conf"
echo "部署完成"
$ bash /root/demo/set/deploy.sh; echo rc=$?
+ export LANG=C.UTF-8
+ LANG=C.UTF-8
+ DEST=/root/demo/set/deploy
+ rm -rf /root/demo/set/deploy
+ mkdir -p /root/demo/set/deploy/conf
+ printf 'mode=standalone\n'
+ cat /root/demo/set/deploy/conf/app.conf
mode=standalone
+ grep -q mode=standalone /root/demo/set/deploy/conf/app.conf
+ echo 部署完成
部署完成
rc=0
每一行 + 前缀都是 -x 打印的执行跟踪,整个部署过程哪一步执行了什么一目了然;grep 校验通过后脚本以 rc=0 收尾。
💡 要点:
set -euxo pipefail四件套中,-x 在生产环境可能产生大量日志,可按需裁剪为set -euo pipefail,排障时再临时加回 -x。
四、实战示例
4.1 必填参数与默认值防御(set -u + ${1:?} + ${VAR:-})
位置参数 $1 也可用 ${1:?提示} 强制必填,环境变量用 ${VAR:-默认} 兜底:
#!/usr/bin/env bash
# /root/demo/set/reqarg.sh
set -eu
DB_PASS="${1:?用法: $0 <db_password>}"
DB_HOST="${DB_HOST:-127.0.0.1}"
echo "连接 $DB_HOST,密码长度 ${#DB_PASS}"
三种调用方式的实测:
$ bash /root/demo/set/reqarg.sh; echo rc=$?
/root/demo/set/reqarg.sh: line 3: 1: 用法: /root/demo/set/reqarg.sh <db_password>
rc=1
$ bash /root/demo/set/reqarg.sh q1w2e3r4; echo rc=$?
连接 127.0.0.1,密码长度 8
rc=0
$ DB_HOST=10.0.0.5 bash /root/demo/set/reqarg.sh q1w2e3r4; echo rc=$?
连接 10.0.0.5,密码长度 8
rc=0
不传参数时以 1: 用法: ... 报错退出(1 是位置参数 $1 的参数名形态);传参后正常;DB_HOST 未设置时落到默认 127.0.0.1,设置了则用外部值。
4.2 用 set -x 定位引号缺失导致的分词问题
变量不加引号会被空白分词,肉眼难察觉,-x 跟踪一对比就现形:
#!/usr/bin/env bash
# /root/demo/set/quote-trace.sh
set -x
name="hello world"
echo $name
echo "$name"
$ bash /root/demo/set/quote-trace.sh
+ name='hello world'
+ echo hello world
hello world
+ echo 'hello world'
hello world
跟踪行 + echo hello world(无引号)说明 echo 收到了两个参数 hello 和 world;+ echo 'hello world'(带引号)说明收到一个完整参数。输出相同但语义不同,一旦换成 rm、ssh 这类命令,差别就是事故。
4.3 pipefail 保证备份管道的隐藏失败可见
压缩备份走管道时,中间环节失败必须让脚本知道。正常路径:
#!/usr/bin/env bash
# /root/demo/set/backup.sh
set -euo pipefail
src=/root/demo/set/no-e.sh
dst=/root/demo/set/backup
mkdir -p "$dst"
gzip -c "$src" > "$dst/no-e.sh.gz"
ls -l "$dst/no-e.sh.gz" | awk '{print "已生成:", $NF, "大小:", $5, "字节"}'
$ bash /root/demo/set/backup.sh; echo rc=$?
已生成: /root/demo/set/backup/no-e.sh.gz 大小: 151 字节
rc=0
源文件不存在时:
#!/usr/bin/env bash
# /root/demo/set/backup-bad.sh
set -euo pipefail
gzip -c /root/demo/set/not-exist.txt > /root/demo/set/backup/bad.gz
echo "永远执行不到"
$ bash /root/demo/set/backup-bad.sh; echo rc=$?
gzip: /root/demo/set/not-exist.txt: No such file or directory
rc=1
gzip 失败立即终止脚本,rc=1,不会出现"备份文件根本没生成、监控却显示成功"的假象。
4.4 set -- 重建位置参数实现选项预解析
先把 --verbose 之类的开关从参数里摘出来,再用 set -- 把剩余参数重建回 $1、$2……后续逻辑照常使用位置参数:
#!/usr/bin/env bash
# /root/demo/set/argparse.sh
verbose=off
args=()
while [ $# -gt 0 ]; do
case "$1" in
--verbose) verbose=on ;;
*) args+=("$1") ;;
esac
shift
done
set -- "${args[@]}"
echo "verbose=$verbose 目标=$1 其余参数个数=$#"
$ bash /root/demo/set/argparse.sh --verbose t1 t2 t3; echo rc=$?
verbose=on 目标=t1 其余参数个数=3
rc=0
$ bash /root/demo/set/argparse.sh only; echo rc=$?
verbose=off 目标=only 其余参数个数=1
rc=0
--verbose 被剥离进 verbose 变量,剩余参数经 set -- "${args[@]}" 重建后 $1 恢复为第一个普通参数,$# 同步修正。
五、注意事项与常见问题
Q1:明明开了 set -e,脚本失败后为什么还在继续跑?
先查三种豁免:失败命令是否位于 if/elif 条件、&& 或 || 链中(见 3.3);失败的是否是管道且没开 pipefail(见 3.7);失败命令是否处于命令替换 $(...) 中——替换内部的失败同样不会直接触发 errexit。都不命中时再确认 -e 确实生效(echo $- 应含 e)。
Q2:交互终端里误执行 set -e 会怎样? 会话会被"下一失败的命令"直接关闭。实测在交互 bash 中执行 set -e 后再执行 false:
$ script -qec "bash -i" /dev/null < /root/demo/set/probe-input.txt
PS1='$ '
set -e
false
echo alive-after-false
exit
root@f1034b9326b2:/# PS1='$ '
$ set -e
$ false
前 5 行是回显的输入内容,$ false 之后既没有 alive-after-false 也没有新提示符——交互会话随 false 直接退出。所以在交互终端试验完 set -e 记得 set +e 恢复,或干脆放脚本里测。
Q3:set -x 的跟踪输出去哪了?重定向日志时为什么把正常输出也弄丢了?
-x 走 stderr,正常输出走 stdout。想只留正常输出用 bash -x script.sh 2>/dev/null,想只留跟踪用 bash -x script.sh 2>trace.log。另外跟踪前缀的 PS4 首字符按嵌套深度重复(见 3.6),++ 不是乱码。
Q4:set -u 报 unbound variable,可变量明明"存在"? 常见三种冤案:$10 被解析成 ${1}0,应写 ${10};数组元素本身未赋值(如 arr 只有下标 0,引用 arr[1]);以及通过可选项传入的变量确实没传。区分"必须存在"与"可以为空",分别用 ${VAR:?提示} 和 ${VAR:-默认} 声明意图。
Q5:set -e 与 pipefail 为什么总是成对出现?
-e 只看"整条命令"的退出码,管道默认只返回最后一个命令的状态。只开 -e 时 失败命令 | 成功命令 依然 rc=0 不退出;补上 -o pipefail 后中间失败才暴露为非零并触发退出(见 3.15 的 strict.sh)。
Q6:set、env、export 三者怎么分工? set 管当前 shell 的选项开关与变量全景(含函数);env 在干净/指定环境中运行命令,只涉及导出变量;export 把变量标记为可被子进程继承。查看用 set/env,修改继承性用 export 或 set -a。
Q7:想一次性关掉严格模式恢复默认怎么办?
把 - 方向全部换成 +:set +euxo pipefail。也可以用 set +o 快照当前状态、改完再回放,实现"受影响的开关只改不改别的"。
六、参考链接
- GNU Bash 手册:The Set Builtin —— https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html
- bash(1) man 手册(man7.org)—— https://man7.org/linux/man-pages/man1/bash.1.html
- GNU Bash 手册:Shell Parameters(位置参数与特殊参数)—— https://www.gnu.org/software/bash/manual/html_node/Shell-Parameters.html
- Google Shell 风格指南(脚本头严格模式建议)—— https://google.github.io/styleguide/shellguide.html
- 本机速查:
help set、man bash(搜索set builtin)