模块 :S02 词法、引用与解析
篇号 :S02-03 / 42
预计阅读 :45 分钟
主线 :Bash
说明 :本篇为 S02 加厚篇,bug 示例与改错题偏多
文章目录
-
- 本篇目标
- [30 秒速览](#30 秒速览)
- 正文
-
- [1. 解析顺序:先展开,再拆分,再通配](#1. 解析顺序:先展开,再拆分,再通配)
- [2. 词拆分(word splitting)与 IFS](#2. 词拆分(word splitting)与 IFS)
-
- [2.1 修改 IFS(谨慎)](#2.1 修改 IFS(谨慎))
- [2.2 空变量与未加引号](#2.2 空变量与未加引号)
- [3. 路径名展开(通配符)](#3. 路径名展开(通配符))
-
- [3.1 双引号内不展开(字面 `*`)](#3.1 双引号内不展开(字面
*)) - [3.2 无匹配时的行为(Bash)](#3.2 无匹配时的行为(Bash))
- [3.3 以 `-` 开头的匹配结果](#3.3 以
-开头的匹配结果) - [3.4 `for` 与通配(常见正确用法)](#3.4
for与通配(常见正确用法))
- [3.1 双引号内不展开(字面 `*`)](#3.1 双引号内不展开(字面
- [4. `var\` vs \`"var"`:经典对照](#4.
$varvs"$var":经典对照) - [5. 经典 bug 集(读脚本时高频)](#5. 经典 bug 集(读脚本时高频))
-
- [Bug 1:路径含空格](#Bug 1:路径含空格)
- [Bug 2:` -n $var ` 与 test](#Bug 2:
[ -n $var ]与 test) - [Bug 3:`for x in lines\`](#Bug 3:`for x in lines`)
- [Bug 4:`touch files\` 当 files 为空](#Bug 4:`touch files` 当 files 为空)
- [Bug 5:URL、用户输入未引号](#Bug 5:URL、用户输入未引号)
- [6. 何时「故意」不加引号](#6. 何时「故意」不加引号)
- [7. 与 `set -u`、`set -e` 的联动](#7. 与
set -u、set -e的联动)
- 读脚本检查清单
- 练习
-
- 判断题
- [实操题 1:观察拆分](#实操题 1:观察拆分)
- [实操题 2:观察通配](#实操题 2:观察通配)
- [改错题 1](#改错题 1)
- [改错题 2](#改错题 2)
- [改错题 3](#改错题 3)
- [S02 模块小结](#S02 模块小结)
- 下一篇预告
本篇目标
弄清 Shell 在执行命令之前 对一行文本做的两件大事:词拆分(word splitting) 与 路径名展开(pathname expansion / 通配符) 。能解释 $var 与 "$var" 的差异,并养成「变量默认加双引号」的习惯。读完本篇,S02 模块收尾 ;下一模块 S03 进入变量与参数展开。
30 秒速览
- 词拆分 :未加引号的展开结果,会按
IFS(默认空格、Tab、换行)切成多个「词」,成为多个参数。 - 路径名展开 :未加引号的
*、?、[...]会匹配当前目录下的文件名,可能匹配不到(行为要小心)。 "$var":通常只要一个词(内容里的 IFS 字符不再拆分)。$var:可能变很多词,还可能触发通配符。- 空变量 / 无匹配 glob :在
set -u、未加引号时特别容易踩坑。 - 读脚本 :看到
$且外侧没有",优先警惕。
正文
1. 解析顺序:先展开,再拆分,再通配
对一条简单命令,可简化理解为(省略部分细节):
text
读入一行 → 引号/转义处理 → 参数/命令替换 $( ) 等展开
→ 词拆分(仅对「未加引号」的片段)
→ 路径名展开(仅对「未加引号」的片段)
→ 执行命令
因此:引号 (S02-01)决定某段文本要不要经过拆分和通配。
#mermaid-svg-kpPvStBrakLW1igX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kpPvStBrakLW1igX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kpPvStBrakLW1igX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kpPvStBrakLW1igX .error-icon{fill:#552222;}#mermaid-svg-kpPvStBrakLW1igX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kpPvStBrakLW1igX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kpPvStBrakLW1igX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kpPvStBrakLW1igX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kpPvStBrakLW1igX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kpPvStBrakLW1igX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kpPvStBrakLW1igX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kpPvStBrakLW1igX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kpPvStBrakLW1igX .marker.cross{stroke:#333333;}#mermaid-svg-kpPvStBrakLW1igX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kpPvStBrakLW1igX p{margin:0;}#mermaid-svg-kpPvStBrakLW1igX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kpPvStBrakLW1igX .cluster-label text{fill:#333;}#mermaid-svg-kpPvStBrakLW1igX .cluster-label span{color:#333;}#mermaid-svg-kpPvStBrakLW1igX .cluster-label span p{background-color:transparent;}#mermaid-svg-kpPvStBrakLW1igX .label text,#mermaid-svg-kpPvStBrakLW1igX span{fill:#333;color:#333;}#mermaid-svg-kpPvStBrakLW1igX .node rect,#mermaid-svg-kpPvStBrakLW1igX .node circle,#mermaid-svg-kpPvStBrakLW1igX .node ellipse,#mermaid-svg-kpPvStBrakLW1igX .node polygon,#mermaid-svg-kpPvStBrakLW1igX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kpPvStBrakLW1igX .rough-node .label text,#mermaid-svg-kpPvStBrakLW1igX .node .label text,#mermaid-svg-kpPvStBrakLW1igX .image-shape .label,#mermaid-svg-kpPvStBrakLW1igX .icon-shape .label{text-anchor:middle;}#mermaid-svg-kpPvStBrakLW1igX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kpPvStBrakLW1igX .rough-node .label,#mermaid-svg-kpPvStBrakLW1igX .node .label,#mermaid-svg-kpPvStBrakLW1igX .image-shape .label,#mermaid-svg-kpPvStBrakLW1igX .icon-shape .label{text-align:center;}#mermaid-svg-kpPvStBrakLW1igX .node.clickable{cursor:pointer;}#mermaid-svg-kpPvStBrakLW1igX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kpPvStBrakLW1igX .arrowheadPath{fill:#333333;}#mermaid-svg-kpPvStBrakLW1igX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kpPvStBrakLW1igX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kpPvStBrakLW1igX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kpPvStBrakLW1igX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kpPvStBrakLW1igX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kpPvStBrakLW1igX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kpPvStBrakLW1igX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kpPvStBrakLW1igX .cluster text{fill:#333;}#mermaid-svg-kpPvStBrakLW1igX .cluster span{color:#333;}#mermaid-svg-kpPvStBrakLW1igX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kpPvStBrakLW1igX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kpPvStBrakLW1igX rect.text{fill:none;stroke-width:0;}#mermaid-svg-kpPvStBrakLW1igX .icon-shape,#mermaid-svg-kpPvStBrakLW1igX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kpPvStBrakLW1igX .icon-shape p,#mermaid-svg-kpPvStBrakLW1igX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kpPvStBrakLW1igX .icon-shape .label rect,#mermaid-svg-kpPvStBrakLW1igX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kpPvStBrakLW1igX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kpPvStBrakLW1igX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kpPvStBrakLW1igX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 未引号的展开结果
按 IFS 词拆分
* ? 通配展开
命令的参数列表
\$var\
2. 词拆分(word splitting)与 IFS
IFS (Internal Field Separator)默认是空格、Tab、换行($' \t\n')。
只有未加双引号(也未被单引号包住)的展开结果,会按 IFS 被切成多个词。
bash
items="a b c"
echo [$items] # 可能 [a] [b] [c] 三个参数(无引号 → 拆分)
echo ["$items"] # [a b c] 一个词
bash
path="/tmp/my dir/file.txt"
wc -l $path # 错:拆成 /tmp/my 与 dir/file.txt
wc -l "$path" # 对:一个路径
2.1 修改 IFS(谨慎)
bash
old_ifs=$IFS
IFS=','
for x in $csv; do echo ">$x<"; done # csv="1,2,3" 时按逗号拆
IFS=$old_ifs
脚本里全局长期改 IFS 容易导致别的命令行为异常;若必须改,用完立即恢复(S01-04 已提示)。
2.2 空变量与未加引号
bash
set -u
extra=
echo ["$extra"] # [] 一个词,通常安全
files=
cp $files /backup/ # 无引号空展开 → cp 可能只剩 /backup/,危险!
cp "$files" /backup/ # 仍应显式判断是否有文件
3. 路径名展开(通配符)
未加引号的模式会由 Shell 按文件名匹配(pathname expansion):
| 模式 | 含义 |
|---|---|
* |
任意长度任意字符(不含 /) |
? |
单个字符 |
[abc] |
匹配 a、b、c 之一 |
[0-9] |
一位数字 |
bash
ls *.log # 当前目录所有 .log
rm report_2024-*.txt # 故意使用通配
3.1 双引号内不展开(字面 *)
bash
pattern="*.log"
echo $pattern # 可能列出所有 .log(先展开变量再通配)
echo "$pattern" # 字面 *.log
3.2 无匹配时的行为(Bash)
若模式没有匹配任何文件 ,Bash 中该词保持为字面字符串 (如字面 *.xyz),命令可能报「文件不存在」。
bash
ls *.this_pattern_matches_nothing
3.3 以 - 开头的匹配结果
bash
rm ./* # 更稳妥:加 ./ 前缀
rm -- * # 或 --
3.4 for 与通配(常见正确用法)
bash
for f in *.sh; do
echo "processing $f" # 循环变量要引号
done
若 *.sh 无匹配 ,Bash 下循环体仍可能执行一次,且 f 为字面 *.sh------可用 shopt -s nullglob 或先判断(S07-01)。
4. $var vs "$var":经典对照
| 写法 | 词拆分 | 通配符 | 典型用途 |
|---|---|---|---|
$var |
会 | 会 | 几乎只在「故意拆词 / 故意 glob」 |
"$var" |
不会 | 不会 | 默认应使用 |
"${var}" |
不会 | 不会 | 接后缀更清晰 |
bash
file="a*"
echo $file # 可能展开为所有 a 开头的文件名
echo "$file" # 字面 a*
5. 经典 bug 集(读脚本时高频)
Bug 1:路径含空格
bash
dir="/data/my project"
rm -rf $dir/* # 灾难:拆词 + 通配
rm -rf "$dir"/* # 对:引号保护路径;* 仍可能通配
Bug 2:[ -n $var ] 与 test
bash
[ -n $var ] # $var 空时可能语法错误
[ -n "$var" ] # 对
Bug 3:for x in $lines
bash
for x in $lines; do echo "$x"; done # 按空格拆,不是按行
# 按行读:S07-02 while read -r
Bug 4:touch $files 当 files 为空
bash
files=
touch $files # 可能 touch 无参 → 行为异常
touch ${files:?need files}
Bug 5:URL、用户输入未引号
含 &、空格时必须 "$url"。
6. 何时「故意」不加引号
| 场景 | 示例 |
|---|---|
| 故意 glob | for f in *.csv |
| 故意拆成多个参数 | 少见;更推荐数组(S03-04) |
默认规则 :先写 "$var",只有能说出「为什么要拆分 / glob」时才去掉引号。
7. 与 set -u、set -e 的联动
| 组合 | 风险 |
|---|---|
set -u + $maybe_empty |
未定义报错;用 ${maybe_empty:-} |
未引号空 glob + cp/rm |
参数列表变短,误操作 |
读脚本检查清单
- 变量、路径、URL 是否
"$..."? -
$*、$@是否写成"$@"(S03-03)? -
for x in $var是否应改为while read或数组? -
*.ext是故意通配还是误写在未引号变量里? - 空变量参与
rm/cp/mv前是否判断或引号? -
[、test内变量是否引号?
练习
判断题
echo "$HOME"与echo $HOME在路径无空格时一定输出相同。files="*.txt"; echo $files可能输出多个文件名。IFS只影响for循环,不影响普通命令参数。- 双引号内的
*永远不会被当作通配符展开。
参考答案
- 对(仍推荐引号)。
- 对。
- 错。
- 对。
实操题 1:观察拆分
bash
a="1 2 3"
printf '|%s|\n' $a
printf '|%s|\n' "$a"
实操题 2:观察通配
bash
touch a1 a2
p='a*'
printf '|%s|\n' $p
printf '|%s|\n' "$p"
rm -f a1 a2
改错题 1
bash
#!/usr/bin/env bash
set -euo pipefail
target=$1
find $target -name '*.log' -delete
参考
bash
target=${1:?usage: $0 <dir>}
find "$target" -name '*.log' -delete
改错题 2
bash
files=""
tar czf backup.tar.gz $files
说明风险并改为安全写法。
参考
先判断 files 非空,或使用数组传递文件列表;避免未引号的空展开传给 tar。
改错题 3
bash
if [ -n $1 ]; then
echo "got arg"
fi
参考
bash
if [ -n "${1:-}" ]; then
echo "got arg"
fi
S02 模块小结
| 篇号 | 能力 |
|---|---|
| S02-01 | 单引号、双引号、无引号 |
| S02-02 | # 注释、\ 转义与续行、here-doc 初识 |
| S02-03 | 词拆分、通配符、"$var" 习惯 |
下一篇预告
S03-01:《变量赋值、读取与只读/export》--- 从「一行怎么解析」进入「变量怎么存、怎么传给子进程」。