🖥️ 第09章 · Shell 脚本入门

变量/条件/循环/函数/定时

Shell 脚本入门

这一章把前面所有命令串成一门"小语言":变量、条件、循环、函数,再到这段落进定时任务里无人值守地运行。学完你就能写出像 /root/scripts/tieba_sign.py、monitor_report.py 那样的自动化脚本。

什么是 Shell 脚本

Shell 脚本就是一个按顺序存放命令的文本文件,用解释器(通常是 bash)逐行执行。它适合"把一组日常命令固化下来",比如隔天跑一次备份、每晚发一次监控报告。

一个最简脚本:

#!/bin/bash
# 第一行 shebang,告诉系统用哪个解释器执行
echo "hello, welcome to $HOSTNAME"
whoami
date

提示 #!/bin/bash 是脚本第一行,不可省。它决定了用 bash 解释还是其它;写错解释器会得到一堆奇怪的语法错误。

创建并运行脚本

# 用任意编辑器写好,保存为 script.sh
chmod +x script.sh                 # 加执行权限(第 4 章)
./script.sh                        # 方式1:直接执行
bash script.sh                     # 方式2:用解释器执行(无需 x 权限)

注意 ./script.sh 需要执行权限;bash script.sh 把文件当成参数交给 bash,不需要执行位。两者都能跑,但 ./ 必须 chmod +x。

变量

Shell 变量不加类型,赋值时 = 两边不能有空格。

name="srv1952823"
echo "这是 $name"            # 用 $ 或 ${} 取值
echo "脚本名是 $0"           # $0 是脚本自身
echo "第1个参数是 $1"         # $1 第1个参数,$2 第2个...
echo "参数个数 $#"
echo "全部参数 $*"
特殊变量 含义
$0 脚本名
$1~$9 第 1~9 个参数
$# 参数个数
$* / $@ 所有参数(整体 / 逐个)
$? 上一条命令的退出码(0 表示成功)
$$ 当前脚本的 PID
# 经典用法:给变量附上命令结果
today=$(date +%F)          # 命令替换,把 date 的输出赋给变量
echo "今天是 $today"

提示 取值用 $,赋值不带 $ 且等号两侧无空格。$(命令) 与反引号 `命令` 等价,推荐用 $(...),可嵌套、可读性更好。

条件判断 if

if [ 条件 ]; then
    命令
elif [ 另一个条件 ]; then
    命令
else
    命令
fi
# 判断目录是否存在
if [ -d /var/www/bangdream ]; then
    echo "目录存在"
else
    echo "目录不存在"
fi

常用条件运算符:

写法 含义
[ -f 文件 ] 文件存在且是普通文件
[ -d 目录 ] 目录存在
[ -e 路径 ] 路径存在(文件或目录)
[ "$a" = "$b" ] 字符串相等(注意有空格)
[ "$a" -gt 5 ] 数字大于 5(大于用 -gt)
[ -z "$a" ] 字符串为空
[ -n "$a" ] 字符串非空

注意 [ 前后与 =、-gt 两侧都要有空格——[$a=$b] 是错的。也可以用 test 条件,与 [ ] 等价。[ ] 用于字符串/文件,(( )) 用于数字运算更直观。

循环 for 与 while

for 变量 in 值列表; do
    命令
done
# 遍历一组文件名
for f in /var/www/bangdream/*.log; do
    echo "处理 $f"
done

# 数字循环
for i in {1..5}; do
    echo "第 $i 次"
done
while [ 条件 ]; do
    命令
done
# 每 60 秒执行一次(配合 sleep)
while :; do
    uptime >> /tmp/uptime.log
    sleep 60
done

提示 写循环前先手动跑一次命令确认结果,再放循环里。循环体里常配 sleep,给系统留点喘息,避免 CPU 打满。

函数

把可复用逻辑封装成函数:

函数名() {
    命令
    命令
}

函数名   # 调用
backup() {
    tar -czf /root/backup/www-$(date +%F).tar.gz -C /var/www bangdream
    echo "已备份到 $?"
}

backup   # 调用

案例:定时备份脚本

把第 5、6 章的内容组合成一个可运行脚本:

#!/bin/bash
# 每周/每日备份 bangdream 应用目录到 /root/backup
BACKUP_DIR="/root/backup"
SRC="/var/www/bangdream"

DATE=$(date +%F)
DEST="$BACKUP_DIR/bangdream-$DATE.tar.gz"

if [ ! -d "$BACKUP_DIR" ]; then
    mkdir -p "$BACKUP_DIR"
fi

tar -czf "$DEST" -C /var/www bangdream 2>/dev/null

if [ $? -eq 0 ]; then
    echo "[ok] 备份完成: $DEST"
else
    echo "[fail] 备份失败: $DEST" >&2
    exit 1
fi
要点 解释
if [ ! -d ... ] 目录不存在则先创建
2>/dev/null 丢弃 tar 的进度噪声
$? 检查 tar 是否成功
exit 1 失败时以非零码退出,便于自动化识别
带日期文件名 不会覆盖旧备份(第 5 章)

定时任务与 cron

脚本写完,经常要定时自动跑,而不是手动执行。cron 是最常用的方案。

crontab -e          # 编辑当前用户的定时任务表
crontab -l          # 查看当前用户的定时任务

cron 时间格式:五段,分别为 分 时 日 月 周。

分钟(0-59) 小时(0-23) 日(1-31) 月(1-12) 周(0-7)
# 每天 7:00 运行贴吧签到
0 7 * * * /usr/bin/python3 /root/scripts/tieba_sign.py >> /var/log/tieba_sign.log 2>&1

# 每天 8:00 运行监控报告
0 8 * * * /usr/bin/python3 /root/scripts/monitor_report.py >> /var/log/monitor_report.log 2>&1

注意 这两行正好对应这台服务器上的真实 cron 任务。cron 里务必用绝对路径、加上 >> 日志 2>&1,否则看不到运行结果;cron 默认环境极简,PATH 可能不含 /usr/local/bin,所以在 cron 里调用命令要么写绝对路径,要么在脚本开头 export PATH=...。

时间写法 含义
7 * * * * 每小时的第 07 分
0 8 * * * 每天 08:00
0 3 * * * 每天 03:00(适合压点备份)
30 2 * * 6 每周六 02:30

脚本调试与规范

惯用法 作用
set -e 任何命令失败就立即退出脚本
set -u 用未定义变量时报错
bash -x script.sh 逐条打印执行过程(排错神器)
全大写的常量名 如 BACKUP_DIR,一眼看出是常量
关键步骤加 echo 输出中间状态,便于跟踪

提示 脚本第一句话常写 #!/bin/bash 和 set -e。遇到"脚本看起来没跑成功"就加 bash -x,看它卡在哪一行。

易错点

  • 缺 chmod +x:不用 bash 直接 ./ 会报 permission denied。
  • = 两侧有空格:shell 会当命令解析而报错。
  • [ ] 里忘了空格:简直是最隐蔽的坑。
  • cron 里用相对路径:环境极简,一定要绝对路径。
  • 脚本没加 & 或 ; 分离命令:echo a echo b 会被当两个命令,除非换行或加 ;。

相关命令

#!/bin/bash(shebang)、set -e/-u/-x、if、for、while、case、函数、crontab、at(一次性定时)、systemd timer(更现代的定时方案)。