Shell 脚本基础:变量、参数、判断、循环与函数

Shell 脚本这东西,说白了就是你把一堆 Linux 命令串起来,让机器替你干活。我刚开始写脚本时,觉得不就是把命令写进文件嘛,后来才发现,真正让脚本变智能的,是变量、条件判断、循环和函数这四样东西。今天咱们就把它们掰开揉碎,讲清楚。

核心要点:Shell 脚本的骨架是变量与参数,肌肉是条件判断,血液是循环,大脑是函数。掌握这四样,你就能写出真正“自动化”的轮换脚本。

1. 变量与参数传递

变量就是给数据起个名字。我习惯用大写字母命名变量名,这样一眼就能看出哪些是变量。

#!/bin/bash
# 变量定义,等号两边不能有空格
LOG_DIR="/var/log/myapp"
BACKUP_DIR="/backup/logs"
RETENTION_DAYS=30

# 使用变量,加 $ 符号
echo "日志目录: $LOG_DIR"
echo "备份目录: $BACKUP_DIR"
echo "保留天数: $RETENTION_DAYS"

参数传递更实用。你想想看,脚本写死了目录,换台机器就得改代码,多麻烦。用位置参数就能解决:

#!/bin/bash
# $0 是脚本名,$1 是第一个参数,$2 是第二个,$# 是参数个数
echo "脚本名: $0"
echo "第一个参数(源目录): $1"
echo "第二个参数(目标目录): $2"
echo "参数总数: $#"

我在项目中遇到过一个问题:用户传了参数但忘了传第二个,脚本直接崩了。后来我养成了检查参数个数的习惯:

