第 9 章 · 01 从命令到脚本:变量与展开 本节摘要:当你发现自己在反复敲同一串命令,就该把它写成脚本了。而把「一行命令」升级成「一段脚本」的第一块基石,就是变量。本节要讲透两件事:一是变量怎么赋值( ,等号两边不能有空格,这是新手最常见的报错源头);二是 Shell 的四种展开——参数展开 、命令替换 、算术展开 、波浪号展开 。更关键的是,你要建立一个心智模型:所有展开都在命令执行之前由 Shell 完成。理解了这一点,你就能解释为什么引号在 Shell 里如此重要——单引号挡住展开,双引号允许展开但挡住单词拆分,不加引号则任由 Shell 把值撕成碎片。这个模型是后续所有章节的地基。 内容来源:综合 Shell 脚本知识与原项目「Linux 命令大全」整理,套用体系化模板。
本节摘要:当你发现自己在反复敲同一串命令,就该把它写成脚本了。而把「一行命令」升级成「一段脚本」的第一块基石,就是变量。本节要讲透两件事:一是变量怎么赋值(
x=5,等号两边不能有空格,这是新手最常见的报错源头);二是 Shell 的四种展开——参数展开$x、命令替换$(cmd)、算术展开$((...))、波浪号展开~。更关键的是,你要建立一个心智模型:所有展开都在命令执行之前由 Shell 完成。理解了这一点,你就能解释为什么引号在 Shell 里如此重要——单引号挡住展开,双引号允许展开但挡住单词拆分,不加引号则任由 Shell 把值撕成碎片。这个模型是后续所有章节的地基。
内容来源:综合 Shell 脚本知识与原项目「Linux 命令大全」整理,套用体系化模板。
阅读完本节,你应当能够:
$x/${x}、命令替换 $(cmd)、算术展开 $((...))、波浪号展开 ~。你已经会用管道把几个命令串起来,比如 tar -czf backup.tar.gz /home/alice。但每次源目录变了,你就得重新敲一遍。脚本的第一步,就是把「会变的东西」抽成变量:
#!/usr/bin/env bash src=/home/alice dst=backup.tar.gz tar -czf "$dst" "$src"
从此你只要改 src 和 dst 两行,剩下的命令原样不动。这就是脚本的本质:把命令参数化,让一段命令能复用。
理解 Shell 最重要的一句话是:Shell 在执行任何命令之前,会先把这一行里所有能展开的东西展开。这个过程叫「展开」(expansion),它发生在命令真正运行之前。
你输入: echo $HOME Shell 展开:echo /home/alice ← 先把 $HOME 替换成它的值 Shell 执行:echo 程序拿到参数 /home/alice,打印出来
也就是说,echo 程序自己根本没见过 $HOME 这个符号——它收到的是 Shell 展开后的结果 /home/alice。命令本身永远只和「展开后的结果」打交道。
关键概念:展开是 Shell 的工作,不是命令的工作。
echo、ls、cp这些命令收到的永远是已经展开完毕的字符串。这个事实解释了 Shell 里几乎所有「奇怪行为」。
这是新手第一坑。在大多数语言里 x = 5 是赋值,但在 Shell 里:
x = 5 # 错!Shell 把它看成:执行命令 x,带参数 = 和 5 x=5 # 对!等号两边紧贴,Shell 才认它是赋值
原因正是上面的心智模型:Shell 把一行命令按空格切成「命令 + 参数」。x = 5 被 Shell 看成「运行名为 x 的命令,参数是 = 和 5」。而 x=5 中间没有空格,是一个整体,Shell 才识别出「哦,这是给变量 x 赋值」。
name=Alice # 赋值字符串 greeting="Hello, world" # 含空格的值必须加引号 count=10 # 赋值数字(本质上还是字符串) now=$(date +%F) # 赋值命令的结果 PATH="$PATH:/opt/bin" # 给已有变量追加内容
⚠️ 注意:赋值时
=左边只能是单个变量名,不能有$,不能有空格。$x=5是错的(Shell 会先把$x展开成空,变成=5,然后报错)。
$x 与 ${x}最基础的展开,把变量的值「填」进去。
name=Alice echo $name # Alice echo "Hello, $name" # Hello, Alice(双引号里照样展开) echo ${name} # Alice(花括号版,更明确) # 花括号的用处:把变量和紧跟的字符隔开 file=report echo $file.txt # 风险:Shell 可能想要变量 file.txt echo ${file}.txt # 安全:明确告诉 Shell 变量名是 file,后面 .txt 是字面量
💡 技巧:养成「变量名后面紧跟字母、数字、下划线时,用
${}」的习惯。${file}.txt永远比$file.txt安全。
参数展开还有一些进阶用法,能省掉不少 sed/awk:
path=/data/log/app.log echo ${path##*/} # app.log (删掉最后一个 / 及其左侧,取文件名) echo ${path%/*} # /data/log (删掉最后一个 / 及其右侧,取目录) echo ${path%.log} # /data/log/app (去后缀) echo ${#path} # 17 (变量长度)
$(cmd)把一条命令的输出「塞进」另一个位置。这是脚本里把命令串起来的关键。
today=$(date +%F) # 把 date 的输出存进变量 echo "今天是 $today" # 今天是 2026-08-04 # 嵌套:先 ls 再数行 count=$(ls /etc | wc -l) echo "/etc 下有 $count 个条目" # 直接用在命令里 tar -czf backup-$(date +%F).tar.gz /home # 备份文件名带日期 kill $(cat /var/run/myapp.pid) # 从 pid 文件读 PID 再杀进程
$(cmd) 也可以写成反引号 `cmd`,但反引号不能嵌套、可读性差,现代脚本一律用 $(cmd)。
💡 技巧:命令替换会自动去掉命令输出末尾的换行符。所以
today=$(date +%F)得到的today没有尾部换行,直接拼接很安全。
$((...))Shell 本身是字符串的世界,但用 $((...)) 可以做整数运算。
x=5 y=3 echo $((x + y)) # 8 echo $((x * y)) # 15 echo $((x / y)) # 1(整数除法,截断小数) echo $((x % y)) # 2(取余) echo $((2 ** 10)) # 1024(幂) # 经典用法:计数器自增 count=0 count=$((count + 1)) # 自增 echo $count # 1
注意 $((...)) 里变量不需要再写 $:$((x + y)) 和 $(($x + $y)) 都行,但前者更简洁。
⚠️ 注意:
$((...))只支持整数运算。$((3 / 2))得1,不是1.5。需要小数就上bc或awk:echo "scale=2; 3/2" | bc得1.50。
~echo ~ # /home/alice(当前用户家目录) echo ~/work # /home/alice/work echo ~root # /root(指定用户的家目录)
~ 是登录 Shell 启动时就定下的,脚本里更常用的是 $HOME(它是个变量,可以改;~ 不能改)。
这是新手和老手的分水岭。规则只有三条:
| 写法 | 展开行为 | 用途 |
|---|---|---|
单引号 '...' |
完全不展开 | 字面字符串、正则、含特殊字符的文本 |
双引号 "..." |
展开 $ \`` \` |
含变量或命令替换的字符串 |
| 无引号 | 展开 + 单词拆分 + 通配 | 几乎总该避免 |
name=Alice echo 'Hello, $name' # Hello, $name(单引号:原样输出) echo "Hello, $name" # Hello, Alice(双引号:展开变量) echo Hello, $name # Hello, Alice(无引号:这里恰好一样) # 但当值里有空格,差别就致命了 file="my report.txt" ls $file # 错!被拆成两个参数: ls my report.txt ls "$file" # 对!作为一个整体: ls "my report.txt"
关键概念:在脚本里引用变量,永远加双引号。
"$var"而不是$var。这一条规则能帮你避开 Shell 里一半的 bug。
dst="/data/my backup" mkdir $dst # 错!等价 mkdir /data/my backup(两个目录) mkdir "$dst" # 对!一个目录 /data/my backup
原因:不加引号时,展开后会再发生「单词拆分」——按空格、制表符、换行把值切成多个参数。双引号能挡住单词拆分。
pattern= grep $pattern file # 等价 grep file(pattern 消失了,file 成了搜索模式) grep "$pattern" file # 等价 grep "" file(搜索空字符串,行为不同)
更危险的是删除操作:rm -rf $dir/,如果 dir 是空变量,就变成 rm -rf /。所以变量必须加引号,且配合 set -u(第 05 节)防未定义变量。
files=$(ls *.txt) echo $files # 所有文件名挤成一行(因为 echo $files 无引号又被拆了) echo "$files" # 保留换行(双引号挡住拆分)
记住:命令替换的结果如果要原样保留格式,引用时必须加双引号。
echo '今天是 $today' # 今天是 $today(单引号挡住了展开) echo "今天是 $today" # 今天是 2026-08-04
正则、awk 脚本、固定文本用单引号;含变量和命令替换的字符串用双引号。
$(...),反引号不能嵌套# 想表达「先 date,再取第 1 列」 result=`date | awk '{print $1}'` # 错!反引号里不能再有反引号 result=$(date | awk '{print $1}') # 对!$() 可嵌套,可读
x=5,等号两边不能有空格——空格会让 Shell 把它当成「命令 + 参数」,这是新手的头号坑。$x/${x}、命令替换 $(cmd)、算术展开 $((...))、波浪号展开 ~。其中 ${x} 能和后续字符隔开,是更安全的写法。$(cmd) 而不是反引号,因为前者可嵌套、可读、现代。$((...)) 只算整数,需要小数用 bc 或 awk。set -u 才能彻底防住。这一节你掌握了脚本的「细胞」——变量与展开。下一节我们给脚本加上「分支」:用 test、[、[[ ]] 做条件判断,让脚本学会「如果……就……否则……」。注意 [[ ]] 是 Bash 内建,比传统的 [ 更强更安全,我们会讲清两者的区别和取舍。