模块: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"过程:
- 启动
date(子 shell / 子进程环境执行) - 收集stdout成一段文本
- 替换
$(...)所在位置
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?
练习
判断题
`date`与$(date)在 Bash 中通常等价。( cd /tmp )之后,父 shell 的当前目录变为/tmp。$((1+2))会执行名为1+2的命令。x=$(a=1; echo 2)执行后,父 shell 中a为 1。- 命令替换会捕获命令的 stderr 作为替换文本。
- 对。
- 错(子 shell 里 cd)。
- 错(算术展开)。
- 错(
a=1在子进程)。 - 错(默认只 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)进程替换。