上一课我们知道 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"
done

while 循环——条件为真就继续:

n=1
while [ $n -le 3 ]; do   # n <= 3 时循环
    echo "n=$n"
    n=$((n + 1))
done

until 循环——直到条件为真才停(while 的反面):

n=1
until [ $n -gt 3 ]; do   # 直到 n > 3 才停
    echo "n=$n"
    n=$((n + 1))
done
三种循环记法:for 遍历列表,while 真就干,until 真就停。

26.4 函数与退出码

一句话理解函数名() { ... } 定义,调用就是写名字;函数/脚本最后 returnexit 一个数字,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. $1$#$@$? 分别是什么?
  2. set -e 的作用?为什么健壮脚本都加它?
  3. 判断文件是否存在怎么写?
现在你能写基本的脚本了。但脚本的真正威力,在于把一堆小工具串成流水线——grepsedawk 这三个"文本三剑客",加上管道和重定向,能让你用一行命令从几万行日志里筛出想要的东西。stdin/stdout/stderr 是什么?管道 | 怎么把命令接起来? 下一课,我们走进 管道、重定向与文本三剑客,这是 shell 最硬核、也最实用的部分。

标签: 计算机基础, 操作系统, Linux

添加新评论