bash 命令详解与参数实测(Ubuntu 22.04)
以 Ubuntu 22.04(bash 5.1.16)实测逐参数讲解 bash 本体:-c 退出码语义、-x 调试与 PS4、-e 出错即退、-r 受限模式、-l 登录 shell、--noprofile/--norc、-s 读 stdin、脚本执行与 source 差异、BASH_VERSION/SHLVL/BASHPID 环境变量,并给出与 dash(/bin/sh)的行为差异对比。
bash 命令详解与参数实测(Ubuntu 22.04)
目标:全面掌握 bash 二进制本体(/bin/bash)的启动参数与行为边界——
-c执行命令串的退出码语义、-x跟踪调试与 PS4 定制、-e出错即退、-r受限模式的四类封锁实测、-l登录 shell 的 profile 加载、--noprofile/--norc的精确跳过范围、-s从标准输入执行、bash 脚本与source的进程级差异,以及 BASH_VERSION/SHLVL/BASHPID 等环境变量的实测验证,最后与 /bin/sh 指向的 dash 做逐项行为对比,全部命令与回显已在 Ubuntu 22.04(bash 5.1.16)环境实测通过。
一、环境信息
本文所有命令均在以下环境实测,回显逐字照抄:
- 操作系统:Ubuntu 22.04.5 LTS(
PRETTY_NAME="Ubuntu 22.04.5 LTS") - 内核架构:x86_64
- 被测对象:GNU bash, version 5.1.16(1)-release(x86_64-pc-linux-gnu),二进制 /bin/bash(→ /usr/bin/bash,usrmerge 合并布局)
- 软件包版本:bash 5.1-6ubuntu1.1;对比参照 dash 0.5.11+git20210903+057cd650a4ed-3build1(/bin/sh 的指向)
- 执行身份:root
- 演示目录:
/root/demo/bash/(保存本文实测脚本 bash_0.sh ~ bash_11.sh 与演示脚本 wdb-debug.sh、wdb-errexit2.sh 等,每个参数小节均可独立复现) - 实测方式:每条实测写成脚本执行并完整留存回显;
bash -i相关实验在非终端环境下执行,其固有的终端告警一并照抄并解释
二、命令概览
2.1 语法与测试数据
bash 既是交互 shell,也是一个「解释器」型命令:可以执行单条命令串(-c)、执行脚本文件、从标准输入读脚本(-s),并以一组启动选项控制其初始化行为(读不读 profile、开不开受限模式等)。语法(来自实测 bash --help):
Usage: bash [GNU long option] [option] ...
bash [GNU long option] [option] script-file ...
本文覆盖的参数与选项:
-c 命令串:把字符串当脚本执行,其后第一个参数成为 $0;-x:跟踪每条展开后的命令(调试利器,配合 PS4 定制前缀);-e:任一命令失败即退出(快速失败语义);-r/--restricted:受限模式,封锁 cd、改 PATH、绝对路径命令、输出重定向等;-l/--login:按登录 shell 启动,加载 /etc/profile 与 ~/.profile;--noprofile/--norc:分别跳过 profile 与 ~/.bashrc 的读取;-s:从标准输入读脚本,-s之后的参数成为位置参数。
本文测试数据约定:演示脚本统一放在 /root/demo/bash/,以 wdb- 前缀命名(wdb-debug.sh、wdb-errexit2.sh、wdb-child.sh、wdb-sourced.sh);环境变量实验通过纯 bash 进程链(脚本层 → bash -c 层 → 内层)逐级取值,避免环境干扰。
💡 技术拓展:执行 shell 脚本有三条入口路径——
bash 脚本(子进程解释执行)、source 脚本(当前 shell 内执行)、./脚本(依赖 shebang 决定解释器,本质同第一条)。本文实际采用 bash 参数主线,3.8 节用同一组脚本实测前两条的进程与变量差异;理解这三条路径是排查「变量为什么不生效」类问题的基础。
2.2 程序本体与 bash --help 全量输出(逐行照抄)
先确认程序归属与布局,再取全量帮助——这是逐参数实测的地图:
$ type -a bash
bash is /usr/bin/bash
bash is /bin/bash
$ ls -l /bin/bash /usr/bin/bash
-rwxr-xr-x 1 root root 1396520 Mar 14 2024 /bin/bash
-rwxr-xr-x 1 root root 1396520 Mar 14 2024 /usr/bin/bash
$ bash --help
GNU bash, version 5.1.16(1)-release-(x86_64-pc-linux-gnu)
Usage: bash [GNU long option] [option] ...
bash [GNU long option] [option] script-file ...
GNU long options:
--debug
--debugger
--dump-po-strings
--dump-strings
--help
--init-file
--login
--noediting
--noprofile
--norc
--posix
--pretty-print
--rcfile
--restricted
--verbose
--version
Shell options:
-ilrsD or -c command or -O shopt_option (invocation only)
-abefhkmnptuvxBCHP or -o option
Type `bash -c "help set"' for more information about shell options.
Type `bash -c help' for more information about shell builtin commands.
Use the `bashbug' command to report bugs.
bash home page: <http://www.gnu.org/software/bash>
General help using GNU software: <http://www.gnu.org/gethelp/>
关键信息解读:GNU long options 区列出了全部长选项(--login/--noprofile/--norc/--restricted 等,与短选项一一对应);Shell options 区一行 ilrsD or -c command 概括了短选项组,其中 -c 标注 invocation only——只能在启动时给,shell 运行起来后不可追加。
💡 说明:注意 --help 首行的版本串是
5.1.16(1)-release-(x86_64-pc-linux-gnu)(release 后是连字符),与--version输出的5.1.16(1)-release (x86_64-pc-linux-gnu)(空格)措辞不同,这是 bash 自身输出的小怪癖,照抄自实测。bash 也没有「打印单个参数帮助」的机制,全量 help 即上文。
2.3 版本信息与 /bin/sh 指向
$ bash --version | head -1
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)
$ dpkg -S /bin/bash
bash: /bin/bash
$ dpkg -l bash | tail -1
ii bash 5.1-6ubuntu1.1 amd64 GNU Bourne Again SHell
$ ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Mar 23 2022 /bin/sh -> dash
$ dpkg -S /bin/dash
dash: /bin/dash
$ dpkg -l dash | tail -1
ii dash 0.5.11+git20210903+057cd650a4ed-3build1 amd64 POSIX-compliant shell
💡 要点:Ubuntu 22.04 的 /bin/sh 指向 dash 而非 bash——这是 3.10 对比实验的环境前提,也是 #!/bin/sh 脚本在 Ubuntu 上与 #!/bin/bash 行为不一致的根源。跨发行版脚本必须显式声明需要的解释器。
三、参数逐项实测
3.1 bash -c:执行命令串与退出码语义
-c 让 bash 把后面的字符串当作一段脚本执行,退出码 = 字符串内最后一条命令的退出码(显式 exit 除外)。四种典型情形实测:
bash -c 'echo hello-from-bash-c'; echo "rc=$? (正常)"
bash -c 'exit 42'; echo "rc=$? (exit 指定)"
bash -c 'false'; echo "rc=$? (最后一条命令 false)"
bash -c 'echo ok; false; echo after-false'; echo "rc=$? (继续执行到最后一条)"
实测回显:
hello-from-bash-c
rc=0 (正常)
rc=42 (exit 指定)
rc=1 (最后一条命令 false)
ok
after-false
rc=0 (继续执行到最后一条)
💡 要点:
- 第三、四组对比是关键:
false收尾返回 1;中间失败(false)但最后一条 echo 成功则整体返回 0——bash -c '命令1; 命令2'不会因为命令1失败而停,这一点与bash -e(3.3)组合使用才能实现「任一步失败即失败」。 -c串内可以用分号串联多条命令,引号内的变量在子 bash 进程里展开,与外层 shell 的变量同名也不互相污染(进程隔离)。-c串之后的参数按$0、$1、$2... 传入,见 3.7 的等价形态实测。
3.2 bash -x:跟踪执行与 PS4 定制
-x 把每条命令「展开后的最终形态」打印到 stderr,前缀由 PS4 变量控制(默认 + )。先看默认形态,演示脚本 wdb-debug.sh 内容为 name=wdb; for i in 1 2; do echo "hello $name $i"; done:
bash -x /root/demo/bash/wdb-debug.sh
实测回显:
+ name=wdb
+ for i in 1 2
+ echo 'hello wdb 1'
hello wdb 1
+ for i in 1 2
+ echo 'hello wdb 2'
hello wdb 2
在脚本内自定义 PS4(加行号前缀),脚本 wdb-debug2.sh 开头为 PS4='+L$LINENO: ' 加 set -x:
bash /root/demo/bash/wdb-debug2.sh
实测回显:
+L4: name=wdb
+L5: echo 'hello wdb'
hello wdb
💡 要点:
- 跟踪行显示的是展开后的命令:
echo 'hello wdb 1'里的变量已被替换成实际值——排查「变量为什么是空/错值」时看这里一目了然。 - PS4 自定义实测通过「脚本内赋值 + set -x」生效(
+L4:、+L5:即行号前缀);实测中把 PS4 通过外部环境变量传入(PS4='...' bash -x 脚本)未生效,仍是默认+前缀——PS4 定制请写在脚本内或通过 BASH_ENV 方案加载。 - 生产排障常用组合:
bash -x 脚本 2> wdb-trace.log,把跟踪流与正常输出分离,避免日志混杂。
3.3 bash -e:出错即退(快速失败)
-e 开启 errexit:任一命令返回非 0 就立即终止脚本。用同一个脚本对比加与不加(脚本 wdb-errexit2.sh 内容为 echo step1; false; echo step2):
bash /root/demo/bash/wdb-errexit2.sh; echo "rc=$?"
bash -e /root/demo/bash/wdb-errexit2.sh; echo "rc=$?"
实测回显:
step1
step2
rc=0
step1
rc=1
💡 要点:
- 不加
-e:false 之后继续执行 step2,最后一条命令成功所以整体 rc=0——失败被「冲淡」了。 - 加
-e:false 处立即退出,step2 不再执行,整体 rc=1——这就是部署脚本要的「快速失败」。 - 注意
-e的豁免名单:if 条件、&&/||链中非末位的命令、while/until条件处的失败不会触发退出;对「允许失败的探测命令」要显式写成命令 || true。
3.4 bash -r:受限模式(四类操作全部被拒)
-r(--restricted)启动一个「沙笼」shell:cd、修改 PATH/SHELL 等关键变量、执行含斜杠的命令名、输出重定向全部拒绝。逐项实测:
bash -r -c 'cd /tmp' 2>&1; echo "cd rc=$?"
bash -r -c 'PATH=/tmp' 2>&1; echo "改PATH rc=$?"
bash -r -c 'SHELL=/bin/sh' 2>&1; echo "改SHELL rc=$?"
bash -r -c 'wdbvar=1' 2>&1; echo "普通变量赋值 rc=$?"
bash -r -c '/bin/echo abs-ok' 2>&1; echo "绝对路径命令 rc=$?"
bash -r -c 'echo relative > /tmp/wdb-x.txt' 2>&1; echo "重定向 rc=$?"
bash -r -c 'echo plain-ok' 2>&1; echo "内建 echo rc=$?"
实测回显:
bash: line 1: cd: restricted
cd rc=1
bash: line 1: PATH: readonly variable
改PATH rc=1
bash: line 1: SHELL: readonly variable
改SHELL rc=1
普通变量赋值 rc=0
bash: line 1: /bin/echo: restricted: cannot specify `/' in command names
绝对路径命令 rc=1
bash: line 1: /tmp/wdb-x.txt: restricted: cannot redirect output
重定向 rc=1
plain-ok
内建 echo rc=0
对照组(非受限 bash 同样操作全部成功):
cd rc=0
abs-ok
绝对路径命令 rc=0
💡 要点:
- 四类封锁逐一实证:
cd: restricted、PATH: readonly variable、cannot specify '/' in command names、cannot redirect output,各自的失败返回 1;普通变量赋值与内建命令不受限(rc=0)。 - 受限模式封锁的是「换目录、换 PATH、按路径执行、写文件」四条逃逸通道,不是安全边界——被限用户仍可执行 PATH 内的命令,运维上把它当「防误操作护栏」而非安全隔离。
- 受限 shell 通常配合
chsh -s /bin/rbash 用户或 SSH 的 ForceCommand 场景使用,给受控账号一个只能跑白名单命令的环境。
3.5 bash -l:登录 shell 加载 profile(shopt login_shell 判定)
-l(--login)按登录 shell 启动:加载 /etc/profile 及 ~/.profile。用探针文件实证加载差异(/etc/profile.d/wdb-marker.sh 内容为 echo MARKER-PROFILE-LOADED;本机 /root/.profile 默认内容会在 $BASH 存在时 source ~/.bashrc):
$ cat /root/.profile
# ~/.profile: executed by Bourne-compatible login shells.
if [ "$BASH" ]; then
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
fi
mesg n 2> /dev/null || true
bash -l -c 'echo login-done'
echo '--- 上面对比:不加 -l ---'
bash -c 'echo nonlogin-done'
bash -l -c 'shopt -q login_shell && echo IS-LOGIN || echo NOT-LOGIN'
bash -c 'shopt -q login_shell && echo IS-LOGIN || echo NOT-LOGIN'
实测回显:
MARKER-PROFILE-LOADED
login-done
--- 上面对比:不加 -l ---
nonlogin-done
MARKER-PROFILE-LOADED
IS-LOGIN
NOT-LOGIN
💡 要点:
- 加
-l时探针 MARKER-PROFILE-LOADED 出现(profile 链被执行)、shopt -q login_shell判定 IS-LOGIN;不加-l时两者皆无——加载行为与判定开关完全对应。 -l -c组合对 cron/远程单命令场景很有用:需要 PATH 等登录环境时,bash -l -c '命令'等价于「先登录再执行」;而裸bash -c拿到的是精简环境,这是「手动跑成功、cron 里失败」的常见根因。- profile 链的顺序:/etc/profile(及 /etc/profile.d/*.sh)→ ~/.profile(Ubuntu 默认在 $BASH 存在时再 source ~/.bashrc)。
3.6 bash --noprofile 与 --norc:精确控制初始化跳过范围
两个长选项分别跳过两类初始化文件:--noprofile 跳过 /etc/profile 与 ~/.profile;--norc 跳过交互 shell 的 ~/.bashrc。注意各自的作用范围不同——--noprofile 影响登录 shell,--norc 影响交互 shell。实测:
bash --noprofile -l -c 'echo noprofile-login-done'
bash -ic 'echo "bashrc-marker=[$WDB_RC_MARKER]"' 2>&1
bash --norc -ic 'echo "bashrc-marker=[$WDB_RC_MARKER]"' 2>&1
实测回显(前序实验已在 /root/.bashrc 放入 export WDB_RC_MARKER=yes 探针;两条 -i 命令在非终端环境下固有的告警一并照抄):
noprofile-login-done
bash: cannot set terminal process group (28207): Inappropriate ioctl for device
bash: no job control in this shell
bashrc-marker=[yes]
bash: cannot set terminal process group (28207): Inappropriate ioctl for device
bash: no job control in this shell
bashrc-marker=[]
💡 要点:
-l+--noprofile:探针 MARKER 未出现(与 3.5 对照),profile 被跳过。-i+--norc:bashrc-marker 为空(bashrc 未加载),不加 --norc 时为 yes——-i让非终端进程也按交互 shell 初始化(读 ~/.bashrc),--norc精确关掉这一步。- 两条
cannot set terminal process group/no job control in this shell告警:-i在没有终端的环境下必然出现,属预期行为而非故障;它们也实证了「交互 shell 尝试接管终端进程组与作业控制」这条启动路径。 - 实用场景:排查「是不是配置文件搞坏了环境」时,用
bash --noprofile --norc起一个干净 shell 做最小环境对照实验。
3.7 bash -s:从标准输入读脚本与位置参数
-s 让 bash 从 stdin 而非文件读脚本;-s 之后的参数按位置传给这段脚本($1、$2...),与 -c 的传参形态互为等价:
printf '%s\n' 'echo from-stdin' 'pwd' | bash -s
printf '%s\n' 'echo "args: 1=$1 2=$2"' | bash -s wdbA wdbB
bash -c 'echo "args: 1=$1 2=$2"' bash wdbA wdbB
实测回显:
from-stdin
/
args: 1=wdbA 2=wdbB
args: 1=wdbA 2=wdbB
💡 要点:
bash -s 参数...:-s之后的第一个参数是$1(不是 $0,脚本名场景 bash 会用自身路径补位);第二组与第三组输出完全一致——-s与-c在「脚本 + 位置参数」语义上等价,区别只在脚本从 stdin 还是参数串来。- 典型用途:
curl ... | bash -s -- 参数这类远程脚本执行(--分隔远程脚本与本地参数);以及管道中间对文本流做一段 shell 逻辑处理。
3.8 bash 脚本 与 source:进程与变量的差异
同一个 shell 片段,用子进程执行与用 source 内联执行,进程身份与变量作用域完全不同。实测脚本对(wdb-child.sh 以 exit 5 结尾;wdb-sourced.sh 以 return 5 结尾):
$ cat /root/demo/bash/wdb-child.sh
#!/bin/bash
wdb_val=child-value
echo "child: pid=$$ ppid=$PPID"
exit 5
$ cat /root/demo/bash/wdb-sourced.sh
wdb_val=sourced-value
echo "sourced: pid=$$ ppid=$PPID"
return 5
echo "outer pid=$$"
wdb_val=outer-init
bash /root/demo/bash/wdb-child.sh
echo "rc=$? 当前shell wdb_val=[$wdb_val]"
source /root/demo/bash/wdb-sourced.sh
echo "rc=$? 当前shell wdb_val=[$wdb_val]"
实测回显:
outer pid=28245
child: pid=28253 ppid=28245
rc=5 当前shell wdb_val=[outer-init]
sourced: pid=28245 ppid=0
rc=5 当前shell wdb_val=[sourced-value]
💡 要点:
- 进程证据:
bash 脚本的 pid=28253 ≠ 外层 28245(新进程);source 的 pid=28245 与外层相同(当前 shell 内联执行)。实测环境的演示 shell 由外部程序直接启动、父进程不可见,故 source 回显中 ppid=0,真实登录会话中这里会显示父 shell 的 PID。 - 变量证据:子进程方式执行后
wdb_val仍是 outer-init(子进程的赋值随进程消失);source 后变成 sourced-value(写进了当前 shell)。让工具/环境生效要用 source(如source ~/.bashrc),要隔离就用子进程。 - 退出语义证据:两者都返回 5,但机制不同——脚本里写
exit 5配子进程执行是「子进程退出」;source 的文件里必须用return 5,若 source 的文件里执行了exit,整个当前 shell 会跟着退出(这是 source 最经典的翻车点)。source 还有点号别名(. 文件),POSIX 兼容写法。
3.9 环境变量实测:BASH_VERSION、BASH、SHLVL、BASHPID 与 $$
bash 注入的一组自省变量,脚本可用来做版本分支与进程判断:
echo "BASH_VERSION=$BASH_VERSION"
echo "BASH=$BASH"
echo "SHLVL=$SHLVL"
echo "shell-pid($$)=$$ BASHPID=$BASHPID"
echo "EUID=$EUID UID=$UID PPID=$PPID"
实测回显:
BASH_VERSION=5.1.16(1)-release
BASH=/usr/bin/bash
SHLVL=1
shell-pid($$)=28279 BASHPID=28279
EUID=0 UID=0 PPID=0
SHLVL 的三层递增与 BASHPID/$$ 的差异(用进程链验证:脚本层 → bash -c 层 → bash 脚本内层):
echo "L0(脚本层): SHLVL=$SHLVL"
bash -c 'echo "L1(bash -c): SHLVL=$SHLVL"'
bash /tmp/wdb-mid.sh # mid.sh 内容: bash /tmp/wdb-inner.sh; inner.sh 内容: echo "inner: SHLVL=$SHLVL"
echo '### BASHPID 与 $$ 的区别'
bash -c 'echo "in bash -c: pid($$)=$$ BASHPID=$BASHPID"'
( echo "subshell: pid($$)=$$ BASHPID=$BASHPID" )
实测回显:
L0(脚本层): SHLVL=1
L1(bash -c): SHLVL=2
inner: SHLVL=3
in bash -c: pid(28503)=28503 BASHPID=28503
outer-shell: pid(28522)=28522 BASHPID=28522
subshell: pid(28522)=28522 BASHPID=28528
💡 要点:
- SHLVL 每深一层 bash 加 1(实测 1→2→3),可用它判断「我在第几层 shell」;回到原 shell 后值不变(变量属于各自进程)。
$$与 BASHPID:bash -c 的主进程里两者相同(28503,此时 bash 就是该进程);子 shell(括号)里才显出差异——$$保持父 shell 的 28522,BASHPID 是子 shell 自己的 28528。判断「代码是否在子 shell 里跑」,比较$$与$BASHPID即可。- EUID/UID 用于权限分支(root 下均为 0);PPID 在本实测环境显示 0(父进程不在同一可见范围),真实登录会话中为父进程 PID。
3.10 技术拓展:bash 与 sh/dash 行为对比
Ubuntu 的 /bin/sh 指向 dash(2.3 实测),#!/bin/sh 脚本实际由 dash 解释。同一批语法分别用 bash -c 与 sh -c 执行,逐项实测差异:
echo '--- echo 转义 ---'
bash -c 'echo "a\nb"'
sh -c 'echo "a\nb"'
echo '--- [[ ]] ---'
bash -c '[[ 1 -eq 1 ]] && echo bash-double-bracket-ok'; echo "bash rc=$?"
sh -c '[[ 1 -eq 1 ]] && echo dash-double-bracket-ok'; echo "sh rc=$?"
echo '--- 数组 ---'
bash -c 'a=(1 2 3); echo ${a[1]}'; echo "bash rc=$?"
sh -c 'a=(1 2 3); echo ${a[1]}'; echo "sh rc=$?"
echo '--- 花括号展开 ---'
bash -c 'echo {1..3}'
sh -c 'echo {1..3}'
echo '--- here-string ---'
bash -c 'cat <<< hello'; echo "bash rc=$?"
sh -c 'cat <<< hello' 2>&1; echo "sh rc=$?"
echo '--- 两者都支持 ---'
bash -c 'echo $((1+2)) $(echo cmdsub)'
sh -c 'echo $((1+2)) $(echo cmdsub)'
实测回显:
--- echo 转义 ---
a\nb
a
b
--- [[ ]] ---
bash-double-bracket-ok
bash rc=0
sh: 1: [[: not found
sh rc=127
--- 数组 ---
2
bash rc=0
sh: 1: Syntax error: "(" unexpected
sh rc=2
--- 花括号展开 ---
1 2 3
{1..3}
--- here-string ---
hello
bash rc=0
sh: 1: Syntax error: redirection unexpected
sh rc=2
--- 两者都支持 ---
3 cmdsub
3 cmdsub
汇总对比表(全部来自上面实测):
| 特性 | bash 5.1.16 实测 | dash 0.5.11(/bin/sh)实测 |
|---|---|---|
echo "a\nb" | 原样输出 a\nb(不解释转义) | 解释转义,输出 a 换行 b |
echo -e | 按转义序列输出 | -e 被当普通文本打印(-e a) |
[[ 1 -eq 1 ]] | 支持,rc=0 | [[: not found,rc=127 |
数组 a=(1 2 3) | 支持,${a[1]}=2 | Syntax error: "(" unexpected,rc=2 |
花括号展开 {1..3} | 展开为 1 2 3 | 原样输出 {1..3} |
here-string <<< | 支持,rc=0 | Syntax error: redirection unexpected,rc=2 |
$()、$((算术)) | 支持 | 支持(POSIX 基本盘) |
💡 说明:迁移口诀——dash 兼容的写法只有 POSIX 子集:测试用
[ ]、分支用 case、算术用$(( ))、命令替换用$();[[ ]]、数组、{a..b}、<<<、process substitution、==模式匹配等都是 bash 扩展。脚本要跨 /bin/sh 环境,就用checkbashisms类工具扫一遍 bashism,或干脆 shebang 明确写#!/bin/bash。本文实际采用 bash 全量特性主线,因为系统默认交互 shell 就是 bash。
四、实战示例
4.1 场景一:用 bash -x 排查变量展开问题
现象:脚本输出与预期不符,怀疑变量没取到。用 -x 看每条命令的展开后形态(脚本 wdb-debug.sh:name=wdb; for i in 1 2; do echo "hello $name $i"; done):
bash -x /root/demo/bash/wdb-debug.sh
实测回显:
+ name=wdb
+ for i in 1 2
+ echo 'hello wdb 1'
hello wdb 1
+ for i in 1 2
+ echo 'hello wdb 2'
hello wdb 2
要点:+ 行是「实际执行的命令(变量已展开)」,普通行是命令输出。若变量为空,跟踪行会显示 echo 'hello ',一眼定位到赋值行缺失或拼写错误(赋值行自身也会以 + name=wdb 形式出现在跟踪里)。配合行号前缀(3.2 的 PS4=+L$LINENO: )可直接定位到源码行。
4.2 场景二:bash -e 让部署脚本快速失败
部署脚本多步串行,中间步骤失败必须立即停止,否则后续步骤会在坏状态上继续(以 wdb-errexit2.sh 为例,内容 echo step1; false; echo step2):
bash -e /root/demo/bash/wdb-errexit2.sh; echo "rc=$?"
实测回显:
step1
rc=1
要点:加 -e 后脚本在 false 处立即终止(step2 未执行、rc=1),调用方(CI/上层编排)拿到的非 0 退出码能正确触发回滚。可以再配合 set -o pipefail(-e 不管管道中间段的失败)与显式 命令 || { echo 失败原因; exit 1; } 输出上下文,构成完整的快速失败骨架。
4.3 场景三:从 /bin/sh 迁移到 bash 的兼容自检
脚本要从 #!/bin/sh(dash)改为享受 bash 扩展时,先跑一遍差异清单(全部来自 3.10 实测)确认语法假设:
sh -c '[[ 1 -eq 1 ]] && echo ok'; echo "sh rc=$?"
bash -c '[[ 1 -eq 1 ]] && echo ok'; echo "bash rc=$?"
实测回显:
sh: 1: [[: not found
sh rc=127
ok
bash rc=0
要点:dash 下 [[ 直接 127(内建都不存在)。迁移检查项:[[ ]]→[ ]、数组→空格分隔字符串+set --、{1..n}→seq、<<<→echo | 管道、echo 转义依赖→printf。反向(bash 脚本要兼容 sh)同理逐项降级;不确定目标解释器时,readlink -f /bin/sh 先确认指向。
五、注意事项与常见问题
Q1:bash -c 'a; b' 的退出码到底是什么?为什么我的失败被吞了?
等于最后一条命令的退出码(3.1 实测:中间 false、末尾 echo 成功 → 整体 0)。要「任一步失败即失败」,用 bash -ec 'a; b'(-e 的快速失败语义,3.3 实测)或改用 set -e 的脚本文件。
Q2:脚本第一行该写 #!/bin/sh 还是 #!/bin/bash? 取决于用没用 bash 扩展。Ubuntu 的 /bin/sh 是 dash(2.3 实测软链),[[ ]]、数组、{a..b}、<<< 等 bashism 在 dash 下全部报错(3.10 实测 127/2)。用了扩展就写 #!/bin/bash;纯 POSIX 脚本写 #!/bin/sh 换取更快的启动与更广的兼容。判断标准:最小化系统(可能没装 bash)倾向 POSIX,运维机倾向 bash。
Q3:source 和 bash 执行脚本,变量为什么一个生效一个不生效?
进程边界决定一切。bash 脚本 开新进程,里面的赋值随进程结束消失(3.8 实测 wdb_val 仍是 outer-init);source 在当前 shell 内联执行,赋值留在当前 shell(wdb_val 变为 sourced-value)。让 alias/函数/环境变量生效用 source;需要隔离副作用(怕脚本改坏当前环境)用子进程执行。source 的文件里用 return 而不是 exit(exit 会连当前 shell 一起退出)。
Q4:bash -x 输出太多没法看,怎么只关注关键行?
三层过滤:① 用 PS4 加行号前缀(PS4='+L$LINENO: ',写在脚本内,3.2 实测外部环境变量传入不生效);② -x 脚本 2> trace.log 落盘后 grep 关键字;③ 临时分段——只在怀疑的段落前后 set -x / set +x,缩小跟踪窗口。
Q5:bash -r 受限模式是安全沙箱吗?能防住什么? 不是安全边界。实测它只封四条通道:cd、改 PATH/SHELL、含 / 的命令名、输出重定向(3.4),且普通赋值与内建命令照常(rc=0)。用户仍可执行 PATH 内命令、仍可调用内建做大量操作。它适合给受控账号「防误操作」(rbash + 白名单 PATH + 只读 HOME),不能替代真正的隔离手段(权限、沙箱、最小化 shell 如 dash/rssh 类)。
Q6:非终端环境下跑 bash -i 报 cannot set terminal process group / no job control,是坏了吗?
没坏,是预期告警。-i 让 bash 按交互模式初始化,但没有终端可控时它拿不到终端进程组、开不了作业控制,于是打印这两行并继续工作(3.6 实测:告警之后命令照常执行、--norc 的对照变量也正确输出)。在脚本/流水线里误加 -i 会出现这两行,删掉 -i 即可。
六、参考链接
- bash 手册(Ubuntu 22.04 jammy)INVOCATION 与 RESTRICTED SHELL 段:https://manpages.ubuntu.com/manpages/jammy/en/man1/bash.1.html
- bash 参考手册(GNU 官方)Bash Startup Files / The Restricted Shell / Bash Variables:https://www.gnu.org/software/bash/manual/html_node/index.html
- dash 手册(/bin/sh 的真身,POSIX 子集行为):https://manpages.ubuntu.com/manpages/jammy/en/man1/dash.1.html
- Ubuntu Wiki——DashAsBinSh(/bin/sh→dash 迁移背景与 bashism 清单):https://wiki.ubuntu.com/DashAsBinSh
- 本机速查:
bash --help(启动选项)、man bash后搜索 INVOCATION、RESTRICTED SHELL、Bash Variables 关键字定位