第 4 章 · 01 变量与基础语法 一切脚本都以三样东西开场:解释器声明、变量、参数。本节把"词法层"一次讲透——shebang 的语义、变量定义与引用规则、引号陷阱、命令替换、位置参数家族,以及字符串子串提取。 学习目标 理解 shebang 的作用,写出规范脚本头并让脚本可执行 掌握变量定义规则(等号两侧无空格)与 ${} 安全引用 区分单引号与双引号、$() 与 $(()) 的不同语义 熟练使用 $0/$1/$#/$?/$$/$@/$ 等位置参数与特殊变量 掌握 ${var%.} 与 ${var//.} 两类子串提取 一、Shebang:脚本的第一行 脚本第一行 有一个专门的名字——Shebang(由 "sharp" 井号和 "bang" 感叹号组合而来)。
一切脚本都以三样东西开场:解释器声明、变量、参数。本节把"词法层"一次讲透——shebang 的语义、变量定义与引用规则、引号陷阱、命令替换、位置参数家族,以及字符串子串提取。
脚本第一行 #!/bin/bash 有一个专门的名字——Shebang(由 "sharp" 井号和 "bang" 感叹号组合而来)。它的作用是告诉操作系统:"请用 /bin/bash 这个解释器来执行本文件"。没有这一行,内核会尝试按当前 shell 的语法解释脚本,遇到 bash 特有语法就会出错。
/bin/bash 是 Bash(Bourne-again shell)的路径。Bash 是 Linux 发行版最常用的默认登录 shell,语法上兼容古老的 Bourne shell(sh),并扩展了大量现代特性:[[ ]] 条件、数组、字符串操作等。因此绝大多数脚本都以 #!/bin/bash 开头。
一个更灵活的变体是 #!/usr/bin/env bash:它通过 env 命令在 PATH 中查找 bash。不同发行版把 bash 放在 /bin 还是 /usr/bin 并不一致,用 env 写法可以自动适配。代价是牺牲一点安全性(PATH 被篡改时可能加载到恶意程序),所以系统脚本(如 systemd 单元)仍偏好绝对路径。
写好了脚本头,还要让它可执行,三步走:
chmod +x hello.sh # 加上执行权限(第 3 章讲过的 x 位) ./hello.sh # 用相对路径执行
如果忘记加执行权限,会报 "Permission denied"。也可以直接 bash hello.sh 强制用 Bash 解释,但那样 shebang 形同虚设,不推荐。
Shell 变量定义的规则只有一条硬性要求:等号两侧不能有空格。
HW="Hello World" # 正确:等号紧贴 x = 2 # 错误:被解析为"运行命令 x,参数 = 和 2"
第二条规则:引用变量加 $。$HW 是最简写法;当变量名紧挨着其他字符时,必须用花括号界定边界,比如 echo "${HW}suffix" 输出 Hello Worldsuffix,而 echo "$HWsuffix" 会去读一个不存在的变量。
引号是新手翻车重灾区,三条规则记牢:
| 写法 | 语义 | 示例结果(设 name=Qoder) |
|---|---|---|
"$name" |
双引号:整体为一个参数,变量展开 | hello Qoder(一个参数) |
'$name' | 单引号:字面量,一切原样 | $name |
||
$name |
无引号:变量展开,且结果按空白切词 | 可能被拆成多个参数 |
经验法则:不确定时永远加双引号。"$1" 能安全处理带空格的参数,$1 不能。
命令替换 $(command) 把命令的输出当作值:
DATE=$(date) # DATE 变量 = 当前日期字符串 FILES=$(ls /tmp) # 也可以配合管道:$(ls /tmp | head -5)
早期的写法是反引号 `date`,但反引号在嵌套和转义上极易出错,$() 是唯一推荐写法。注意区分三兄弟:
$var 变量引用 $(cmd) 命令替换(运行命令,取输出) $((expr)) 算术替换(计算表达式,取数值)
算术替换 $(( 1 + 2 )) 是内建机制,无需外部命令。另一种方式是用 expr 外部命令:expr 1 + 2,但 expr 要求每个操作数之间打空格、性能差、兼容问题多,现代脚本一律用 $(( ))。
脚本启动时传入的参数称为位置参数,配合一组特殊变量构成完整的"参数家族":
| 变量 | 含义 |
|---|---|
$0 |
脚本自身名称 |
$1 |
第一个参数 |
$# |
参数个数 |
$? |
上一条命令的退出码(0 成功,非 0 失败) |
$$ |
当前脚本的进程号 PID |
$@ |
所有参数组成的数组,每个参数独立成项 |
$* |
所有参数拼成的一个字符串 |
$@ 与 $* 的区别值得单独说:$@ 是"数组",展开后每个参数是一个独立单词;$* 是"一个字符串",把所有参数用空格拼起来。在 for 循环遍历参数、或把参数原样转传给下游命令时,用 "$@" 能保持每个参数完整;用 "$*" 则带空格的参数会被错误拆开。
参数还有一个经典的"缺省值"模式,用于没有传参时给默认值:
echo "${1:-yay}"
如果第一个参数存在就打印它,否则打印 yay。这是编写健壮脚本最常用的一招(后面的练习"伟大的一天"就直接考它)。
Shell 的参数展开(Parameter Expansion)提供了强大的字符串截取能力,最常用的两类是围绕"最后一个点"做文章:
${var%.*} # 删除最后一个点及其之后的内容(取点号之前的部分) ${var//*.} # 删除到最后一个点为止(取点号之后的部分)
注意两条的微妙差异:%.* 是"贪婪地去尾"——匹配最短后缀、从最后一个点开始删;//*. 是"贪婪地掐头"——* 匹配到最后一个点,留下点后面的尾巴。假设 var=archive.tar.gz:
${var%.*} → archive.tar ${var//*.} → gz
典型应用:处理文件名时分离"主体"与"扩展名",判断文件类型。与之配套的还有长度提取 ${#var} 返回字符串长度,以及头尾去字符 ${var#*.}(去最短前缀)。
本节建立了脚本的"词法层":shebang 声明解释器、等号无空格地定义变量、双引号安全地引用、() 捕获命令输出、(( )) 做算术、位置参数家族读取入参、{var%.*}/{var//*.} 切分字符串。这些是每一行脚本的地基,下一节在这上面搭建条件、循环与函数。
有了变量和参数,脚本还只能"直线跑完"。第 4 章第 2 节《条件循环与函数》将引入分支与循环控制流,让脚本学会"判断"和"重复",并掌握函数复用与交互输入。