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)的行为差异对比。

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

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 (继续执行到最后一条)

💡 要点:

  1. 第三、四组对比是关键:false 收尾返回 1;中间失败(false)但最后一条 echo 成功则整体返回 0——bash -c '命令1; 命令2' 不会因为命令1失败而停,这一点与 bash -e(3.3)组合使用才能实现「任一步失败即失败」。
  2. -c 串内可以用分号串联多条命令,引号内的变量在子 bash 进程里展开,与外层 shell 的变量同名也不互相污染(进程隔离)。
  3. -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

💡 要点:

  1. 跟踪行显示的是展开后的命令:echo 'hello wdb 1' 里的变量已被替换成实际值——排查「变量为什么是空/错值」时看这里一目了然。
  2. PS4 自定义实测通过「脚本内赋值 + set -x」生效(+L4:、+L5: 即行号前缀);实测中把 PS4 通过外部环境变量传入(PS4='...' bash -x 脚本)未生效,仍是默认 + 前缀——PS4 定制请写在脚本内或通过 BASH_ENV 方案加载。
  3. 生产排障常用组合: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

💡 要点:

  1. 不加 -e:false 之后继续执行 step2,最后一条命令成功所以整体 rc=0——失败被「冲淡」了。
  2. 加 -e:false 处立即退出,step2 不再执行,整体 rc=1——这就是部署脚本要的「快速失败」。
  3. 注意 -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

💡 要点:

  1. 四类封锁逐一实证:cd: restricted、PATH: readonly variable、cannot specify '/' in command names、cannot redirect output,各自的失败返回 1;普通变量赋值与内建命令不受限(rc=0)。
  2. 受限模式封锁的是「换目录、换 PATH、按路径执行、写文件」四条逃逸通道,不是安全边界——被限用户仍可执行 PATH 内的命令,运维上把它当「防误操作护栏」而非安全隔离。
  3. 受限 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

💡 要点:

  1. 加 -l 时探针 MARKER-PROFILE-LOADED 出现(profile 链被执行)、shopt -q login_shell 判定 IS-LOGIN;不加 -l 时两者皆无——加载行为与判定开关完全对应。
  2. -l -c 组合对 cron/远程单命令场景很有用:需要 PATH 等登录环境时,bash -l -c '命令' 等价于「先登录再执行」;而裸 bash -c 拿到的是精简环境,这是「手动跑成功、cron 里失败」的常见根因。
  3. 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=[]

💡 要点:

  1. -l + --noprofile:探针 MARKER 未出现(与 3.5 对照),profile 被跳过。
  2. -i + --norc:bashrc-marker 为空(bashrc 未加载),不加 --norc 时为 yes——-i 让非终端进程也按交互 shell 初始化(读 ~/.bashrc),--norc 精确关掉这一步。
  3. 两条 cannot set terminal process group / no job control in this shell 告警:-i 在没有终端的环境下必然出现,属预期行为而非故障;它们也实证了「交互 shell 尝试接管终端进程组与作业控制」这条启动路径。
  4. 实用场景:排查「是不是配置文件搞坏了环境」时,用 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

💡 要点:

  1. bash -s 参数...:-s 之后的第一个参数是 $1(不是 $0,脚本名场景 bash 会用自身路径补位);第二组与第三组输出完全一致——-s 与 -c 在「脚本 + 位置参数」语义上等价,区别只在脚本从 stdin 还是参数串来。
  2. 典型用途: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]

💡 要点:

  1. 进程证据:bash 脚本 的 pid=28253 ≠ 外层 28245(新进程);source 的 pid=28245 与外层相同(当前 shell 内联执行)。实测环境的演示 shell 由外部程序直接启动、父进程不可见,故 source 回显中 ppid=0,真实登录会话中这里会显示父 shell 的 PID。
  2. 变量证据:子进程方式执行后 wdb_val 仍是 outer-init(子进程的赋值随进程消失);source 后变成 sourced-value(写进了当前 shell)。让工具/环境生效要用 source(如 source ~/.bashrc),要隔离就用子进程。
  3. 退出语义证据:两者都返回 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

💡 要点:

  1. SHLVL 每深一层 bash 加 1(实测 1→2→3),可用它判断「我在第几层 shell」;回到原 shell 后值不变(变量属于各自进程)。
  2. $$ 与 BASHPID:bash -c 的主进程里两者相同(28503,此时 bash 就是该进程);子 shell(括号)里才显出差异——$$ 保持父 shell 的 28522,BASHPID 是子 shell 自己的 28528。判断「代码是否在子 shell 里跑」,比较 $$ 与 $BASHPID 即可。
  3. 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]}=2Syntax error: "(" unexpected,rc=2
花括号展开 {1..3}展开为 1 2 3原样输出 {1..3}
here-string <<<支持,rc=0Syntax 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 即可。

六、参考链接