Shell 脚本自动化实战:从单行命令到可复用脚本
前面几篇已经覆盖了 find、tar、grep、sed、awk、管道和重定向,但这些工具单独使用时,每次都要重新敲一遍。Shell 脚本的价值就在于把一次性操作变成可重复、可分享、可调度的自动化流程。它不需要学习新语言——脚本里写的就是你平时在终端敲的那些命令,只是加上了变量、条件判断和循环。
最基础的脚本
创建一个文件 backup.sh:
#!/bin/bash
# 备份指定目录到 /tmp/backup
tar -czf /tmp/backup_$(date +%Y%m%d).tar.gz /home/user/documents
echo "备份完成"
赋予执行权限并运行:
chmod +x backup.sh
./backup.sh
第一行的 #!/bin/bash 叫 shebang,告诉系统用哪个解释器执行这个脚本。# 开头的是注释,$(date +%Y%m%d) 是命令替换,会先执行 date 命令再嵌入结果。
实际使用中,高频场景与模式
1. 变量与参数
#!/bin/bash
# 自定义变量
BACKUP_DIR="/tmp/backup"
DATE=$(date +%Y%m%d)
# 接收命令行参数
SOURCE_DIR=$1 # 第1个参数
RETENTION_DAYS=$2 # 第2个参数,默认保留天数
# 参数默认值(如果没传则用默认值)
RETENTION_DAYS=${2:-7}
echo "备份目录: $BACKUP_DIR"
echo "源目录: $SOURCE_DIR"
echo "保留天数: $RETENTION_DAYS"
tar -czf "${BACKUP_DIR}/backup_${DATE}.tar.gz" "$SOURCE_DIR"
./backup.sh /home/user/documents 30
用变量代替硬编码路径,脚本通用性大幅提升。${2:-7} 表示如果第 2 个参数未提供,则默认值为 7。
2. 条件判断
#!/bin/bash
# 检查参数是否提供
if [ -z "$1" ]; then
echo "用法: $0 <目录路径>"
exit 1
fi
# 检查目录是否存在
if [ ! -d "$1" ]; then
echo "错误: 目录 '$1' 不存在"
exit 1
fi
# 检查是否有足够的磁盘空间(简单示例)
AVAILABLE=$(df /tmp | awk 'NR==2 {print $4}')
if [ "$AVAILABLE" -lt 1048576 ]; then # 小于 1GB
echo "警告: /tmp 空间不足"
exit 1
fi
echo "开始备份..."
tar -czf "/tmp/backup_$(date +%Y%m%d_%H%M%S).tar.gz" "$1"
echo "备份完成"
[ ] 是 test 命令的简写形式,常用判断:
| 判断 | 含义 |
|---|---|
-z "$var" |
变量为空 |
-n "$var" |
变量非空 |
-f "$file" |
文件存在且为普通文件 |
-d "$dir" |
目录存在 |
-e "$path" |
路径存在(文件或目录) |
"$a" -eq "$b" |
数值相等 |
"$a" -lt "$b" |
数值小于 |
"$a" -gt "$b" |
数值大于 |
3. 循环处理
#!/bin/bash
# 遍历目录中的每个文件
for file in /var/log/*.log; do
if [ -f "$file" ]; then
size=$(stat -c%s "$file")
echo "$file: ${size} bytes"
fi
done
# 遍历命令行传入的多个参数
for dir in "$@"; do
echo "处理: $dir"
tar -czf "${dir##*/}.tar.gz" "$dir"
done
# while 循环读取文件内容
while IFS= read -r line; do
echo "处理: $line"
done < server_list.txt
"$@" 展开为所有命令行参数(每个参数保持独立),"$*" 则将所有参数合并为一个字符串。遍历文件列表时优先用 "$@"。
4. 函数封装
#!/bin/bash
# 日志函数
log() {
local level=$1
shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*"
}
# 备份函数
backup_dir() {
local source=$1
local dest=$2
if [ ! -d "$source" ]; then
log "ERROR" "源目录不存在: $source"
return 1
fi
log "INFO" "开始备份 $source -> $dest"
tar -czf "$dest" "$source"
local status=$?
if [ $status -eq 0 ]; then
log "INFO" "备份成功"
else
log "ERROR" "备份失败,退出码: $status"
fi
return $status
}
# 主逻辑
backup_dir "/home/user/documents" "/tmp/backup.tar.gz"
backup_dir "/etc/nginx" "/tmp/nginx_backup.tar.gz"
函数让脚本结构清晰,local 声明局部变量避免污染全局命名空间,return 返回退出码供调用方判断。
5. 错误处理与安全选项
#!/bin/bash
set -euo pipefail
# -e: 任何命令返回非零退出码时立即退出
# -u: 使用未定义的变量时报错
# -o pipefail: 管道中任一命令失败则整个管道失败
# 捕获错误并清理
cleanup() {
echo "发生错误,清理临时文件..."
rm -f /tmp/temp_backup_*
}
trap cleanup ERR
# 正常执行
temp_file="/tmp/temp_backup_$$"
cp important_data "$temp_file"
process "$temp_file"
mv "$temp_file" final_output
set -euo pipefail 应成为每个严肃脚本的标配。trap 可以在脚本退出(正常或异常)时执行清理操作,$$ 是当前进程 ID,用于生成唯一的临时文件名。
6. 结合前面学过的工具
#!/bin/bash
set -euo pipefail
LOG_DIR="/var/log/nginx"
REPORT="/tmp/nginx_report_$(date +%Y%m%d).txt"
{
echo "=== Nginx 日志分析报告 ==="
echo "生成时间: $(date)"
echo ""
echo "--- 访问量最高的 10 个 IP ---"
awk '{print $1}' "$LOG_DIR"/access.log.* | sort | uniq -c | sort -rn | head -10
echo ""
echo "--- 各状态码分布 ---"
awk '{print $9}' "$LOG_DIR"/access.log.* | sort | uniq -c | sort -rn
echo ""
echo "--- 404 错误最多的 URL ---"
awk '$9 == "404" {print $7}' "$LOG_DIR"/access.log.* | sort | uniq -c | sort -rn | head -10
echo ""
echo "--- 日志文件总大小 ---"
du -sh "$LOG_DIR"/access.log.*
} > "$REPORT"
echo "报告已生成: $REPORT"
用 { ... } > file 将一组命令的输出统一重定向到文件,避免每条命令都写一次 >>。
7. 定时任务(cron)
# 编辑 crontab
crontab -e
# 每天凌晨 2 点执行备份
0 2 * * * /home/user/scripts/backup.sh /home/user/documents /tmp/backups
# 每小时检查一次磁盘空间
0 * * * * /home/user/scripts/check_disk.sh
# 每周一清理 7 天前的日志
0 3 * * 1 find /var/log -name "*.log" -mtime +7 -delete
cron 格式:分 时 日 月 星期 命令。脚本中建议使用绝对路径,因为 cron 执行时环境变量很少。
8. 调试技巧
# 语法检查(不执行)
bash -n script.sh
# 逐步跟踪执行(显示每条命令及其参数)
bash -x script.sh
# 在脚本中局部开启调试
set -x # 开启调试
some_command # 这行会被打印出来
set +x # 关闭调试
bash -x 输出中 + 开头的行是实际执行的命令,非常适合排查"为什么变量值是空的"这类问题。
如果不知道如何下手,可以先尝试
#!/bin/bash
echo "当前时间: $(date)"
echo "当前用户: $(whoami)"
echo "当前目录: $(pwd)"
echo "磁盘使用:"
df -h /
保存为 info.sh,chmod +x info.sh,然后 ./info.sh 运行。确认能正常输出后,逐步加入变量、参数和条件判断。
或者,写一个简单的清理脚本:
#!/bin/bash
set -euo pipefail
TARGET_DIR=${1:-/tmp}
DAYS=${2:-7}
echo "清理 $TARGET_DIR 中 ${DAYS} 天前的临时文件..."
find "$TARGET_DIR" -type f -name "*.tmp" -mtime +"$DAYS" -delete
echo "清理完成"
脚本编写最佳实践
- 始终用
set -euo pipefail:让错误尽早暴露,而不是悄悄继续执行。 - 变量加双引号:
"$var"而不是$var,防止空格和通配符导致意外展开。 - 用绝对路径:脚本被 cron 或其他程序调用时,工作目录可能不是预期的。
- 关键操作先预览:删除、移动、覆盖类操作,先用
echo打印会执行什么,确认无误后再真正执行。 - 退出码有意义:成功返回 0,失败返回非零值,方便被其他脚本调用时判断。
- 写注释和用法说明:三个月后你自己也会忘记这个脚本是干嘛的。
- 点赞
- 收藏
- 关注作者
评论(0)