Skip to content

Shell 脚本编程 ​

Shell 脚本是把命令行技能放大的杠杆——一劳永逸地自动化重复操作。本篇覆盖从变量到生产级脚本的全部知识点。

基础语法 ​

变量 ​

bash
#!/bin/bash
# 变量赋值(等号两边不能有空格!)
name="production-server"
count=42

# 使用变量(推荐双引号包裹)
echo "Server: ${name}, Count: ${count}"
echo "Log path: /var/log/${name}/app.log"   # 花括号划定边界

# 特殊变量
echo "脚本名: $0"
echo "第1个参数: $1, 第2个: $2"
echo "参数个数: $#"
echo "所有参数: $@"
echo "上一命令退出码: $?"    # 0=成功, 非0=失败
echo "当前进程 PID: $$"

字符串操作 ​

bash
str="hello-world-backup-2026.tar.gz"

echo ${str%.tar.gz}         # hello-world-backup-2026(去除最短匹配后缀)
echo ${str%%.*}             # hello-world-backup-2026(去除最长匹配后缀)
echo ${str#*-}              # world-backup-2026.tar.gz(去除最短匹配前缀)
echo ${str##*-}             # 2026.tar.gz(去除最长匹配前缀)
echo ${str/world/universe}  # hello-universe-backup-2026.tar.gz(替换)
echo ${#str}                # 字符串长度

# 实战:提取日期用于备份命名
backup_file="db_2026-07-26.sql.gz"
date_part=${backup_file#*_}        # 2026-07-26.sql.gz
date_part=${date_part%.sql.gz}     # 2026-07-26

数组 ​

bash
servers=("web01" "web02" "db01" "redis01")

echo ${servers[0]}          # web01
echo ${servers[@]}          # 所有元素
echo ${#servers[@]}         # 数组长度

# 遍历数组
for server in "${servers[@]}"; do
    echo "Deploying to $server..."
done

条件判断 ​

if / test / [ ] ​

bash
# 三种写法等价
if test -f /etc/nginx/nginx.conf; then ... fi
if [ -f /etc/nginx/nginx.conf ]; then ... fi     # [ ] 是 test 的别名
if [[ -f /etc/nginx/nginx.conf ]]; then ... fi   # [[ ]] 是 bash 扩展,更强大

# 文件测试
[ -f file ]    # 是否为普通文件
[ -d dir  ]    # 是否为目录
[ -e path ]    # 是否存在
[ -r file ]    # 是否可读
[ -w file ]    # 是否可写
[ -x file ]    # 是否可执行
[ -s file ]    # 是否存在且非空

# 字符串测试
[ "$a" = "$b"  ]    # 相等(注意变量加引号!)
[ -z "$str"    ]    # 是否为空
[ -n "$str"    ]    # 是否非空

# 数值测试
[ "$n" -eq 5  ]     # equal
[ "$n" -ne 5  ]     # not equal
[ "$n" -gt 5  ]     # greater than
[ "$n" -lt 5  ]     # less than
[ "$n" -ge 5  ]     # greater or equal
[ "$n" -le 5  ]     # less or equal

# 逻辑运算
[ "$a" = "yes" ] && [ "$b" = "yes" ]    # AND
[ "$a" = "yes" ] || [ "$b" = "yes" ]    # OR
[ ! -f file ]                             # NOT

[[ ]] vs [ ] — 到底用哪个? ​

bash
# [ ] 的问题:
[ $a = "hello" ]        # 如果 $a 为空,会变成 [ = "hello" ],语法错!
[ "$a" = "hello" ]      # 正确,但总是要加引号很烦

# [[ ]] 的优点:
[[ $a = "hello" ]]      # 不加引号也安全
[[ $a =~ ^[0-9]+$ ]]    # 支持正则匹配
[[ $a == hello && $b == world ]]  # 支持 && ||

建议

脚本里统一用 [[ ]],它更安全、功能更强。只有需要兼容 /bin/sh(非 bash)时才用 [ ]。

循环 ​

bash
# for 循环(最常用)
for i in {1..5}; do
    echo "Round $i"
done

for file in /var/log/*.log; do
    echo "Processing $file"
    gzip "$file"
done

# while 循环(读文件逐行处理)
while IFS= read -r line; do
    echo "Line: $line"
done < /etc/hosts

# 无限循环 + break
while true; do
    if curl -s http://localhost:8080/health > /dev/null; then
        echo "Service is up!"
        break
    fi
    sleep 2
done

# case 分支
case "$1" in
    start)
        echo "Starting service..."
        ;;
    stop)
        echo "Stopping service..."
        ;;
    restart|reload)
        echo "Restarting service..."
        ;;
    *)
        echo "Usage: $0 {start|stop|restart}"
        exit 1
        ;;
esac

函数 ​

bash
#!/bin/bash

# 函数定义
log_info() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"
}

log_error() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2
}

# 带返回值的函数
check_port() {
    local port=$1    # local = 局部变量,不影响外部
    if ss -tlnp | grep -q ":$port "; then
        return 0     # 成功(0=成功,非0=失败)
    else
        return 1
    fi
}

# 调用
if check_port 8080; then
    log_info "Port 8080 is listening"
else
    log_error "Port 8080 is NOT listening"
fi

调试三板斧 ​

bash
#!/bin/bash
set -e          # 任何命令失败(非零退出)立即退出
set -u          # 使用未定义变量时报错退出
set -o pipefail # 管道中任何命令失败都算失败,而不仅最后一个

# 调试时加 -x,执行时打印每条命令
bash -x script.sh
# 或在脚本内:
set -x
# ... 需要调试的代码 ...
set +x

生产脚本必须加

set -euo pipefail 这三个选项能拦截 90% 的脚本 bug,比事后排查高效得多。

实战脚本 ​

1. 带日期+自动清理的备份脚本 ​

bash
#!/bin/bash
set -euo pipefail

# === 配置 ===
BACKUP_DIR="/backup/mysql"
RETENTION_DAYS=7            # 保留最近 7 天
DATABASE="myapp"
DB_USER="backup"
DB_PASS="your_password"

# === 日期标记 ===
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/${DATABASE}_${DATE}.sql.gz"

# === 备份 ===
mkdir -p "$BACKUP_DIR"
mysqldump -u"$DB_USER" -p"$DB_PASS" "$DATABASE" | gzip > "$BACKUP_FILE"

echo "[OK] Backup saved: $BACKUP_FILE"

# === 清理旧备份 ===
find "$BACKUP_DIR" -name "${DATABASE}_*.sql.gz" -mtime +${RETENTION_DAYS} -delete
echo "[OK] Removed backups older than ${RETENTION_DAYS} days"

2. 批量重命名 ​

bash
#!/bin/bash
# 将所有 .jpeg 改名为 .jpg
for file in *.jpeg; do
    [ -e "$file" ] || continue    # 没有匹配文件时跳过
    mv "$file" "${file%.jpeg}.jpg"
    echo "Renamed: $file → ${file%.jpeg}.jpg"
done

3. CPU 监控告警脚本 ​

bash
#!/bin/bash
set -euo pipefail

THRESHOLD=80
ALERT_EMAIL="ops@example.com"

CPU_USAGE=$(LC_ALL=C top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)
CPU_USAGE=${CPU_USAGE%.*}    # 取整数部分

if [ "$CPU_USAGE" -gt "$THRESHOLD" ]; then
    MESSAGE="[ALERT] CPU usage is ${CPU_USAGE}% on $(hostname) at $(date)"
    echo "$MESSAGE"
    # 发送告警(根据实际情况选一种)
    # echo "$MESSAGE" | mail -s "CPU Alert" "$ALERT_EMAIL"
    # curl -X POST -d "$MESSAGE" https://your-webhook-url
fi

常见坑 ​

坑 1:变量不用双引号 ​

bash
# 错误:文件名有空格会出问题
file="my document.txt"
rm $file        # 实际执行 rm my document.txt,把两个文件都删了!

# 正确
rm "$file"

变量引号规则

永远用双引号包裹变量,除非你明确知道不需要。唯一的例外是故意做 word splitting 的场景。

坑 2:空格敏感 ​

bash
name = "hello"    # 错误!两边有空格
name="hello"      # 正确

if [ $a = "yes" ] # 错误!内部要有空格
if [ "$a" = "yes" ]  # 正确:[ 是命令,后面需要空格

坑 3:管道中的错误被忽略 ​

bash
# 错误:即使 grep 失败,$? 也是 0(因为最后一个命令 echo 成功)
cat /etc/shadow | grep "bob" | echo "done"

# 正确:set -o pipefail
set -o pipefail
cat /etc/shadow | grep "bob"   # 现在 grep 失败 → 脚本退出

生产环境经验 ​

  • 脚本模板化:每个脚本都加 set -euo pipefail + 日志函数 + 错误处理
  • 幂等性:脚本要能安全地重复执行,不应因重复运行而出错
  • 配置与逻辑分离:用变量或外部配置文件,别把 IP/密码硬编码在逻辑里
  • 返回值检查:关键操作后检查 $?,别假设一切正常
  • shellcheck:sudo apt install shellcheck && shellcheck script.sh 做静态检查

🎯 本章要点 ​

  • $? 是上一个命令的退出码(0=成功,非0=失败)
  • 变量赋值等号两边不能有空格
  • 脚本开头加 set -euo pipefail 捕获错误
  • 用 [[ -f path ]] 检查文件是否存在
加载练习题中...

有问题或补充?欢迎留言