Shell脚本实战:从变量循环到三剑客,搞定Linux自动化运维
这次我们来看一套 Shell 脚本编程实战内容,主题是变量、循环、函数和文本三剑客。网上讲 Shell 的教程很多,但不少是零散命令的拼凑,拿到运维场景里根本连不成一条能跑的链路。这套内容的价值在于:它把编写 Linux 自动化任务最常用的语法和工具串起来了,不是“看懂语法”就结束,而是能直接写出批量处理、日志分析、服务巡检一类的脚本。
先交代清楚目标读者。如果你正在做运维、云计算运维、桌面运维,或者刚入门 Linux 想要一条更高效的学习路径,这篇文章可以直接收藏。正文会从环境准备开始,逐步讲变量、条件判断、循环、函数,再重点拆解 grep、sed、awk 这三个文本处理工具,最后落到批量任务、日志清理、定时任务、API 调用这类真实场景。看完之后,你至少能独立写出一个带参数校验、带日志输出、能定时执行的脚本。
文章不会停留在“列命令”这个层面,而是会给出完整示例、执行思路和常见错误。每段代码都标注了语言,直接复制到你的 Linux 环境就能跑。涉及生产环境的部分,会明确给出使用边界,避免把一条没验证的脚本丢到线上机器里造成事故。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 适用平台 | Linux / Unix 环境,主流发行版均可 |
| 编程语言 | Bash / Shell |
| 核心内容 | 变量、条件判断、循环、函数、grep/sed/awk 三剑客 |
| 启动方式 | Bash 解释器直接执行,无需安装额外框架 |
| 硬件要求 | 无 GPU 需求,纯 CPU 环境即可 |
| 是否支持批量任务 | 支持,适合批量文件处理、日志分析、服务巡检、备份清理 |
| 是否支持 API 接入 | 可通过 curl 等命令调用 HTTP 接口 |
| 是否支持定时调度 | 支持,配合 cron 定时任务 |
| 学习成本 | 较低,掌握基础命令后即可上手 |
| 适合场景 | Linux 运维、自动化测试、日志分析、云计算运维、桌面运维 |
从应用场景看,Shell 脚本是 Linux 环境下最直接的自动化手段。不需要安装庞大的运行环境,只要系统里有 Bash,就能写、能跑、能接到 cron 里做周期任务。
2. 适用场景与使用边界
先说适合做什么。日常运维里有大量重复操作,比如检查多台服务器的存活状态、清理过期日志、批量重命名文件、提取日志里的 IP 和状态码、统计接口响应时间,这些都可以用 Shell 脚本快速完成。Shell 的哲学是“把命令串起来”,所以它特别适合处理文本、文件和进程类任务。
再说哪些场景不建议用 Shell。如果遇到非常复杂的 JSON 数据处理、多线程并发计算、图形界面程序、大规模分布式任务调度,更稳妥的选择是 Python、Go 或专门的编排工具。Shell 脚本一旦写得过于复杂,调试成本会明显上升,不如直接用更现代的语言维护。
使用边界要特别注意三件事:
第一,不要在未测试的情况下直接在生产环境运行脚本。Shell 脚本里很多命令是不可逆的,例如rm -rf、重定向覆盖文件、批量执行远程命令,先在小范围或测试机上验证。
第二,涉及批量远程操作、端口扫描、数据抓取时,确保你有目标主机或系统的合法授权。未授权的扫描、爆破、批量探测可能违反安全合规要求。
第三,不要在脚本里硬编码明文密码、密钥或敏感数据。如果必须调用凭据,优先使用环境变量、加密配置或密钥管理工具。日志输出也要避免包含敏感信息。
Shell 脚本是运维利器,但越强的工具越要控制使用边界。写脚本之前先想清楚:这个任务是否适合 Shell、会不会影响现有服务、失败后如何回滚。
3. 环境准备与前置条件
Shell 脚本开发对硬件要求非常低,你只需要一台安装了 Linux 的机器或虚拟机,即使是云服务器、树莓派、Windows WSL 也都可以。现在常见的 Linux 发行版如 Ubuntu、Debian、CentOS、Rocky Linux 都自带 Bash。
先确认当前环境的 Shell 类型和版本:
echo $SHELL bash --version正常情况下输出里会显示/bin/bash或/usr/bin/bash,以及 Bash 的版本号。如果系统中同时存在 zsh、sh 或其他 Shell,本文的脚本统一按照 Bash 语法编写。
开发 Shell 脚本不需要专门的 IDE,但推荐使用支持语法高亮的编辑器,比如 vim、nano、VS Code。如果是远程服务器,直接用 vim 编辑最方便。
检查系统里是否具备常用命令:
which grep sed awk curl tar crontab这些命令在大多数发行版中都是默认安装的。如果提示缺少某个命令,可以根据系统包管理器安装。例如:
# Ubuntu / Debian sudo apt update sudo apt install -y grep sed gawk curl cron # CentOS / Rocky / RHEL sudo yum install -y grep sed gawk curl cronie创建一个专门存放脚本的目录,避免散落在用户目录或系统目录:
mkdir -p ~/shell-practice cd ~/shell-practice这个目录就是我们的实验区。后续创建的脚本都放在这里,保持文件整洁,也方便测试权限和路径。
第一个脚本很简单,验证环境是否可用:
#!/bin/bash echo "Hello, Shell" echo "当前 Shell: $SHELL" echo "当前用户: $(whoami)"写入文件后,需要通过chmod添加执行权限,再运行:
chmod +x ~/shell-practice/first.sh ~/shell-practice/first.sh看到三行输出,说明环境完全正常。
4. Shell 变量与基础语法
4.1 变量定义与引用
Shell 变量不需要声明类型,赋值时注意等号两边不能有空格:
#!/bin/bash name="alice" age=26 echo "姓名: $name" echo "年龄: ${age}岁"变量名由字母、数字、下划线组成,不能以数字开头。使用$变量名或${变量名}引用变量,推荐使用花括号写法,便于在拼接字符串时明确边界。
Shell 还支持只读变量:
#!/bin/bash readonly VERSION="1.0.0" VERSION="2.0.0" # 这里会报错4.2 特殊变量
脚本执行时,系统会传入一些特殊变量,它们在自动化脚本中非常常用:
| 特殊变量 | 含义 |
|---|---|
$0 | 当前脚本的文件名 |
$1、$2 | 第 1、2 个位置参数 |
$# | 参数个数 |
$@ | 所有参数列表 |
$? | 上一条命令的退出码,0 表示成功 |
$$ | 当前进程 PID |
写一个脚本演示:
#!/bin/bash echo "脚本名称: $0" echo "第一个参数: $1" echo "第二个参数: $2" echo "参数数量: $#" echo "所有参数: $@" echo "上一条命令退出码: $?"执行测试:
bash args.sh hello world输出会依次显示脚本名、传入的两个参数、参数数量和退出码。这类特殊变量在写带参脚本时是基础中的基础。
4.3 字符串处理
字符串拼接和截取是脚本开发中最常见的操作之一:
#!/bin/bash str="hello, linux shell" echo "长度: ${#str}" echo "截取前5个字符: ${str:0:5}" echo "截取从第7个字符开始: ${str:7}" echo "替换第一个 linux 为 Linux: ${str/linux/Linux}" echo "替换所有 l 为 L: ${str//l/L}"4.4 算术运算
Bash 内置的算术运算使用$(( )),不要直接写1 + 1让它计算,那会被当作字符串:
#!/bin/bash a=10 b=3 echo "加法: $((a + b))" echo "减法: $((a - b))" echo "乘法: $((a * b))" echo "除法取整: $((a / b))" echo "取余: $((a % b))"如果脚本里有比较复杂的数学逻辑,也可以借助awk或 Python,但简单的运算用$(( ))就够。
4.5 用户输入与交互
使用read命令读取用户输入,适合写成半交互式的运维脚本:
#!/bin/bash read -p "请输入要检查的服务名: " service_name echo "你输入的是: $service_name"如果要实现静默输入,比如输入密码,可以加-s参数:
read -s -p "请输入密码: " password echo "" echo "密码长度: ${#password}"4.6 命令替换
命令替换是把命令的输入结果保存到变量里,这是 Shell 自动化的核心能力之一:
#!/bin/bash current_date=$(date +%Y-%m-%d) current_dir=$(pwd) echo "今天日期: $current_date" echo "当前目录: $current_dir"旧式写法使用反引号`,不推荐。$( )更清晰,且支持嵌套。例如:
hours=$(($(date +%H) + 0)) echo "当前小时: $hours"5. 条件判断与流程控制
5.1 if / elif / else
Shell 的条件判断语法要注意方括号前后的空格,少了空格会直接报错:
#!/bin/bash read -p "请输入一个数字: " num if [ "$num" -gt 10 ]; then echo "这个数大于 10" elif [ "$num" -eq 10 ]; then echo "这个数等于 10" else echo "这个数小于 10" fi数字比较用以下运算符:-eq等于、-ne不等于、-gt大于、-lt小于、-ge大于等于、-le小于等于。
字符串比较使用=或!=:
#!/bin/bash str1="hello" str2="world" if [ "$str1" = "$str2" ]; then echo "两个字符串相等" else echo "两个字符串不相等" fi if [ -z "$str1" ]; then echo "字符串为空" fi5.2 文件判断
Linux 运维脚本里大量使用文件判断,例如检查配置文件是否存在、目录是否可写、日志文件是否为空:
| 表达式 | 含义 |
|---|---|
-f "$file" | 是否为普通文件 |
-d "$dir" | 是否为目录 |
-e "$path" | 路径是否存在 |
-s "$file" | 文件是否非空 |
-r "$file" | 是否可读 |
-w "$file" | 是否可写 |
-x "$file" | 是否可执行 |
#!/bin/bash path="/etc/nginx/nginx.conf" if [ -f "$path" ]; then echo "文件存在" else echo "文件不存在" fi if [ -r "$path" ]; then echo "文件可读" fi5.3 逻辑运算符
组合多个条件时,用&&表示与、||表示或、!表示非:
#!/bin/bash age=25 city="beijing" if [[ "$age" -ge 18 && "$city" == "beijing" ]]; then echo "条件满足" fi这里使用了[[ ]],它相比[ ]支持更丰富的语法,比如正则匹配,推荐在 Bash 脚本中优先使用:
#!/bin/bash url="https://example.com/api/v1" if [[ "$url" =~ ^https:// ]]; then echo "以 https 开头" fi5.4 case 多分支
case 适合处理有限分支场景,比如根据用户传入参数选择执行不同逻辑:
#!/bin/bash action=$1 case "$action" in start) echo "启动服务" ;; stop) echo "停止服务" ;; restart) echo "重启服务" ;; *) echo "用法: $0 {start|stop|restart}" exit 1 ;; esaccase 的写法比一串 if/elif 更清晰,尤其是在写服务管理脚本时非常有效。
5.5 退出码与安全保护
Shell 脚本里exit可以主动退出并返回状态码。后面配合$?可以判断脚本是否成功:
#!/bin/bash if [ ! -d "/var/log/myapp" ]; then echo "目录不存在,创建中" mkdir -p /var/log/myapp if [ $? -ne 0 ]; then echo "创建目录失败" exit 1 fi fi echo "目录准备完成"另外,在脚本开头加一句set -e,可以让脚本在任何一条命令出错时立即退出,避免错误被忽略;set -u可以检查未定义变量;set -o pipefail可以防止管道中错误被吞掉。推荐在正式脚本中这样写:
#!/bin/bash set -euo pipefail这行配置相当于给脚本加了一层保护,很多“诡异问题”都能在第一时间暴露出来。
6. 循环结构与批量任务
6.1 for 循环
for 循环可以遍历数字列表、文件列表、命令行参数等。最常见的用法是遍历指定区间:
#!/bin/bash for i in {1..5}; do echo "第 $i 次循环" done遍历文件列表是运维常用场景:
#!/bin/bash for file in /tmp/*.log; do echo "处理文件: $file" done批量重命名文件,例如把所有.txt后缀改成.bak:
#!/bin/bash for file in *.txt; do mv "$file" "${file%.txt}.bak" echo "重命名: $file -> ${file%.txt}.bak" done这里使用了变量替换${file%.txt},作用是把后缀.txt去掉。如果文件名中包含空格,必须加引号。
6.2 while 循环
while 循环适合在满足条件时反复执行。最典型的场景是逐行读取文件:
#!/bin/bash while read -r line; do echo "读取到: $line" done < /etc/hostname也可以用 while 做计数循环:
#!/bin/bash count=1 while [ "$count" -le 5 ]; do echo "count=$count" count=$((count + 1)) done6.3 无限循环与后台运行
一些巡检或监控类脚本需要持续运行,可以使用无限循环配合sleep控制频率:
#!/bin/bash while true; do echo "$(date +%F\ %T) 监控中,内存使用率: $(free -h | awk '/^Mem/{print $3}')" sleep 5 done这种脚本不要直接在前台跑,可以用nohup放到后台运行,把输出写入日志:
nohup bash monitor.sh > monitor.log 2>&1 &6.4 循环控制 break / continue
break 跳出整个循环,continue 跳过当前循环剩余部分进入下一轮:
#!/bin/bash for i in {1..10}; do if [ "$i" -eq 3 ]; then echo "跳过数字 3" continue fi if [ "$i" -eq 7 ]; then echo "到达 7,退出循环" break fi echo "当前数字: $i" done6.5 循环内使用后台并发
如果有大量批量任务,顺序执行太慢,可以在循环内用&后台执行,再用wait等待所有任务结束:
#!/bin/bash for ip in 192.168.1.1 192.168.1.2 192.168.1.3; do ( ping -c 1 -W 1 "$ip" > /dev/null 2>&1 && echo "$ip 存活" || echo "$ip 不通" ) & done wait echo "所有检查完成"这种并发写法会同时发起多个 ping,相比逐条执行能明显减少总耗时。但要注意,并发数量不要太多,否则会拖慢系统,生产环境建议限制并发数。
6.6 循环内批量检查服务端口
批量检查多台服务器端口是否开放,结合nc或/dev/tcp实现:
#!/bin/bash hosts=("192.168.1.10" "192.168.1.11") port=22 for host in "${hosts[@]}"; do timeout 3 bash -c "echo > /dev/tcp/$host/$port" 2>/dev/null if [ $? -eq 0 ]; then echo "$host:$port 端口正常" else echo "$host:$port 端口不可达" fi done7. 函数封装与脚本结构
7.1 函数定义与调用
当脚本里有多处重复逻辑时,应该封装成函数。函数需要在调用之前定义:
#!/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" } log_info "开始执行备份" log_error "备份失败"7.2 函数参数与返回值
函数内部的$1、$2是函数参数,不是脚本参数。返回值用return返回 0 到 255 的整数:
#!/bin/bash check_service() { local service_name=$1 systemctl is-active "$service_name" > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "active" return 0 else echo "inactive" return 1 fi } status=$(check_service nginx) echo "nginx 状态: $status"如果想返回一个字符串,直接用 echo 输出,用变量接收,比 return 更灵活。
7.3 局部变量 local
函数内部定义的变量默认是全局的,容易污染外部环境。使用local声明局部变量是更安全的做法:
#!/bin/bash my_func() { local temp="内部值" echo "函数内: $temp" } my_func echo "函数外访问 temp: $temp"上面代码最后一行会输出空字符串,因为temp只在函数内有效。
7.4 脚本通用结构
一个正式脚本应该有清晰的阶段划分。下面是一个带参数校验、日志、主函数调用的结构模板:
#!/bin/bash set -euo pipefail # 常量定义 SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) LOG_FILE="$SCRIPT_DIR/script.log" # 日志函数 log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" } # 参数校验 if [ $# -lt 1 ]; then log "参数不足,用法: $0 <target>" exit 1 fi # 核心处理函数 process() { local target=$1 log "开始处理 $target" # 实际业务逻辑 } # 主函数入口 main() { process "$1" log "全部完成" } main "$1"这种结构看起来很“标准”,但在真实运维里很实用。日志、参数校验、退出码控制都在最前面,后面改业务逻辑时不用调整外围结构。
8. 三剑客实战:grep、sed、awk
文本三剑客是 Linux 运维处理日志和配置文件的看家工具。单独掌握每个命令不难,难的是把它们串到脚本流程里。
8.1 grep:文本过滤
grep 用来在文件或标准输出中匹配文本行。最常用参数:
| 参数 | 含义 |
|---|---|
-E | 支持扩展正则表达式 |
-i | 忽略大小写 |
-v | 反向匹配 |
-c | 统计匹配行数 |
-n | 显示行号 |
-r | 递归搜索目录 |
示例:
# 在 nginx 访问日志中搜索 500 状态码 grep -n ' 500 ' /var/log/nginx/access.log # 统计错误日志行数 grep -c 'ERROR' /var/log/app/error.log # 排除注释行和空行,查看配置文件有效内容 grep -vE '^\s*#|^\s*$' /etc/nginx/nginx.conf在脚本里,grep 经常配合条件判断使用:
#!/bin/bash if grep -q 'listen 443' /etc/nginx/nginx.conf; then echo "已配置 HTTPS 监听" else echo "未配置 HTTPS 监听" fi-q参数表示静默模式,不输出内容,只影响退出码,适合在 if 条件中使用。
8.2 sed:流式编辑
sed 可以对文本进行增删改查,不需要用 vim 打开文件,非常适合脚本内自动修改配置。
替换字符串是最常用的操作:
# 把文件中的 old 替换为 new sed 's/old/new/' file.txt # 全局替换并直接写回文件 sed -i 's/old/new/g' file.txt # 在每行末尾追加内容 sed -i 's/$/ 追加内容/' file.txt # 删除匹配行 sed -i '/^#/d' file.txtsed 里最常见的坑是-i的兼容性。GNU sed 写sed -i没问题,macOS 自带 BSD sed 需要加扩展名,否则会提示undeclared identifier。
在脚本里批量修改配置文件非常实用。例如把所有配置项的旧 IP 换成新 IP:
#!/bin/bash config_file="/etc/myapp/config.ini" sed -i 's/192.168.1.100/10.0.0.100/g' "$config_file"执行前一定要先备份,或者先不带-i输出预览:
sed 's/192.168.1.100/10.0.0.100/g' "$config_file" | head -508.3 awk:文本分析与列处理
awk 是一个小型的文本处理语言,最擅长按列处理数据。默认按照空格或制表符切分每一行,$1表示第一列,$NF表示最后一列,NF表示列数,NR表示行号。
先看一个最简单的例子:
# 打印 /etc/passwd 中的用户名和默认 Shell awk -F: '{print $1, $NF}' /etc/passwd-F:指定冒号为分隔符,因为 passwd 文件是用冒号分隔的。
实际运维中,awk 最常用于分析访问日志。比如统计 nginx 日志中访问量前 10 的 IP:
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10这里不只是 awk,还串联了sort、uniq、head,这就是 Shell 自动化的典型写法:用管道把多个工具组合起来完成一个分析任务。
统计 HTTP 状态码分布:
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c计算请求耗时总和和平均值:
awk '{sum += $NF} END {print "请求数", NR, "总耗时", sum, "平均耗时", sum/NR}' /var/log/nginx/access.logawk 在脚本中还被用来提取进程 PID、磁盘使用率等指标:
#!/bin/bash disk_usage=$(df / | awk 'NR==2{print $5}' | sed 's/%//') echo "根分区使用率: $disk_usage%" if [ "$disk_usage" -gt 85 ]; then echo "磁盘使用率过高,需要清理" fi8.4 三剑客组合实战:日志分析
把三剑客组合起来,可以完成一条完整的日志分析链路。下面是统计今天的 ERROR 日志按模块分类数量的脚本:
#!/bin/bash today=$(date +%Y-%m-%d) log_file="/var/log/myapp/error_$today.log" if [ ! -f "$log_file" ]; then echo "日志文件不存在: $log_file" exit 1 fi # 提取 ERROR 行,去掉时间戳前缀,按模块分类统计 grep ' ERROR ' "$log_file" \ | sed 's/^\[.*\] \[\(.*\)\] .*/\1/' \ | sort \ | uniq -c \ | sort -rn这条命令依次完成:过滤错误日志、提取模块名、排序、统计数量、按数量倒序排列。
9. 自动化任务实战
9.1 清理由脚本自动归档后的旧文件
很多服务会产生大量临时文件,需要定期清理超过 7 天的文件:
#!/bin/bash # 清理 /var/log/myapp/ 下超过 7 天的 .log 文件 find /var/log/myapp/ -name "*.log" -mtime +7 -exec ls -lh {} \;find配合mtime可以找出指定时间之前修改过的文件。先把要删的文件列出来,确认无误后再加-delete:
find /var/log/myapp/ -name "*.log" -mtime +7 -delete不要直接在生产环境执行带-delete的命令。第一次运行先使用ls -lh检查列表,确认范围准确后再清理。
9.2 每周备份数据库
定期备份是一个典型需求。下面脚本将 MySQL 数据库导出为 SQL 文件,并压缩保存:
#!/bin/bash set -euo pipefail BACKUP_DIR="/data/backup/mysql" DB_NAME="myapp" DB_USER="backup_user" DB_PASS="${MYSQL_PASSWORD}" mkdir -p "$BACKUP_DIR" backup_file="$BACKUP_DIR/${DB_NAME}_$(date +%Y%m%d_%H%M%S).sql.gz" mysqldump -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" | gzip > "$backup_file" echo "备份完成: $backup_file"这里密码从环境变量读取,而不是硬编码在脚本里,更符合安全规范。
9.3 服务健康检查
结合 curl 检查 HTTP 服务是否正常,失败时重启并发送通知:
#!/bin/bash set -euo pipefail url="http://127.0.0.1:8080/health" service_name="myapp" http_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$url" || echo 000) if [ "$http_code" = "200" ]; then echo "[$(date '+%F %T')] $service_name 状态正常" else echo "[$(date '+%F %T')] $service_name 异常,HTTP 状态码: $http_code" systemctl restart "$service_name" echo "已尝试重启" fi9.4 定时任务 crontab
脚本写完以后,可以交给 cron 定期执行。先编辑当前用户的 crontab:
crontab -e添加以下几类配置:
# 每天凌晨 3 点执行备份脚本 0 3 * * * /home/user/shell-practice/backup.sh >> /home/user/shell-practice/backup.log 2>&1 # 每 5 分钟执行健康检查 */5 * * * * /home/user/shell-practice/health_check.sh >> /home/user/shell-practice/health.log 2>&1 # 每周日凌晨 1 点清理日志 0 1 * * 0 find /var/log/myapp/ -name "*.log" -mtime +30 -delete写 crontab 时注意:脚本里的命令要使用绝对路径,因为 cron 环境与交互式终端不同,PATH 可能不完整。脚本本身最好也加上日志输出和失败重试机制。
9.5 批量服务器巡检
生产环境有多台服务器时,可以写一个带主机列表的巡检脚本:
#!/bin/bash set -euo pipefail hosts_file="/etc/myapp/hosts.txt" log_file="/var/log/myapp/inventory.log" while read -r host; do if [ -z "$host" ]; then continue fi echo "[$(date '+%F %T')] 检查 $host" ping -c 1 -W 2 "$host" > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "$host 可达" else echo "$host 不可达" | tee -a "$log_file" fi done < "$hosts_file"如果要远程执行命令检查磁盘、内存,可以配合 SSH 密钥登录后逐台执行。但务必确认你拥有这些服务器的合法管理权限,避免未授权访问。
9.6 Shell 脚本调用 REST API
Shell 虽然不是写 HTTP 服务端的最佳语言,但作为客户端调用 API 非常方便。下面是一个调用 API 获取数据的示例:
#!/bin/bash set -euo pipefail api_url="https://api.example.com/v1/status" token="${API_TOKEN}" # 发起 GET 请求并保存响应 response=$(curl -s --max-time 10 -H "Authorization: Bearer $token" "$api_url") echo "API 响应: $response" # 检查响应中是否包含 success if echo "$response" | grep -q '"status":"success"'; then echo "任务执行成功" else echo "任务可能失败,需要人工确认" exit 1 fi发送 POST 请求批量提交数据:
curl -s -X POST "https://api.example.com/v1/tasks" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $token" \ -d '{"task_name": "backup", "target": "web-01"}'如果接口返回 JSON,并且需要提取字段,可以用jq工具。它专门处理 JSON 数据:
#!/bin/bash response=$(curl -s https://api.example.com/v1/status) task_id=$(echo "$response" | jq -r '.task_id') status=$(echo "$response" | jq -r '.status') echo "任务 ID: $task_id, 状态: $status"在未安装 jq 的服务器上,可以用yum install jq或apt install jq安装,非常轻量。
9.7 批量任务带日志与失败重试
批量任务不能只“无脑跑”,需要记录每个任务的执行状态和失败次数。下面是一个带简单重试机制的脚本:
#!/bin/bash set -euo pipefail task_file="/tmp/tasks.txt" log_file="/var/log/myapp/batch_task.log" max_retry=3 while read -r task; do [ -z "$task" ] && continue retry=0 while [ $retry -lt "$max_retry" ]; do retry=$((retry + 1)) echo "[$(date '+%F %T')] 开始执行任务: $task (第 $retry 次)" if bash -c "$task" >> "$log_file" 2>&1; then echo "[$(date '+%F %T')] 任务成功: $task" break else echo "[$(date '+%F %T')] 任务失败: $task" sleep 2 fi done done < "$task_file" echo "批量任务结束,详细日志见 $log_file"这个脚本读入任务列表,每条任务最多重试 3 次,并把执行日志追加到日志文件。生产环境如果要控制并发,可以在此基础上加入后台执行与wait。
10. 资源占用与性能观察
Shell 脚本本身几乎没有常驻内存消耗,真正影响性能的是脚本里调用的外部命令和循环方式。掌握以下几点,能明显提升脚本执行效率。
第一,减少子进程创建。Shell 中每调用一次外部命令(grep、sed、awk、curl 等)都会创建新进程。如果循环 1000 次,每次执行 3 个外部命令,就会创建 3000 个进程。常见的优化思路是先捕获命令输出,再在变量层面做处理,而不是在循环里反复启动命令。
第二,合理使用管道。管道虽然方便,但每条管道都会产生额外进程。如果只是简单文本处理,可以直接用 awk 完成过滤和统计,避免 “grep + awk + sed” 三个进程串行处理同一份数据。
第三,使用time测算脚本耗时:
time bash batch_task.sh输出会显示 real、user、sys 三项时间。real 是真实耗时,user 是用户态 CPU 时间,sys 是内核态 CPU 时间。
第四,使用top、ps观察脚本所在进程的资源占用:
ps -ef | grep batch_task.sh top -p $(pgrep -f batch_task.sh | head -1)如果脚本在循环中持续占用 CPU,可能是出现了死循环,或外部命令调用链条过长。可以用set -x开启调试模式,观察脚本执行过程,定位卡住的步骤。
第五,控制并发数量。前面提到的后台并发写法能加速任务,但如果一次性启动几十上百个后台进程,系统负载会快速上升。使用简单的并发控制:
#!/bin/bash max_concurrent=5 task_count=0 for ip in 192.168.1.{1..20}; do ( ping -c 1 -W 1 "$ip" > /dev/null 2>&1 && echo "$ip 存活" || echo "$ip 不通" ) & task_count=$((task_count + 1)) if [ $((task_count % max_concurrent)) -eq 0 ]; then wait fi done wait echo "并发巡检完成"这样做既利用了并行能力,又不会把系统资源打满。
11. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 执行脚本报 Permission denied | 文件没有执行权限 | ls -l script.sh | chmod +x script.sh |
| 提示 command not found | 环境变量 PATH 不完整 | echo $PATH | 使用绝对路径,或安装缺失命令 |
| 脚本运行时全部正常,但 cron 执行失败 | cron 环境没有加载用户环境变量 | 在脚本中打印日志,查看 cron 输出 | 脚本开头添加 source 配置文件,或使用绝对路径 |
/bin/bash^M错误或换行符导致怪问题 | 脚本是 Windows 编辑的,存在 CRLF 换行 | file script.sh | 使用sed -i 's/\r$//' script.sh转换 |
| 变量比较时报 integer expression expected | 变量不是数字或为空 | echo "变量值: $num"检查 | 先做变量判空和类型处理 |
| 条件判断一直不生效 | 方括号前后缺少空格 | 检查[和] | 改成[ "$var" = "value" ] |
| 脚本中 rm 或 mv 没有生效 | 变量包含空格,未加引号 | echo "$file"确认内容 | 变量引用统一加双引号 |
| grep 匹配不到内容但文本确实存在 | 正则表达式错误或文件编码问题 | cat file | head查看原文 | 调整正则,确认必要编码 |
| awk 输出为空 | 分隔符设置不正确 | 查看原始文本结构 | 根据实际分隔符调整-F |
忘了给脚本加执行权限,直接./script.sh运行 | 权限位不足 | chmod +x script.sh | 用bash script.sh临时运行 |
使用bash script.sh能运行,./script.sh无法运行 | 缺少 shebang 或没有权限 | head -1 script.sh | 添加#!/bin/bash并设置权限 |
Shell 脚本问题排查最重要的不是记住所有语法,而是学会“把执行过程打开看”。在脚本开头加set -x,执行时会把每条命令展开到终端,命令是否正确、变量是否展开都能直接看到。
还需要注意一点:脚本里如果出现rm -rf或> file这类破坏性命令,执行前一定要打印确认信息。可以写一段确认逻辑:
#!/bin/bash read -p "确认要删除 $target 目录吗?输入 yes 继续: " confirm if [ "$confirm" != "yes" ]; then echo "已取消" exit 1 fi这样至少能防止手误造成不可逆后果。
12. 最佳实践与使用建议
第一,脚本开头统一使用set -euo pipefail。这一行能避免大量因为命令失败、变量未定义、管道错误被忽略导致的问题。第一次写脚本的人可能觉得它碍事,但一旦跑过生产任务,就会明白这行保护的价值。
第二,变量引用统一加双引号。"$file"和$file的区别,在处理带空格的文件名时会直接决定脚本是否崩溃。这是新手最容易踩的坑。
第三,目录与文件路径分清楚。脚本的工作目录不一定是你执行命令的目录。在脚本开头声明:
BASE_DIR=$(cd "$(dirname "$0")" && pwd) cd "$BASE_DIR"这样无论从哪里调用脚本,文件路径都能保持稳定。
第四,日志要有时间戳和级别。运维脚本如果没有任何输出,出了问题会非常难查。至少每条关键操作要输出一行带时间戳的信息。
第五,批量任务必须加失败重试和日志。一次跑完就结束的脚本只适合教学,生产环境里网络抖动、资源不足、依赖服务没起来都会导致任务失败。重试机制和日志是底线。
第六,敏感信息不入库。脚本里的密码、Token、私钥路径不要明文写入代码仓库。使用环境变量、配置文件权限控制或专门的密钥管理工具。
第七,合规与授权。批量巡检、远程执行命令、调用第三方接口前,确认你拥有合法授权。不要用 Shell 脚本做未授权扫描、批量爆破、抓取他人数据等操作。测试环境先跑通,生产环境先小范围验证,再加到 cron。
第八,写脚本时就要考虑失败后的影响。不要写那种默认“一切正常”的脚本。每个关键步骤后都要判断退出码,失败时提供明确提示。
13. 从这套实战内容继续往后怎么走
看完这套内容,你已经具备独立编写运维脚本的基础能力。建议先验证的是:把前文的批量重命名、日志分析和服务健康检查脚本在你的 Linux 环境里跑一遍,确认每个环节都能正常输出。
最容易踩的坑集中在三处:一是条件判断两边的空格,二是 Windows 编辑导致的 CRLF 换行,三是脚本执行权限没有设置。这三个问题解决了,大部分脚本都能顺利跑起来。
如果还想继续深入,下一阶段可以按这个顺序扩展:
- 在现有脚本中引入
getopts,支持类似./deploy.sh --env prod --force的参数解析方式; - 用 Shell 脚本封装一套自己的工具函数库,把日志、时间格式化、HTTP 请求、文件备份等通用能力抽出来;
- 结合
jq处理 JSON 格式的 API 响应,实现更完整的接口自动化; - 把脚本接入 crontab,逐步建立自己的运维自动化任务集;
- 更进一步,可以关注 Ansible、Python 脚本等更高级的自动化工具,但 Shell 作为底层能力始终不会过时。
Shell 脚本的门槛不在于语法有多难,而在于你能否把零散的 Linux 命令组织成一条清晰、可靠、可维护的执行链。把变量、条件、循环、函数和文本处理这一套流程练熟了,后续再去接触更复杂的自动化工具,底子就会扎实很多。