if [ $# -lt 2 ]; then
    echo "用法: $0 <源目录> <目标目录>"
    exit 1
fi
特殊变量 含义 示例
$0 脚本文件名 ./rotate.sh
$1 ~ $9 第 1~9 个位置参数 ./rotate.sh /var/log
$# 参数个数 2
$@ 所有参数列表 /var/log /backup
$? 上一条命令的退出码 0 表示成功

个人技巧:我写脚本时,习惯在开头用变量定义所有“可调参数”,比如路径、天数、阈值。这样别人接手时,改开头几行就行,不用翻遍整个脚本。

2. 条件判断(if/else)

条件判断就是让脚本“长脑子”。最常见的场景:检查文件是否存在、目录是否可写、上一个命令是否成功。

#!/bin/bash
LOG_FILE="/var/log/myapp/access.log"

if [ -f "$LOG_FILE" ]; then
    echo "日志文件存在,开始处理..."
    # 处理逻辑
else
    echo "错误: 日志文件 $LOG_FILE 不存在"
    exit 1
fi

为什么会这样?很多新手写条件判断时,忘了给变量加双引号。如果变量是空的,不加引号会直接报语法错误。我曾经因为这个坑,在线上排查了半小时。

常用的文件测试操作符:

操作符 含义 示例
-f 是否为普通文件 [ -f /etc/passwd ]
-d 是否为目录 [ -d /var/log ]
-e 是否存在(文件或目录) [ -e /tmp/test ]
-w 是否可写 [ -w /var/log ]
-x 是否可执行 [ -x /usr/bin/rsync ]
-s 文件是否非空 [ -s /var/log/error.log ]

数值比较和字符串比较也常用:

#!/bin/bash
CURRENT_DAYS=35
RETENTION_DAYS=30

# 数值比较
if [ "$CURRENT_DAYS" -gt "$RETENTION_DAYS" ]; then
    echo "当前保留天数超过阈值,需要清理"
fi

# 字符串比较
STATUS="success"
if [ "$STATUS" = "success" ]; then
    echo "操作成功"
fi

避坑指南:我曾经在数值比较时用了 > 而不是 -gt,结果脚本一直跑不对。> 是重定向符号,不是比较符!数值比较用 -eq、-ne、-gt、-lt、-ge、-le,字符串比较用 =、!=。

3. 循环结构(for/while)

循环就是让脚本“不厌其烦地重复”。轮换脚本里,循环用得最多:遍历日志文件、批量处理备份、逐行读取配置文件。

for 循环:适合已知范围或列表的场景。

#!/bin/bash
# 遍历当前目录下的所有 .log 文件
for file in *.log; do
    echo "处理文件: $file"
    gzip "$file"
    mv "$file.gz" /backup/logs/
done

我习惯用 for 循环处理日期范围,比如生成最近 7 天的日期:

#!/bin/bash
for day in {1..7}; do
    DATE=$(date -d "-$day days" +%Y%m%d)
    echo "清理 $DATE 的日志..."
    # 实际清理逻辑
done

while 循环:适合条件驱动的场景,比如逐行读取文件。

#!/bin/bash
# 逐行读取日志文件列表
cat /tmp/log_list.txt | while read line; do
    if [ -f "$line" ]; then
        echo "压缩: $line"
        gzip "$line"
    else
        echo "跳过: $line (文件不存在)"
    fi
done

为什么会用 while 而不是 for?你想想看,如果文件列表有 10 万行,for 循环会先把所有行加载到内存,而 while 逐行读取,内存占用小得多。我在处理上百万行日志时,用 while 循环从来没崩过。

循环类型 适用场景 示例
for var in list 遍历已知列表或范围 遍历文件、日期、数组
for ((expr1; expr2; expr3)) C 风格循环,需要计数器 遍历 1 到 100
while condition 条件驱动,不确定次数 逐行读取、等待进程结束
until condition 条件为假时执行 等待服务启动

个人经验:循环里一定要加 sleep 或限制频率,尤其是操作远程服务器时。我曾经写了个循环,没加延迟,直接把生产环境的磁盘 IO 打满了。现在我的循环里都会加一句 sleep 0.1,给系统喘口气。

4. 函数定义与调用

函数就是“把重复代码打包”。我写脚本的原则是:同一段代码出现两次以上,就封装成函数。

#!/bin/bash

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

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

# 调用函数
log_info "开始日志轮换"
log_error "磁盘空间不足,轮换失败"

函数也可以有返回值,但注意 Shell 函数的返回值只能是 0~255 的整数:

#!/bin/bash

check_disk_usage() {
    local usage=$(df /var/log | tail -1 | awk '{print $5}' | sed 's/%//')
    if [ "$usage" -gt 80 ]; then
        return 1  # 磁盘使用率超过 80%
    else
        return 0
    fi
}

if check_disk_usage; then
    echo "磁盘空间充足,继续轮换"
else
    echo "磁盘空间不足,停止轮换"
    exit 1
fi

嗯,这里要注意:函数里的变量默认是全局的。我踩过这个坑——函数里改了某个变量,结果影响到了主流程。解决办法是用 local 关键字声明局部变量:

rotate_logs() {
    local log_dir="$1"   # 局部变量
    local days="$2"
    # 函数内部逻辑...
}

避坑指南:我曾经写了一个日志轮换函数,里面用了全局变量 COUNT,结果主流程也在用这个变量,两个地方互相覆盖,导致轮换次数完全不对。从那以后,函数内部变量一律加 local,成了我的铁律。

知识体系总览

下面这张图,把今天讲的内容串起来了。你可以把它当作 Shell 脚本的“心法口诀”:

Shell 脚本基础四要素 Shell 脚本基础四要素 Shell 脚本 变量与参数传递 条件判断 if/else 循环结构 for/while 函数定义与调用 位置参数 $1 $2 特殊变量 $# $? 文件测试 -f -d -e 数值/字符串比较 for 遍历列表/范围 while 逐行/条件驱动 函数定义与调用 局部变量 local 核心原则:变量存数据,判断做决策,循环干重复,函数包逻辑 四者结合,就能写出可维护、可复用的自动化脚本

这四样东西,单独看都不难。但把它们组合起来,就能写出真正“聪明”的脚本。我见过太多人把脚本写成“面条代码”——几百行堆在一起,改一个地方要翻半天。用函数拆分逻辑,用变量管理配置,用循环处理批量任务,用条件判断处理异常,这才是专业运维该有的写法。

最后说一句:别怕写脚本时出错。我写过的脚本,没有一千也有八百,每次上线前都会在测试环境跑一遍。Shell 脚本的好处是调试成本低——加个 set -x 就能看到每一步的执行过程。多练、多踩坑,慢慢就熟练了。