Shell 脚本:别让你的自动化变成“自爆化”

张开发
2026/4/20 1:11:19 15 分钟阅读

分享文章

Shell 脚本:别让你的自动化变成“自爆化”
太长不看版老鸟脚本头#!/bin/bash写死别用#!/bin/sh坑太多。调试bash -x script.sh能看到每一行执行过程。变量引用永远用双引号包起来$var否则空格和通配符会让你怀疑人生。返回值$?是上一条命令的退出码0表示成功。安全set -euo pipefail是脚本的“保命三件套”。我见过一个脚本备份数据库前忘了检查磁盘空间结果把系统盘写爆了。自动化不是写出来就完事要考虑到每一步可能失败。一、你为什么要学 Shell 脚本运维、开发、DevOps只要你和 Linux 打交道Shell 脚本就是你的瑞士军刀。它能帮你每天自动备份日志、数据库批量修改 100 台服务器的配置监控系统资源出问题自动报警把繁琐的手动操作变成一行命令读完这篇你会写出健壮的、可维护的、不会半夜炸醒你的脚本。前置条件会敲基本的 Linux 命令ls、grep、awk。我在 Ubuntu 22.04 和 CentOS 7 上都测过macOS 的 bash 版本旧一点3.2但大部分语法通用。二、脚本的基本骨架直接复制就能用我写脚本时开头一定是这个模板#!/bin/bash set -euo pipefail # 脚本所在目录绝对路径 SCRIPT_DIR$(cd $(dirname ${BASH_SOURCE[0]}) pwd) # 日志函数 log() { echo [$(date %Y-%m-%d %H:%M:%S)] $* } # 错误退出 die() { echo ERROR: $* 2 exit 1 } # 你的逻辑从这里开始 log 脚本开始运行解释一下set -euo pipefail这三个参数我强烈建议每个脚本都加上参数作用不用的后果-e任何命令失败返回值非0立即退出某行报错还继续跑越错越离谱-u引用未定义变量时报错变量拼写错误变成空删错文件-o pipefail管道中只要有一个命令失败整个管道就算失败cat file | grep x即使cat失败grep也返回0你以为成功彩蛋set -x可以在调试时打印每条命令。上线时记得关掉否则日志刷屏。三、变量引用时别偷懒3.1 定义和使用nameTony # 等号两边不能有空格 echo Hello, $name # 输出 Hello, Tony echo Hello, ${name} # 花括号可选但遇到拼接时有用 echo Hello, $name! # 也能工作但 ${name}! 更清晰我踩过的坑$name如果包含空格不加大括号会分裂成多个参数。所以一律用双引号包起来filemy important doc.txt rm $file # 报错rm 收到了 my important doc.txt 三个参数 rm $file # 正确删除一个文件3.2 默认值和替换# 如果变量没定义用默认值 echo ${LOG_DIR:-/var/log} # LOG_DIR 未定义输出 /var/log # 如果变量没定义报错退出 echo ${REQUIRED_VAR:?请设置该变量}四、条件判断if 的写法巨坑[ ]和[[ ]]有什么区别我用[[ ]]因为它支持正则、不会因为变量为空报错。if [[ -f /etc/passwd ]]; then echo 文件存在 fi if [[ $USER root ]]; then echo 你是老大 else echo 你只是个普通人 fi常用文件测试操作符操作符含义-f存在且是普通文件-d存在且是目录-e存在文件或目录-r/-w/-x可读/可写/可执行-s文件非空数字比较别用那是字符串比较if [[ $count -gt 10 ]]; then # -gt, -lt, -eq, -ge, -le echo 超过10 fi五、循环处理批量任务5.1 for 循环# 遍历文件 for file in /var/log/*.log; do echo 处理 $file gzip $file done # C 风格 for ((i1; i10; i)); do echo 第 $i 次 done5.2 while 循环读文件while IFS read -r line; do echo 行内容: $line done data.txtIFS保留行首尾空格-r防止转义。这是标准写法记下来就行。六、函数别把脚本写成两千行# 定义函数 backup_file() { local src$1 local dst$2 cp -p $src $dst || return 1 echo 备份完成: $dst } # 调用 backup_file /etc/nginx/nginx.conf /backup/nginx.conf要点用local声明局部变量避免污染全局。$1、$2是传给函数的参数不是脚本参数。函数返回值用return0~255调用后检查$?。七、错误处理脚本的“安全气囊”7.1 检查命令是否成功if ! mkdir /backup/mydata; then echo 创建目录失败退出 exit 1 fi或者直接用||短路mkdir /backup/mydata || { echo 失败; exit 1; }7.2 捕获退出信号trap这个技巧救过我很多次脚本被中断时清理临时文件。temp_file$(mktemp) # 无论正常退出还是 CtrlC都删除临时文件 trap rm -f $temp_file EXITtrap可以捕获的信号INTCtrlC、TERM、EXIT脚本结束。八、实战例子一个备份脚本把上面知识点串起来。假设我要备份 MySQL 数据库并保留最近 7 天。#!/bin/bash set -euo pipefail # 配置 BACKUP_DIR/backup/mysql DB_NAMEmyapp DB_USERroot DB_PASSyourpassword # 生产环境用 .my.cnf别明文写 RETENTION_DAYS7 # 创建备份目录如果不存在 mkdir -p $BACKUP_DIR # 生成备份文件名 DATE$(date %Y%m%d_%H%M%S) BACKUP_FILE$BACKUP_DIR/${DB_NAME}_${DATE}.sql.gz # 执行备份 mysqldump -u $DB_USER -p$DB_PASS $DB_NAME | gzip $BACKUP_FILE # 检查备份是否成功 if [[ $? -eq 0 -s $BACKUP_FILE ]]; then echo 备份成功: $BACKUP_FILE else echo 备份失败 2 exit 1 fi # 删除超过7天的备份 find $BACKUP_DIR -name ${DB_NAME}_*.sql.gz -mtime $RETENTION_DAYS -delete echo 完成预期输出成功时备份成功: /backup/mysql/myapp_20250125_030001.sql.gz 完成九、常见错误与解决方法错误 1./script.sh: Permission denied原因脚本没有执行权限。解决chmod x script.sh错误 2[: too many arguments原因[ $var value ]中$var是空值或包含多个单词。解决用双引号[ $var value ]或改用[[ ]]。错误 3command not found但命令明明存在原因脚本的环境变量PATH可能不包含/usr/local/bin等。解决在脚本里写绝对路径或开头加export PATH/usr/local/bin:$PATH。错误 4循环读取文件时最后一行丢失原因用while read line但文件最后一行没有换行符。解决用while read line || [[ -n $line ]]。十、几个能让你效率翻倍的彩蛋$()vs 反引号用$(cmd)不要用 cmd 因为前者支持嵌套。${var#pattern}从开头删除最短匹配。filea.b.c.txt${file%.*}得a.b.c${file##*.}得txt。数组arr(a b c)echo ${arr[]}所有元素echo ${#arr[]}个数。read -p交互输入read -p 输入你的名字: name。select菜单不用写一堆 echo直接用select opt in 启动 停止 重启; do case $opt in 启动) echo 启动服务; break ;; 停止) echo 停止服务; break ;; 重启) echo 重启服务; break ;; esac done十一、验证你的脚本水平自己动手写一个脚本要求接受一个参数目录路径。检查目录是否存在不存在则报错退出。找出该目录下所有.log文件统计每个文件的行数输出格式文件名: 行数。将结果保存到report.txt。参考实现先别看写完再对#!/bin/bash set -euo pipefail target_dir${1:-} if [[ -z $target_dir ]]; then echo 用法: $0 目录路径 2 exit 1 fi if [[ ! -d $target_dir ]]; then echo 错误: $target_dir 不是目录 2 exit 1 fi reportreport.txt $report # 清空文件 for logfile in $target_dir/*.log; do if [[ -f $logfile ]]; then lines$(wc -l $logfile) echo $(basename $logfile): $lines 行 $report fi done echo 报告已生成: $reportShell 脚本看着简单但想写得稳、不出幺蛾子得花心思。我写了十年脚本至今还在踩坑。你有什么独门技巧或者惨痛教训欢迎留言分享让大伙儿少走弯路。如果这篇对你有帮助点个赞让更多人看到。

更多文章