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 脚本的好处是调试成本低——加个 set -x 就能看到每一步的执行过程。多练、多踩坑,慢慢就熟练了。