开篇:一个真实的工作场景
一个常见的"删错文件"事故是这样发生的:
rm *.txt ~
运维本意是"删除当前目录所有 txt 文件",结果手滑多敲了一个空格——~ 被单独展开成了家目录,rm 把家目录整个删了。这起真实事故告诉我们:通配符的展开是 Shell 帮你做的,不是命令自己做的,理解"展开"发生在哪一步,是保命的技能。
再比如你想删除文件名里带空格的文件,直接写 rm file name.txt 会被拆成两个文件;想打印变量却写错引号,输出完全不是你想的那样。今天这篇文章,就把 Bash 的通配符、转义、引号和各类展开机制一次讲透,帮你彻底告别"玄学"命令行。
一、核心认知:先有"展开",后有执行
Bash 执行一条命令时,先做各种展开,再把展开后的结果交给命令。整个流程(简化版)是:
命令行输入
→ 大括号展开 {a,b}
→ 波浪号展开 ~
→ 参数展开 $var
→ 命令替换 $(cmd)
→ 算术展开 $((1+2))
→ 分词(按空白拆分)
→ 文件名展开(通配符 * ? [ ])
→ 交给命令执行
这个顺序解释了几乎所有"为什么这么写不行"的问题。比如 echo * 输出的是当前目录的文件列表,因为 * 在交付给 echo 之前就被 Shell 展开成了文件名。
二、通配符(文件名展开)
语法格式
命令 含通配符的模式
通配符一览
| 通配符 | 匹配规则 | 示例 | 匹配到的示例文件 |
|---|---|---|---|
* | 任意长度任意字符(含空) | *.log | a.log、app.log |
? | 恰好 1 个任意字符 | file?.txt | file1.txt、filea.txt |
[abc] | 方括号内任一字符 | file[12].txt | file1.txt、file2.txt |
[a-z] | 字符范围 | file[a-z].txt | filea.txt、filez.txt |
[!abc] | 排除括号内字符 | file[!0-9].txt | filea.txt(不含数字) |
{a,b,c} | 大括号展开(不是文件名匹配) | file{1,2}.txt | file1.txt、file2.txt |
实战示例与预期输出
示例 1:批量匹配并查看
假设当前目录有 a.log、app.log、a.txt:
ls *.log
预期输出:
a.log app.log
示例 2:方括号精确匹配
ls file[12].txt
预期输出(若只有 file1.txt 存在):
file1.txt
⚠️ 常见错误
- 没有匹配到文件时:Bash 默认把模式原样传给命令,导致
ls *.nonexist报"没有那个文件"。可以开启nullglob(见知识扩展)让不匹配时展开为空。 *不匹配隐藏文件:以.开头的文件需要显式写.*。rm *有风险:*会展开当前目录所有非隐藏文件,建议先echo *预览。
三、转义:让特殊字符"现出原形"
反斜杠 \ 让紧跟在后面的字符失去特殊含义,按字面处理。
语法格式
\特殊字符
示例与预期输出
示例:删除文件名带空格的文件
touch "my file.txt" # 先创建
rm my\ file.txt # 转义空格
ls # 确认已删除
示例:打印美元符号
echo "价格是 \$100"
预期输出:
价格是 $100
注意:
\$在双引号内转义了$,避免被当作变量引用。
四、引号:单引号、双引号、反引号
规则总览
| 引号 | 作用 | 是否展开变量/命令 | 典型用途 |
|---|---|---|---|
'...' 单引号 | 内部所有字符按字面处理 | ❌ 不展开 | 原样输出、含特殊字符的字符串 |
"..." 双引号 | 保留变量/命令展开,但阻止分词与通配符展开 | ✅ 展开 | 拼接带空格的变量 |
`...` 反引号 | 命令替换(已不推荐) | — | 被 $(...) 取代 |
示例与预期输出
示例 1:单引号 vs 双引号
name="linux"
echo '$name 和 $name'
echo "$name 和 $name"
预期输出:
$name 和 $name
linux 和 linux
示例 2:双引号防止分词
dir="/tmp/my dir"
ls "$dir" # 正确:作为一个整体
ls $dir # 错误:被拆成 /tmp/my 和 dir 两个参数
示例 3:命令替换的两种写法(推荐 $())
echo "今天是 $(date +%F)"
echo "旧写法:`date +%F`"
预期输出:
今天是 2026-09-16
旧写法:2026-09-16
$(...) 的优势:可嵌套、易读($(echo $(date))),强烈推荐。
引号嵌套速记
- 单引号里不能再放单引号,可以
'\''表示,但更推荐用双引号或改写法; - 双引号里可以放单引号:
echo "it's ok"✅; - 想输出
"本身:echo "\""或echo '"'。
五、变量展开的完整形态
除了最基本的 $var,还有一批控制默认值的高级写法,脚本里非常常用:
| 写法 | 含义 | 示例(var 未设置时) |
|---|---|---|
${var:-默认值} | 为空则用默认值(不赋值) | echo ${port:-8080} → 8080 |
${var:=默认值} | 为空则赋值并输出 | ${port:=8080} 后 $port 为 8080 |
${var:?报错信息} | 为空则报错退出 | ${host:?host 未设置} |
${#var} | 变量长度 | ${#name} |
${var:2:4} | 子串截取 | ${str:2:4} |
${var#模式} / ${var##模式} | 删除前缀(最短/最长) | ${file##*/} 取文件名 |
${var%模式} / ${var%%模式} | 删除后缀(最短/最长) | ${file%.txt} 去扩展名 |
${var/旧/新} | 替换第一个匹配 | ${url/http/https} |
示例与预期输出
示例:从路径提取文件名和目录
path="/var/log/nginx/access.log"
echo "文件名: ${path##*/}"
echo "目录: ${path%/*}"
echo "去扩展名: ${path%.log}"
预期输出:
文件名: access.log
目录: /var/log/nginx
去扩展名: /var/log/nginx/access
这些写法写脚本时能省大量 sed/awk 调用。
六、知识扩展:改变展开行为的 Shell 选项
Bash 提供一批开关,显著影响展开行为,全部可用 shopt -s 选项 开启:
| 选项 | 效果 | 示例 |
|---|---|---|
nullglob | 通配符无匹配时展开为空(而不是保留原样) | for f in *.png; do ... 不再拿到字面量 |
failglob | 无匹配时报错 | 适合脚本中严格检查 |
globstar | ** 递归匹配子目录 | ls **/*.md 匹配所有层级的 md |
nocaseglob | 文件名匹配忽略大小写 | ls *.TXT 也能匹配 txt |
dotglob | * 也匹配隐藏文件 | rm * 会波及隐藏文件,慎用 |
shopt -s globstar nullglob
ls **/*.conf # 递归找所有 conf
原理探究:为什么展开顺序重要?举个经典例子——echo $((2+3) * 5) 会报错,因为算术展开先于文件名展开完成,$((2+3)) 变成 5 后,5 * 5 被当作路径模式处理。理解"展开发生在命令执行前、且按固定顺序",很多怪现象都能解释。
七、注意事项与最佳实践
rm前先echo预览:echo *.log看清展开结果再删。- 含空格路径永远加双引号:
"$file"、"$dir"是脚本安全底线。 - 需要字面特殊字符用单引号:正则表达式、密码、带
$的字符串一律单引号。 - 命令替换用
$(...),反引号容易嵌套出错。 ~只在开头展开:echo ~/dir有效,echo a~输出原样。- 脚本里开
set -u(未定义变量报错),配合${var:?}防手滑。
总结
这一篇揭示了命令行"魔法"的真相:Shell 在命令执行前按固定顺序完成大括号展开、波浪号展开、参数展开、命令替换、算术展开、分词和文件名展开。通配符 * ? [] 负责文件名匹配,反斜杠负责转义,单引号完全字面、双引号保留展开但阻止分词。掌握展开顺序和引号规则,你就能写出安全、可预测的 Bash 命令,避免"删错文件"这类事故。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/m0_66083353/article/details/165631449



