操作系统学习笔记 · 第 26 课 · Shell 脚本基础
上一课我们知道 shell 是"命令解释器"。但它远不止"一条条敲命令"——它还能把一堆命令写进一个文件,按顺序、带逻辑地自动执行,这就是 shell 脚本。这一课,我们学习脚本的四大件:变量、判断、循环、函数,并写出第一个真正能用的自动化脚本。
从"把命令攒成一个文件"说起
你有没有过这种经历:一连串命令,每次都要手动敲一遍,烦死了。
shell 脚本就是解法:把这一串命令写进一个文件,以后每次运行这个文件,就自动按顺序执行。而且还能加逻辑——判断、循环、函数——让它像个"小程序"。
这一课,我们从零写一个。
26.1 变量与参数
一句话理解:name=value赋值(等号两边不能有空格),$name取值;脚本参数用$1、$2…,$#是参数个数,$@是全部参数,$?是上一条命令的退出码。
定义和使用变量:
name="jerry" # 赋值(注意:等号两边不能有空格!)
echo $name # 取值用 $name,输出 jerry一个最容易踩的坑:name = value是错的(等号两边有空格会被当成命令执行)。必须name=value紧紧挨着。
脚本参数:
你运行 ./script.sh a b c,脚本里能拿到:
$1=a,$2=b,$3=c(第 1、2、3 个参数);$#=3(参数个数);$@= 全部参数(a b c);$?= 上一条命令的退出码。
$? 特别重要,下一节(26.4)细讲退出码。26.2 判断(test / [ ])
一句话理解:if [ 条件 ]; then ... fi做分支,[ ]就是test命令,比较字符串、数字、文件。
脚本里做"如果…就…":
if [ 条件 ]; then
满足条件做的事
fi[ 条件 ] 里的 [ ],其实就是 test 命令的另一种写法。它能判断三类东西:
- 文件:
-f 文件存在且是普通文件、-d 目录存在且是目录; - 字符串:
=相等、!=不等; - 数字:
-eq等于、-gt大于、-lt小于、-ge大于等于。
例子:
if [ -f /etc/passwd ]; then # 文件存在?
echo "有 passwd 文件"
fi
if [ "$a" -gt 10 ]; then # 数字大于 10?
echo "a 大于 10"
fi注意:[和]两边都要有空格,[条件]是错的,要[ 条件 ]。
26.3 循环(for / while / until)
一句话理解:for i in ...遍历列表,while 条件当条件真就循环,until 条件直到条件真才停。
for 循环——遍历一个列表:
for i in 1 2 3; do
echo "第 $i 次"
done
for f in *.txt; do # 遍历所有 txt 文件
echo "处理 $f"
donewhile 循环——条件为真就继续:
n=1
while [ $n -le 3 ]; do # n <= 3 时循环
echo "n=$n"
n=$((n + 1))
doneuntil 循环——直到条件为真才停(while 的反面):
n=1
until [ $n -gt 3 ]; do # 直到 n > 3 才停
echo "n=$n"
n=$((n + 1))
done三种循环记法:for遍历列表,while真就干,until真就停。
26.4 函数与退出码
一句话理解:函数名() { ... }定义,调用就是写名字;函数/脚本最后return或exit一个数字,0 表示成功、非 0 表示失败。
定义和调用函数:
greet() { # 定义
echo "你好,$1"
}
greet "jerry" # 调用(传参数 $1=jerry)退出码——每个命令跑完都会返回一个数字:
- 0 = 成功;
- 非 0 = 失败(不同的非 0 值代表不同的失败原因)。
grep 就是典型:找到了返回 0,找不到返回 1。所以脚本里常这么写:if grep "error" app.log; then # grep 找到 → 返回 0 → 进入 if
echo "日志里有 error"
fi脚本最后可以用 exit 数字 明确设置退出码:
exit 0 # 成功
exit 1 # 失败退出码是脚本之间"沟通"的暗号:别的脚本或 CI 系统,靠 $? 判断你的脚本是成是败。26.5 set -e / set -u / set -x
这是让脚本"健壮"的关键:
一句话理解:set -e让"任一命令失败就立即退出"(默认会继续跑),set -u用未定义变量就报错,set -x打印每条命令(调试用)。
默认情况下,脚本里某条命令失败了,脚本会继续往下跑——这很危险(可能带着错误的状态继续执行)。三个选项:
set -e:任一命令失败,立刻退出。防止"错了还硬跑";set -u:用到未定义的变量就报错退出,防止拼写错误静默通过;set -x:打印每条命令(前面带+),调试神器。
健壮脚本的标准开头,几乎必写:
set -euo pipefail-e:出错就停;-u:未定义变量报错;-o pipefail:管道里任何一环失败都算失败(默认只看最后一环)。
动手实验:第一个脚本
#!/bin/bash
set -euo pipefail
name=${1:-world} # 参数,缺省 world
for i in 1 2 3; do
echo "第 $i 次:你好,$name"
done
if [ -d "$HOME" ]; then
echo "你的家目录是:$HOME"
fi
exit 0保存为 hello.sh,然后:
chmod +x hello.sh # 加可执行权限
./hello.sh jerry # 传参数 jerry
echo $? # 看退出码,应该是 0观察:参数、循环、判断、退出码一起工作了。${1:-world} 是"参数 1,如果没传就用默认值 world"——这是给参数设默认值的常用写法。
深入点:两个进阶话题
① 引号坑
$var 加不加引号,差别巨大:
name="hello world"
echo $name # 会被"空格分词"拆成两段,可能出错
echo "$name" # 当作一个整体,安全规则:脚本里的变量,几乎都要加双引号"$var",防止空格分词和通配符*展开。
② $* vs $@
都被双引号包住时,行为不同:
"$@":每个参数保持独立(推荐)——"$1" "$2" "$3";"$*":所有参数合成一个字符串——"$1 $2 $3"。
想逐个参数处理时,一定用 "$@"。小结与思考题
这一课,我们学会了 shell 脚本的四大件:
- 变量与参数:
name=value、$1$#$@$?; - 判断:
if [ 条件 ]; then ... fi,[ ]就是 test; - 循环:
for遍历、while真就干、until真就停; - 函数与退出码:
f(){}定义,0 成功非 0 失败; set -euo pipefail:健壮脚本的标准开头。
留三个问题:
$1、$#、$@、$?分别是什么?set -e的作用?为什么健壮脚本都加它?- 判断文件是否存在怎么写?
现在你能写基本的脚本了。但脚本的真正威力,在于把一堆小工具串成流水线——grep、sed、awk这三个"文本三剑客",加上管道和重定向,能让你用一行命令从几万行日志里筛出想要的东西。stdin/stdout/stderr 是什么?管道|怎么把命令接起来? 下一课,我们走进 管道、重定向与文本三剑客,这是 shell 最硬核、也最实用的部分。