Shell脚本精读 · S02-03 | 词拆分、通配符与未加引号的变量

模块 :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 与通配(常见正确用法))
      • [4. `var\` vs \`"var"`:经典对照](#4. $var vs "$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 -uset -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 -uset -e 的联动

组合 风险
set -u + $maybe_empty 未定义报错;用 ${maybe_empty:-}
未引号空 glob + cp/rm 参数列表变短,误操作

读脚本检查清单

  • 变量、路径、URL 是否 "$..."
  • $*$@ 是否写成 "$@"(S03-03)?
  • for x in $var 是否应改为 while read 或数组?
  • *.ext故意通配还是误写在未引号变量里?
  • 空变量参与 rm/cp/mv 前是否判断或引号?
  • [test 内变量是否引号?

练习

判断题

  1. echo "$HOME"echo $HOME 在路径无空格时一定输出相同。
  2. files="*.txt"; echo $files 可能输出多个文件名。
  3. IFS 只影响 for 循环,不影响普通命令参数。
  4. 双引号内的 * 永远不会被当作通配符展开。

参考答案

  1. 对(仍推荐引号)。
  2. 对。
  3. 错。
  4. 对。

实操题 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》--- 从「一行怎么解析」进入「变量怎么存、怎么传给子进程」。

相关推荐
暮云星影20 小时前
全志linux开发屏幕适配(一)屏幕参数设置说明
linux·arm开发
swordbob21 小时前
NIO 的 Channel 里有多个 BIO 吗?
linux·网络·nio
Fcy6481 天前
Linux下 信号的保存与捕捉
linux·中断·信号的捕捉·信号的保存
A_humble_scholar1 天前
Linux(九) 进程管理完全指南:从入门到实战
linux·运维·chrome
江华森1 天前
Linux 操作命令完全指南
linux·运维
rjszcb1 天前
Linux,sensor调试笔记1,修改帧率,以及曝光上不去问题
linux
C++ 老炮儿的技术栈1 天前
Ubuntu root账号自动登陆
linux·运维·服务器·c语言·c++·ubuntu·visual studio
2301_780789661 天前
零信任架构中,身份感知防火墙(IAFW)的部署要点与最佳实践
linux·运维·服务器·人工智能·tcp/ip·架构
小狮子&1 天前
ubuntu2604无法共享文件夹问题解决
linux·运维·服务器
biter down1 天前
3:VMware Workstation 安装 Ubuntu 22.04 超详细教程
linux·运维·ubuntu