ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Shell脚本自动化实战:从基础语法到日志备份与定时任务

2026/9/28 5:58:02 拓冰建站 浏览量
Shell脚本自动化实战:从基础语法到日志备份与定时任务 1. 为什么是Shell脚本先想清楚它能解决什么问题以前我在公司负责一套内部服务的日常维护每天最烦的就是那几件固定的事凌晨要备份日志、每周要清理过期的临时文件、偶尔还要批量把几十台服务器的某个配置统一改掉。一开始全是手动操作一台一台登录一条一条命令敲稍微忙一点就容易漏。后来我把这些操作全部写成了Shell脚本配合定时任务每周能省下来大半天的时间而且再也没有因为手误敲错命令导致的问题。Shell脚本说白了就是把一串Linux命令按顺序写进一个文件里让机器替你去执行。它的价值不在于能做什么惊天动地的事而在于把重复、固定、容易出错的操作固化下来变成一条命令、一个双击、一个定时任务。对于运维、后端开发、自动化测试工程师甚至经常跟服务器打交道的产品和技术支持来说这都是值得花一两个周末掌握的基础技能。这篇教程不会跟你扯太深的理论我会从环境准备开始带你写出第一个可执行脚本然后逐步覆盖循环、条件、函数、文本处理这些核心语法再通过日志备份、后台执行、交互式密码处理这些真实场景把脚本从能跑推进到能自动化地跑。每一节我都会把当初自己踩过的坑标出来这些才是文档里通常不会写的东西。2. 环境准备与第一个脚本从Hello World到随手可用的工具2.1 先搞清楚你用的到底是哪个Shell打开终端先执行这条命令echo $SHELL输出一般是/bin/bash也可能是/bin/zsh。macOS 从 Catalina 之后默认成了 zsh但绝大多数 Linux 发行版和服务器上仍然是 bash。这两个在基础语法上差别不大教程里所有例子我都按 bash 写你在 zsh 下跑也基本没问题。Windows 用户想学 Shell 脚本建议直接装 WSL或者用 Git Bash 凑合着跑。我个人更推荐 WSL因为它的行为跟真实 Linux 环境一致后面你写的脚本要部署到服务器上时不会有兼容性意外。确认完环境之后还有一件容易忽略的事你写的脚本文件必须是UTF-8 编码行尾换行符是LFUnix 换行不要是 CRLF。Windows 上编辑过的脚本直接传到 Linux 上经常报bad interpreter错误多半就是换行符问题。这个坑我当年刚入行时踩过排查了半天才发现是 Notepad 默认存成了 CRLF。2.2 第一个脚本Shebang、执行权限、运行方式新建一个文件hello.sh写入以下内容#!/bin/bash # 我的第一个Shell脚本输出一段问候 nameShell新手 echo 你好$name欢迎进入脚本世界。然后执行chmod x hello.sh ./hello.sh这里有三件事必须搞清楚第一#!/bin/bash叫 Shebang它告诉系统用哪个解释器来执行这个文件。如果你不写这一行直接./hello.sh会默认用你当前的 Shell 去解释一旦脚本里用到某个特定解释器的语法就会出问题。所以规范做法是每个脚本第一行都写清楚#!/bin/bash。第二chmod x是给文件加执行权限。很多新手第一反应是我直接bash hello.sh也能跑为什么要加执行权限——这样确实能跑但脚本就失去了可执行程序的属性没法放进/usr/local/bin里当命令用也没法在某些工具里直接被调用。如果你想长期使用某个脚本给它加执行权限是必须的。第三执行时要写./hello.sh而不是hello.sh。因为系统只会在 PATH 环境变量记录的目录里找命令当前目录默认不在其中。除非你把脚本放进/usr/local/bin或改了 PATH否则必须显式带上./前缀。2.3 变量与引号新手最容易翻车的第一个地方Shell 的变量定义很简单变量名值一句话的事但有三条规则我建议你死记硬背等号两边不能有空格name 张三是错的。变量名只能用字母、数字、下划线且不能以数字开头。取值的时候要在变量名前加$比如echo $name严谨一点写成${name}。关于引号单引号和双引号的区别是初学者最容易困惑的点。我举个例子你就明白了name张三 echo 你好$name # 输出你好$name echo 你好$name # 输出你好张三单引号里的内容原样输出$ 符号不会被解析双引号里的变量和命令会被展开。这个区别在拼接路径、传参时非常关键。我建议不用变量展开的地方优先用单引号防止 Shell 帮你做了意外的替换。还有一个常用技巧是命令替换把一条命令的输出存进变量。写法是$(命令)比如current_dir$(pwd) echo 当前目录是$current_dir注意这个场景下不要用反引号早期写法pwd在嵌套时会有解析问题$()的规则更清晰也支持嵌套。3. 核心语法一次讲透条件、循环、函数怎么组合出自动化逻辑3.1 for循环批量处理是最常见的自动化场景Shell 脚本里 for 循环的使用频率极高因为它天生适合处理一批东西一批文件、一批服务器、一批日志。最基本的语法是这样for i in 1 2 3 4 5; do echo 第 $i 次执行 done但实际工作中很少有人会写死这个列表更多是从命令输出里取。比如遍历当前目录下所有.log文件for file in *.log; do echo 处理文件$file # 这里可以追加你的处理逻辑 done如果你要处理的是数字序列比如生成 1 到 100用seq配合for i in $(seq 1 100); do echo 序号$i done再进阶一点的场景是同时遍历两个列表比如批量把old_前缀的文件改名为new_前缀for old in old_*.txt; do new${old/old_/new_} mv $old $new echo $old 已重命名为 $new done这里${old/old_/new_}是 Shell 的字符串替换语法把变量里的old_替换成new_不需要额外调用 sed非常快。注意变量展开一定要加双引号包起来比如mv $old $new否则文件名里一旦有空格就会拆成多个参数命令直接错乱。3.2 条件判断让脚本学会看情况办事自动化脚本不能只会一条道走到黑得能根据情况做不同处理。if 判断的基本结构if [ 条件 ]; then # 条件成立时执行 elif [ 条件2 ]; then # 条件2成立时执行 else # 以上都不成立时执行 fi注意[和]两边必须有空格这是 test 命令的语法要求新手常常写成[条件]导致报错。条件判断最常见的是文件判断和字符串比较。给你一张表这些是我用频率最高的表达式含义典型场景-f 文件是否为普通文件判断配置文件是否存在-d 目录是否为目录判断备份目录是否创建过-e 路径路径是否存在通用存在性检查-z 字符串字符串是否为空检查参数是否传了-n 字符串字符串是否非空检查环境变量是否有值字符串1 字符串2字符串相等判断输入值数字1 -gt 数字2数字大于对比日志文件个数日常场景举个例子检查备份目录存在与否不存在就创建backup_dir/data/backup if [ ! -d $backup_dir ]; then mkdir -p $backup_dir echo 备份目录不存在已自动创建 fi这里的!表示取反-d判断目录组合起来就是如果不存在则创建。这套判断逻辑在自动化脚本里是标配。3.3 函数与返回值把常用逻辑沉淀下来脚本写多了你会发现很多片段反复出现比如记录带时间戳的日志、检查上一条命令是否成功。这时候就该封装成函数。定义函数有两种写法我习惯用带function关键字的function log_info() { local message$1 echo [$(date %Y-%m-%d %H:%M:%S)] [INFO] $message } log_info 脚本开始运行 log_info 备份完成这里有两个关键点。第一local声明局部变量防止函数内部的变量污染全局作用域——脚本里的全局变量一旦被意外改动排查起来非常折磨人。第二$1、$2是传给函数的第一个、第二个参数函数内部用位置参数拿值。至于返回值Shell 函数里的return只能返回 0~255 的整数0 代表成功非 0 代表失败。这个返回值可以通过$?拿到function check_disk() { local usage$(df / | awk NR2 {print $5} | tr -d %) if [ $usage -gt 80 ]; then return 1 fi return 0 } if check_disk; then echo 磁盘空间正常 else echo 磁盘空间超过80%需要关注 fi这个例子顺便展示了函数化的好处判断逻辑藏在函数里主流程只用关心通过还是不通过脚本的可读性一下就上来了。4. 文本处理是Shell的灵魂grep、sed、awk的配合使用场景4.1 grep在Shell脚本中的常见用法如果说循环是自动化脚本的骨架那文本处理就是血肉。服务器上的日志、配置文件、命令输出全是文本你要从中筛出有用的信息grep 是第一个要掌握的。grep 最常见的用法就是按关键字过滤grep ERROR app.log但脚本里用得更多的往往是这几个变体# 递归搜索目录下所有文件-n显示行号 grep -rn error /opt/config/ # 扩展正则匹配多个关键字 grep -E ERROR|WARN app.log # 只输出匹配的部分而不是整行配合提取信息 grep -o [0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\} app.log # 统计匹配行数 grep -c timeout app.log在脚本里grep 最常用的场景是判断某个特征是否存在if grep -q 启动完成 server.log; then echo 服务启动成功 else echo 未检测到启动完成标志需要检查 fi这里的-qquiet很重要——它只关心匹配结果不输出任何内容这样脚本里就不会被多余输出干扰。grep命令本身可以用在 if 条件里匹配到了返回 0否则返回 1直接作为判断依据。4.2 sed做批量替换改配置不再用手工编辑改配置文件是自动化高频场景。比如要把所有配置文件里的http://统一改成https://sed -i s#http://#https://#g /opt/nginx/conf/*.conf这里-i表示原地修改s是替换命令。注意我用#替代了常用的/作为分隔符因为 URL 本身就有斜杠直接用/得写一堆转义可读性很差。这个技巧是我自己实践出来的看别人脚本时也经常见。sed 另一个常用场景是删除固定行。比如清理 crontab 里的某个过期任务sed -i /delete_old_data.sh/d /etc/crontabd表示删除把匹配到delete_old_data.sh的那一行从文件里删除。4.3 awk做列分析提取你真正关心的字段awk 是一个完整的文本分析语言功能极强但脚本里常用的其实就那么三板斧按分隔符切分行取特定列。最经典的是分析访问日志提取每个 IP 的访问次数awk {print $1} access.log | sort | uniq -c | sort -rn$1表示第一列默认按空格或制表符分隔。sort | uniq -c统计次数再sort -rn按次数倒序。这条命令在排查谁在频繁请求时特别好使。更灵活的是指定分隔符比如处理/etc/passwd它是冒号分隔的awk -F: {print $1, $7} /etc/passwd-F:指定冒号为分隔符这条命令打印出每行第一个字段用户名和第七个字段登录 Shell。再配合条件过滤比如找出 CPU 占用过高的进程ps aux | awk $3 50 {print $2, $3, $11}只输出CPU占用超过50%的进程的 PID、占用率和命令名。这三个工具的定位我帮你理一下在做选择时思路会很清楚工具核心能力适用场景grep按条件筛选行从日志中找关键字、统计匹配次数sed对行做替换、删除、插入批量修改配置、清理脏数据awk按列做提取、统计、格式重排日志分析、性能指标提取实际脚本里它们经常串联用。比如统计当天日志里出现次数最多的 5 类错误grep $(date %Y-%m-%d) app.log | grep -E ERROR|Exception | awk -F[][] {print $4} | sort | uniq -c | sort -rn | head -5这条命令把日期过滤、关键字匹配、字段提取、排序统计一次做完。建议你把这几个工具当作管道里的积木来理解——每条命令处理完把结果丢给下一条比记住一堆参数更接近脚本思维。5. 实战案例日志备份脚本从零到上线5.1 先把需求拆清楚自动化脚本最忌讳一上来就写代码。我们先拆解一个真实的日志备份需求每天凌晨把/var/log/myapp/下的日志打包成 tar.gz。备份文件按日期命名方便追溯。只保留最近 7 天的备份过期自动删除。这个需求拆完就三个动作打包、命名、清旧。每一步都很简单但组合起来就是一个能挂在生产环境跑的自动化任务。5.2 获取当前日期时间并转换为数字串备份文件命名最常见的方式就是用日期时间戳。Shell 里一条date命令搞定date %Y%m%d_%H%M%S输出形如20250115_230430。这个数字串的好处是不带特殊字符排序就是时间顺序文件管理器里看名字就知道是什么时候的备份。这在日志备份、数据库导出、发布包命名里都是标配。如果你想要可读性更强的格式比如2025-01-15_23-04-30可以写成date %Y-%m-%d_%H-%M-%S我建议脚本里统一用数字串命名因为后面基于sort、find做文件管理时不会有意外。5.3 完整脚本与注释#!/bin/bash # 日志备份脚本 # 用法./backup_logs.sh set -euo pipefail LOG_DIR/var/log/myapp BACKUP_BASE/data/backup KEEP_DAYS7 STAMP$(date %Y%m%d_%H%M%S) BACKUP_FILE$BACKUP_BASE/myapp_$STAMP.tar.gz # 1. 创建备份目录 mkdir -p $BACKUP_BASE # 2. 打包日志目录 tar -czf $BACKUP_FILE $LOG_DIR # 3. 检查打包是否成功 if [ $? -ne 0 ]; then echo 打包失败退出 exit 1 fi # 4. 删除超过保留天数的备份文件 find $BACKUP_BASE -name myapp_*.tar.gz -mtime $KEEP_DAYS -delete echo 备份完成$BACKUP_FILE这里面有几句值得专门说明。第一set -euo pipefail是每个脚本我都建议放在开头的三连。-e让脚本在任意命令报错时立即退出-u让未定义的变量直接报错而不是静默变成空字符串-o pipefail让管道里任何一条命令失败都算整体失败。这三条能拦下大量低级错误。但要注意-e在 if 条件里的命令不生效比如if grep -q ...就算 grep 没匹配到返回 1也不会让脚本退出——因为它在 if 条件上下文里。第二find ... -mtime $KEEP_DAYS -delete里的-mtime 7表示修改时间超过 7 天的文件。对这个需求来说修改时间基本等于备份时间因为我们生成备份文件后就不再动它。这个清理方式比手动rm一个个删安全得多。5.4 挂到crontab里实现真正的自动化脚本写好只是第一步让它自己按时跑才是自动化。用crontab -e编辑当前用户的定时任务加入这一行0 3 * * * /opt/scripts/backup_logs.sh /var/log/backup_logs_history.log 210 3 * * *表示每天凌晨 3 点整执行。日志重定向到backup_logs_history.log标准输出和错误都记录进去方便事后排查。这里有个经验定时任务的脚本路径一定用绝对路径环境变量需要单独加载。因为 cron 执行时的环境跟你手动登录终端是完全不同的PATH 很短没有加载~/.bashrc。如果你的脚本里用到自定义命令或特定环境变量要么在脚本开头显式 export要么直接写绝对路径。我早期写过不少手动执行正常一进 crontab 就失败的脚本统统是环境变量问题。6. 后台执行与交互输入两个高频场景的坑与解法6.1 用 把任务放到后台别堵住你的终端遇到耗时任务比如大文件传输、批量编译你肯定不想一直开着终端等。Shell 的就是把命令放到后台执行的意思/opt/scripts/deploy.sh /tmp/deploy.log 21 命令末尾的让这个脚本在子 Shell 里后台运行终端立刻恢复可操作。输出重定向到日志文件21把错误输出也合并到同一个文件防止报错信息丢失。但这里有个经典坑直接启动的后台任务在你关闭终端时可能会被 SIGHUP 信号杀掉。解决办法是nohupnohup /opt/scripts/deploy.sh /tmp/deploy.log 21 nohup的含义就是忽略挂断信号。在服务器上用 SSH 执行部署脚本时这个组合几乎是标准姿势否则 SSH 断开部署就中断了。6.2 后台脚本还要交互式输入密码怎么办真实场景里经常遇到这个问题你想后台执行一条 scp 或者 rsync 命令但它要交互式输密码没法在后面直接给。第一种方案如果密码是人为输入的用read -s加超时保护read -s -p 请输入密码 password echo if [ -z $password ]; then echo 密码不能为空 exit 1 fi-s让输入不回显-p输出提示文字。这个方式适合需要人在场的场景。第二种方案完全无人值守的传输场景可以用 expect 脚本来模拟交互#!/usr/bin/expect set timeout 30 spawn scp /data/backup.tar.gz userremote-host:/data/ expect { password: { send YourPassword\r } timeout { puts 连接超时 exit 1 } } expect eofexpect 的原理是监听输出匹配到提示符就发送预定义的输入本质是交互自动化。注意 expect 脚本不一定随系统预装Debian 系需要apt install expectRedHat 系用yum install expect。第三种方案是很多运维脚本实际在用的直接配置 SSH 密钥认证彻底绕开密码。把本机公钥加到目标机器的~/.ssh/authorized_keys里之后 scp、ssh 都不用密码了脚本才能真正做到无人值守。说实话如果是在生产环境做自动化传输我强烈建议走密钥这条路而不是在脚本里硬编码密码——密码写在脚本里有泄露风险密钥则可以单独设置 passphrase 配合 ssh-agent 管理。6.3 后台任务的管理jobs、fg、bg、kill把任务放后台后你还需要管理它。几个常用命令jobs # 查看当前终端有哪些后台任务 fg %1 # 把编号1的任务调回前台 bg %1 # 让暂停的任务继续在后台跑 kill %1 # 按任务编号终止 kill 12345 # 按PID终止调试脚本时我经常遇到的情况是脚本其实在等一个输入你没注意到任务就挂在那儿了。这时候jobs看到的是Stopped状态fg把它调回前台就能看到它在等你什么。这类交互问题排查时先看任务状态比瞎猜有用得多。7. 从脚本到自动化体系Shell能怎么融入更大的工作流7.1 在自动化测试中的定位很多自动化测试工程师以为 Shell 脚本跟自己的工作无关其实恰恰相反。在接口自动化和 UI 自动化里Shell 脚本最常承担三类脏活准备测试数据、清理测试环境、执行批量回归。比如跑 pytest 之前先执行一个准备脚本创建测试账号、灌入基础数据、启动被测服务#!/bin/bash # 测试前准备 set -e # 1. 重置测试数据库 mysql -u tester -pTestPass -e DROP DATABASE IF EXISTS test_db; CREATE DATABASE test_db; # 2. 启动被测服务 nohup /opt/app/start_server.sh /tmp/app.log 21 sleep 5 # 3. 验证服务健康 if curl -s http://127.0.0.1:8080/health | grep -q ok; then echo 服务启动成功开始测试 else echo 服务启动失败终止 exit 1 fi这套准备—启动—健康检查—失败即停的流程是任何自动化测试执行前的标准姿势。没有这层保障测试脚本跑出来的错误可能跟被测代码毫无关系纯粹是环境没准备好。7.2 在自动化运维中的进阶方向如果你的工作跟服务器打交道比较多Shell 脚本是接触自动化运维的起点。很多人第一个自动化运维实践就是写一个批量检查脚本SSH 到多台服务器执行df -h、uptime、free -m收集结果汇总输出。#!/bin/bash # 批量检查服务器资源 servers192.168.1.11 192.168.1.12 192.168.1.13 for host in $servers; do echo $host ssh $host df -h / | tail -1; free -m | grep Mem; uptime done等你跑通了这类脚本再接触 Ansible、SaltStack 这类配置管理工具时会容易得多因为你对批量、并发、幂等这些概念已经有了切身体会。脚本是用代码去驱动机器工具则是把机器再抽象一层基础打牢了工具上手只是时间问题。7.3 调试与质量保障的好习惯最后分享几个让我少掉头发的调试习惯。第一调试用bash -xbash -x script.sh这个命令会逐行打印脚本执行过程每个变量展开后的值都会显示出来。遇到脚本跟我预期不一致的诡异问题这是第一排查手段。也可以在脚本里临时加set -x开启set x关闭只打印你需要看的那段。第二用 ShellCheck 做静态检查。它能在你没运行脚本之前就发现变量没加引号、判断条件写反、管道错误这类问题。很多在bash -x里肉眼半天看不出来的坑ShellCheck 一秒帮你标出来。安装就是个包管理器的事值得花十分钟配置到编辑器里。第三脚本里所有路径变量用双引号包住所有可能包含空格的路径都别裸奔。这个习惯坚持久了你能避开至少一半的脚本 bug。我自己的体会是Shell 脚本的学习曲线其实很平缓语法来回就那些真正的成长来自一次次把重复劳动交给脚本之后的满足感。你看完这篇之后最值得做的动作不是背语法而是找一件自己日常工作中最烦的重复事试着用脚本解决掉它——做完那一件你就彻底入门了。