第 9 章 · 01 从命令到脚本:变量与展开


文档摘要

第 9 章 · 01 从命令到脚本:变量与展开 本节摘要:当你发现自己在反复敲同一串命令,就该把它写成脚本了。而把「一行命令」升级成「一段脚本」的第一块基石,就是变量。本节要讲透两件事:一是变量怎么赋值( ,等号两边不能有空格,这是新手最常见的报错源头);二是 Shell 的四种展开——参数展开 、命令替换 、算术展开 、波浪号展开 。更关键的是,你要建立一个心智模型:所有展开都在命令执行之前由 Shell 完成。理解了这一点,你就能解释为什么引号在 Shell 里如此重要——单引号挡住展开,双引号允许展开但挡住单词拆分,不加引号则任由 Shell 把值撕成碎片。这个模型是后续所有章节的地基。 内容来源:综合 Shell 脚本知识与原项目「Linux 命令大全」整理,套用体系化模板。

第 9 章 · 01 从命令到脚本:变量与展开

本节摘要:当你发现自己在反复敲同一串命令,就该把它写成脚本了。而把「一行命令」升级成「一段脚本」的第一块基石,就是变量。本节要讲透两件事:一是变量怎么赋值(x=5,等号两边不能有空格,这是新手最常见的报错源头);二是 Shell 的四种展开——参数展开 $x、命令替换 $(cmd)、算术展开 $((...))、波浪号展开 ~。更关键的是,你要建立一个心智模型:所有展开都在命令执行之前由 Shell 完成。理解了这一点,你就能解释为什么引号在 Shell 里如此重要——单引号挡住展开,双引号允许展开但挡住单词拆分,不加引号则任由 Shell 把值撕成碎片。这个模型是后续所有章节的地基。

内容来源:综合 Shell 脚本知识与原项目「Linux 命令大全」整理,套用体系化模板。

学习目标

阅读完本节,你应当能够:

  1. 正确地给变量赋值,并解释为什么等号两边不能有空格。
  2. 说清 Shell 的「两阶段处理」:先展开、再执行,所有展开发生在命令真正跑起来之前。
  3. 熟练使用四种展开:参数展开 $x/${x}、命令替换 $(cmd)、算术展开 $((...))、波浪号展开 ~
  4. 区分单引号、双引号、无引号三种情况,知道何时该用哪种。
  5. 解释「单词拆分」与「路径名展开」这两个不加引号时悄悄发生的陷阱。
  6. 把一个手敲的命令改写成带变量的脚本雏形。

一、设计动机(为什么这样设计)

从命令到脚本,差的只是一个变量

你已经会用管道把几个命令串起来,比如 tar -czf backup.tar.gz /home/alice。但每次源目录变了,你就得重新敲一遍。脚本的第一步,就是把「会变的东西」抽成变量:

#!/usr/bin/env bash src=/home/alice dst=backup.tar.gz tar -czf "$dst" "$src"

从此你只要改 srcdst 两行,剩下的命令原样不动。这就是脚本的本质:把命令参数化,让一段命令能复用

核心心智模型:先展开,再执行

理解 Shell 最重要的一句话是:Shell 在执行任何命令之前,会先把这一行里所有能展开的东西展开。这个过程叫「展开」(expansion),它发生在命令真正运行之前。

你输入: echo $HOME Shell 展开:echo /home/alice ← 先把 $HOME 替换成它的值 Shell 执行:echo 程序拿到参数 /home/alice,打印出来

也就是说,echo 程序自己根本没见过 $HOME 这个符号——它收到的是 Shell 展开后的结果 /home/alice。命令本身永远只和「展开后的结果」打交道。

关键概念:展开是 Shell 的工作,不是命令的工作。echolscp 这些命令收到的永远是已经展开完毕的字符串。这个事实解释了 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,然后报错)。

二、高频组合与实战

1. 参数展开 $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 (变量长度)

