Shell脚本精读 · S09-03 | 子 Shell 与命令替换:`()` 与 `$()`
2026/7/8 12:32:02 网站建设 项目流程

模块:S09 进程、管道与重定向
篇号:S09-03 / 42
预计阅读:50 分钟
主线:Bash


文章目录

    • 本篇目标
    • 30 秒速览
    • 正文
      • 1. 三种括号,别混了
      • 2. 命令替换:`$(...)`
      • 3. 旧式反引号:`` `...` ``
      • 4. 引号与词拆分
      • 5. 末尾换行会被去掉
      • 6. 子 shell:`( ... )`
      • 7. 命令替换也在子进程里跑
      • 8. 经典模式:`SCRIPT_DIR`
      • 9. 退出码与 `set -e`
      • 10. 嵌套与多行
      • 11. 与管道子 shell 的对照
      • 12. 读脚本检查清单
    • 练习
      • 判断题
      • 实操题
      • 改错题
    • 下一篇预告

本篇目标

掌握命令替换$(命令)与旧式`命令`,以及子 shell( 命令 )。能区分$( )$(( ))( ){ },理解「输出当文本」「子进程里改的变量父 shell 看不见」,并读懂SCRIPT_DIR=...for x in $(...)等高频写法及其坑。


30 秒速览

  • $(cmd):执行cmd,用其stdout 文本替换到命令行(命令替换)。
  • `cmd`:与$(cmd)等价,难嵌套、难读,新脚本用$()
  • ( cmd )子 shell分组,cd、赋值一般不影响父 shell。
  • $(( expr ))算术展开,不是命令替换(S05-04)。
  • 替换结果未加引号会再经历词拆分(S02-03)。
  • 末尾换行:命令替换会去掉 stdout尾部换行(仅尾部)。

正文

1. 三种括号,别混了

写法名称作用
$(cmd)命令替换跑命令,输出字符串插进命令行
$((n+1))算术展开算整数,不跑外部命令
( cmd )子 shell子进程里跑一组命令
{ cmd; }命令组当前 shell,非子进程(需;或换行)
echo"$(date+%F)"# 命令替换:2026-05-19echo$((1+2))# 算术:3(cd/tmp;pwd)# 子 shell 里 cdpwd# 仍在原目录

2. 命令替换:$(...)

today=$(date+%Y-%m-%d)files=$(ls*.sh2>/dev/null)echo"today=$today"

过程

  1. 启动date(子 shell / 子进程环境执行)
  2. 收集stdout成一段文本
  3. 替换$(...)所在位置

stderr 不会进入替换结果(除非2>&1合并进 stdout):

out=$(greppattern missing2>/dev/null)# out 为空,grep 失败退出码 1 不影响赋值本身是否成功(见下文 set -e)

3. 旧式反引号:`...`

today=`date+%Y-%m-%d`

$(date +%Y-%m-%d)等价。
缺点:嵌套时引号易乱,可读性差:

# 难读echo`echo\`date\``# 清晰echo$(echo$(date))

S02-01:双引号内两种都会展开;单引号内都不展开。


4. 引号与词拆分

path=$(echo"/my dir/file")cp$path# 错:拆成 /my dir/filecp"$path"# 对forfin$(ls*.log);doecho"$f"done

$(ls *.log)未加引号→ 输出按IFS拆成多个单词(S07-01、S02-03)。
路径含空格、换行时会出错。

更稳

forfin*.log;do...done# 或 mapfile/readarray

原则:命令替换结果默认当字符串用就加引号"$(cmd)"


5. 末尾换行会被去掉

$printf'a\nb\n'|od-c# 有末尾 \n$x=$(printf'a\nb\n');echo"$x"|od-c# 替换结果往往去掉**尾部连续换行**(保留中间换行)

读脚本:$(wc -l < f)等单行数字没问题;若要保留末尾空行,别依赖命令替换,用重定向 / 文件


6. 子 shell:( ... )

count=0(count=1;echo"inner=$count")echo"outer=$count"# outer=0
特性子 shell( )当前 shell{ }
cd不影响父目录影响
变量赋值不影响父 shell影响
速度略慢(fork)
管道右侧常在子 shell(S07-02、S09-02)

临时切换目录

(cd"$subdir"&&make)# 父 shell 仍留在原目录

分组重定向(S09-01 提过):

{cmd1|cmd2;}>all.log2>&1

{ cmd; }不是子 shell,cd会留在当前 shell——别与( )混用。


7. 命令替换也在子进程里跑

outer=1inner=$(outer=2;echo$outer)echo"outer=$outerinner=$inner"# outer=1 inner=2

$( )里的赋值不会改父 shell 变量(与| while read类似,都是子进程问题)。


8. 经典模式:SCRIPT_DIR

SCRIPT_DIR="$(cd "$(dirname"${BASH_SOURCE[0]}")"&&pwd)"
部分作用
dirname "${BASH_SOURCE[0]}"当前脚本所在目录(相对或绝对)
cd ... && pwd子 shell里 cd,得到绝对路径
外层$( )把 pwd 输出赋给SCRIPT_DIR

父 shell工作目录不变(S08-03)。


9. 退出码与set -e

set-euopipefailx=$(false)# false 非 0 → 在 Bash + set -e 下,脚本可能在此退出

命令替换失败(内部命令非 0)在set -e下常被视为失败。
需要容忍时:

x=$(greppattern f||true)ifout=$(may_fail);thenecho"ok:$out"fi

$?after assignment:检查的是赋值整条还是内部命令,读脚本时注意 Bash 版本与set -e细节;稳妥写法用if out=$(cmd); then


10. 嵌套与多行

host=$(curl-fsS"$url"|jq-r'.host')

内层可以是管道(S09-02);$?/PIPESTATUS在赋值后要立刻查。

多行可读性:

hash=$(openssl dgst-sha256"$file"\|awk'{print $2}')

11. 与管道子 shell 的对照

场景子进程?父 shell 见变量?
var=$(cmd)只见var字符串,不见 cmd 里其它赋值
cmd1 | cmd2右侧常是右侧循环里赋值不见
( cmd )不见
{ cmd; }

12. 读脚本检查清单

  • $( )还是$(( ))还是( )
  • $(...)是否加了"..."防拆词?
  • 是否用`...`嵌套难读代码?
  • cd( )还是裸写误改全局目录?
  • for x in $(...)是否该改成 glob /while read

练习

判断题

  1. `date`$(date)在 Bash 中通常等价。
  2. ( cd /tmp )之后,父 shell 的当前目录变为/tmp
  3. $((1+2))会执行名为1+2的命令。
  4. x=$(a=1; echo 2)执行后,父 shell 中a为 1。
  5. 命令替换会捕获命令的 stderr 作为替换文本。
参考答案
  1. 对。
  2. 错(子 shell 里 cd)。
  3. 错(算术展开)。
  4. 错(a=1在子进程)。
  5. 错(默认只 stdout;stderr 仍到终端或需2>&1)。

实操题

下面输出什么?

x=10(x=20;y=30)echo"x=$xy=${y:-unset}"z=$(x=40;echo50)echo"x=$xz=$z"
参考答案
x=10 y=unset x=10 z=50

改错题

#!/usr/bin/env bashset-euopipefailforuserin$(getentpasswd|cut-d:-f1);doechouser=$userdone
参考

用户名含空格时会拆错。可改为:

whileIFS=:read-ruser _;doecho"user=$user"done<<(getentpasswd)

或保证字段解析用read,不要for ... in $(...)拆整表输出。


下一篇预告

S09-04:《后台任务、wait与进程替换》—cmd &wait $!<(cmd)进程替换。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询