2. 命令替换 $(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 没有尾部换行,直接拼接很安全。

3. 算术展开 $((...))

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。需要小数就上 bcawkecho "scale=2; 3/2" | bc1.50

4. 波浪号展开 ~

echo ~ # /home/alice(当前用户家目录) echo ~/work # /home/alice/work echo ~root # /root(指定用户的家目录)

~ 是登录 Shell 启动时就定下的,脚本里更常用的是 $HOME(它是个变量,可以改;~ 不能改)。

5. 三种引号的区别(本节最重要的实战)

这是新手和老手的分水岭。规则只有三条:

写法 展开行为 用途
单引号 '...' 完全不展开 字面字符串、正则、含特殊字符的文本
双引号 "..." 展开 $ \`` \` 含变量或命令替换的字符串
无引号 展开 + 单词拆分 + 通配 几乎总该避免
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。

三、踩坑与排错

坑 1:变量不加引号,含空格的路径被拆开

dst="/data/my backup" mkdir $dst # 错!等价 mkdir /data/my backup(两个目录) mkdir "$dst" # 对!一个目录 /data/my backup

原因:不加引号时,展开后会再发生「单词拆分」——按空格、制表符、换行把值切成多个参数。双引号能挡住单词拆分。

坑 2:变量值是空,导致命令缺参数

pattern= grep $pattern file # 等价 grep file(pattern 消失了,file 成了搜索模式) grep "$pattern" file # 等价 grep "" file(搜索空字符串,行为不同)

更危险的是删除操作:rm -rf $dir/,如果 dir 是空变量,就变成 rm -rf /所以变量必须加引号,且配合 set -u(第 05 节)防未定义变量

坑 3:命令替换丢了换行,多行结果变成一行

files=$(ls *.txt) echo $files # 所有文件名挤成一行(因为 echo $files 无引号又被拆了) echo "$files" # 保留换行(双引号挡住拆分)

记住:命令替换的结果如果要原样保留格式,引用时必须加双引号

坑 4:把单引号当双引号用,导致变量不展开

echo '今天是 $today' # 今天是 $today(单引号挡住了展开) echo "今天是 $today" # 今天是 2026-08-04

正则、awk 脚本、固定文本用单引号;含变量和命令替换的字符串用双引号。

坑 5:反引号 vs $(...),反引号不能嵌套

# 想表达「先 date,再取第 1 列」 result=`date | awk '{print $1}'` # 错!反引号里不能再有反引号 result=$(date | awk '{print $1}') # 对!$() 可嵌套,可读

本节要点回顾

  1. 变量赋值 x=5,等号两边不能有空格——空格会让 Shell 把它当成「命令 + 参数」,这是新手的头号坑。
  2. 核心心智模型:先展开,再执行。所有展开(变量、命令替换、算术、波浪号)都在命令真正运行之前由 Shell 完成,命令本身只看到展开后的结果。
  3. 四种展开:参数展开 $x/${x}、命令替换 $(cmd)、算术展开 $((...))、波浪号展开 ~。其中 ${x} 能和后续字符隔开,是更安全的写法。
  4. 命令替换一律用 $(cmd) 而不是反引号,因为前者可嵌套、可读、现代。
  5. 算术展开 $((...)) 只算整数,需要小数用 bcawk
  6. 三种引号:单引号不展开、双引号展开但不拆分、无引号既展开又拆分。脚本里引用变量永远加双引号——这一条规则消灭一半的 bug。
  7. 变量为空是定时炸弹,配合第 05 节的 set -u 才能彻底防住。

这一节你掌握了脚本的「细胞」——变量与展开。下一节我们给脚本加上「分支」:用 test[[[ ]] 做条件判断,让脚本学会「如果……就……否则……」。注意 [[ ]] 是 Bash 内建,比传统的 [ 更强更安全,我们会讲清两者的区别和取舍。


发布者: 作者: 灏天文库 转发
评论区 (0)
U