Shell脚本从入门到生产环境:自动化运维的核心技能

📅 2026/8/24 5:40:04
Shell脚本从入门到生产环境:自动化运维的核心技能
你有没有过这样的经历每天上班第一件事就是打开终端重复执行那几条固定的命令检查服务状态、拉取日志、备份数据、清理临时文件。一开始还能记住时间一长不仅容易漏掉步骤还总担心自己敲错命令。更麻烦的是当需要把一套操作交给同事或者部署到新服务器时光靠口头交代或者写个文档出错率极高。这就是 Shell 脚本存在的意义。它远不止是“把命令写进一个文件”那么简单。很多人学 Shell上来就背for循环、if判断的语法结果真到写脚本时要么被变量替换坑得晕头转向要么脚本在别人机器上跑不起来要么一个rm -rf写错路径追悔莫及。Shell 脚本真正的价值在于将一次性的、依赖个人记忆和手速的临时操作固化成可重复、可交接、可审计的自动化流程。它解决的不是“会不会敲命令”而是“如何让复杂、重复的工作变得可靠且无需操心”。今天我们就从零开始但不止于语法一起看看如何把 Shell 从“知道”变成“能用”再到“敢用在生产环境”。1. 先别急着写循环理解 Shell 脚本的“生存环境”很多人学 Shell 脚本的第一个误区就是打开编辑器就开始写#!/bin/bash然后埋头研究语法。这就像学开车只背方向盘怎么转却不看路况和交通规则。Shell 脚本的“生存环境”——也就是它运行时的上下文——决定了脚本的健壮性。1.1 Shell 不是编程语言它是“胶水”和“指挥官”首先得摆正心态。Shell特别是 Bash的设计初衷不是为了进行复杂的数值计算或构建大型应用那是 Python、Go 的领域。它的核心能力是调用和组合系统命令ls,grep,awk,find,curl等才是干活的主力。管理进程和文件启动、停止程序重定向输入输出操作文件路径。提供简单的流程控制用if、for、while来决定命令的执行顺序和条件。所以一个高效的 Shell 脚本通常包含 20% 的 Shell 语法和 80% 的对各种系统命令的理解与巧妙组合。你的学习重点应该放在如何用 Shell 语法把这些命令“粘合”起来并处理好它们执行过程中的各种情况。1.2 脚本的第一行#!/bin/bash到底在说什么这行被称为shebang。它的作用是指定脚本的解释器。但这里有几个关键细节/bin/bash还是/bin/shsh通常是bash的符号链接但为了兼容性bash以sh模式运行时会关闭一些扩展功能。最稳妥的做法是明确使用#!/bin/bash除非你明确需要兼容非常古老的系统。路径问题如果你的 Bash 安装在/usr/local/bin/bash就应该写完整的路径。用which bash命令可以查看。如果没有 shebang系统会默认用当前用户的登录 Shell 来执行可能是bash也可能是zsh、csh导致脚本行为不一致。注意写完shebang后按惯例会空一行再开始写脚本正文这是一个良好的可读性习惯。1.3 执行权限与执行方式为什么你的脚本“跑不起来”这是新手最常遇到的坑。你写好了script.sh直接输入script.sh系统告诉你command not found。# 错误方式 $ script.sh # 正确方式1使用解释器直接执行不需要执行权限 $ bash script.sh # 正确方式2赋予执行权限后通过路径执行 $ chmod x script.sh $ ./script.sh # 注意前面的 ./ # 正确方式3将脚本所在目录加入 PATH 环境变量后直接执行 $ export PATH$PATH:/your/script/dir $ script.sh核心区别bash script.sh把脚本文件作为参数传给bash命令。bash进程负责读取并解释文件内容。./script.sh系统看到文件有执行权限x会读取第一行的shebang来确定用哪个解释器然后启动该解释器来执行脚本。如果你不写./系统只会在PATH环境变量列出的目录里寻找名为script.sh的命令显然找不到。所以记住这个固定搭配chmod x之后一定要用./开头来执行当前目录下的脚本。2. 变量从“存储值”到“安全地替换”变量是脚本的基石但也是最容易埋雷的地方。Shell 变量处理看似简单实则有很多“坑”。2.1 定义、使用与引号陷阱nameWorld # 定义变量等号两边不能有空格 echo Hello $name # 使用变量输出Hello World echo Hello $name # 双引号内变量会被替换输出Hello World echo Hello $name # 单引号内所有字符原样输出输出Hello $name第一个大坑变量赋值等号两边不能有空格。name World会被 Shell 解析为尝试执行一个叫name的命令并带参数和World这显然会失败。第二个大坑未定义的变量和空变量。默认情况下引用一个未定义的变量其值为空字符串不会报错。这有时会导致隐蔽的错误。rm -rf $DIRECTORY/* # 如果 DIRECTORY 变量为空这条命令会变成 rm -rf /*灾难如何避免使用${}形式引用变量并养成好习惯# 使用花括号明确变量边界 echo Hello ${name}! # 设置默认值如果变量未定义或为空则使用默认值 echo Directory is: ${DIRECTORY:-/tmp/backup} # 在删除等重要操作前检查变量是否为空 if [ -z ${DIRECTORY} ]; then echo 错误DIRECTORY 变量为空 2 exit 1 fi rm -rf ${DIRECTORY}/* # 即使变量被检查过路径也建议用双引号括起来部分2.2 命令替换把命令的输出存入变量这是 Shell 脚本自动化的关键能力之一。# 使用反引号 已过时不推荐 current_datedate # 使用 $()推荐更清晰且支持嵌套 current_date$(date) file_count$(ls -l | wc -l) kernel_version$(uname -r)$()会先执行括号内的命令然后用该命令的标准输出stdout替换整个表达式。注意命令的错误输出stderr不会被捕获。2.3 环境变量、局部变量与脚本参数环境变量对当前 Shell 及其所有子进程都可见。使用export VARvalue设置。像PATH,HOME,USER这些都是系统预定义的环境变量。局部变量只在当前 Shell 脚本进程中有效。默认定义的变量就是局部变量。脚本参数执行脚本时传入的参数。$0脚本名称本身。$1,$2,$3...第1、2、3...个参数。$#传入参数的个数。$所有参数列表每个参数都是独立的带引号字符串。for arg in $$*所有参数列表将所有参数视为一个整体字符串。不常用容易出错$?上一个命令的退出状态码。0 表示成功非0 表示失败。处理参数时一个常见的需求是检查参数是否足够if [ $# -lt 2 ]; then echo 用法$0 源目录 目标目录 exit 1 fi source_dir$1 target_dir$23. 流程控制让脚本学会“判断”和“重复”掌握了变量脚本还只是静态的。流程控制赋予了脚本逻辑。3.1 条件判断if、test与[ ]Shell 中条件判断的核心命令是test它通常以[ ]的形式出现注意括号内左右必须有空格。# 判断文件是否存在 if [ -f /path/to/file.txt ]; then echo 文件存在。 fi # 判断字符串是否相等 if [ $USER root ]; then echo 你好管理员。 else echo 你好普通用户 $USER。 fi # 判断数字大小 count10 if [ $count -gt 5 ]; then echo 计数大于5。 fi常用判断符号文件测试-e file文件/目录是否存在。-f file是否为普通文件。-d file是否为目录。-r file是否可读。-w file是否可写。-x file是否可执行。字符串比较,!相等不相等。-z string字符串长度是否为0空。-n string字符串长度是否非0。整数比较-eq,-ne等于不等于。-gt,-lt大于小于。-ge,-le大于等于小于等于。高级判断[[ ]]Bash 扩展的测试命令比[ ]更强大更安全支持正则匹配。# 使用 [[ ]] 可以安全地处理变量空值和字符串比较 filenamesome file.txt if [[ -f $filename ]]; then # 即使变量包含空格[[ ]] 也能正确处理 echo 找到文件。 fi # 正则匹配 if [[ $hostname ~ ^web[0-9] ]]; then echo 这是一台Web服务器。 fi3.2 循环for、while与untilfor循环常用于遍历列表。# 遍历静态列表 for fruit in apple banana orange; do echo 水果$fruit done # 遍历命令输出结果按空格、换行等分隔 for file in $(ls *.log); do echo 处理文件$file # 注意如果文件名包含空格这里会出问题更好的方法是用 find 或 while read done # 类C风格的for循环 for ((i0; i10; i)); do echo 数字$i donewhile循环当条件为真时持续循环。# 读取文件每一行安全处理空格 while IFS read -r line; do echo 行内容$line done /path/to/file.txt # 无限循环直到满足条件用 break 退出 count0 while true; do echo 等待中... $count ((count)) if [ $count -eq 5 ]; then break fi sleep 1 doneuntil循环与while相反条件为假时循环。until systemctl is-active --quiet nginx; do echo Nginx 未启动等待... sleep 2 done echo Nginx 已启动4. 函数封装重复逻辑让脚本模块化当你的脚本超过 50 行或者同一段代码出现两次以上就该考虑使用函数了。4.1 定义与调用# 函数定义 function say_hello() { local name$1 # 使用 local 声明局部变量避免污染全局 echo Hello, $name! } # 或者省略 function 关键字 say_goodbye() { echo Goodbye, $1! } # 函数调用 say_hello Alice say_goodbye Bob # 获取函数返回值通过 $? 获取退出状态码 check_service() { if systemctl is-active --quiet $1; then return 0 # 成功 else return 1 # 失败 fi } check_service nginx if [ $? -eq 0 ]; then echo Nginx 正在运行。 fi关键点local关键字在函数内定义局部变量这是写出安全、可重用函数的关键。没有local变量在函数外也可见容易引发冲突。返回值Shell 函数通过return返回一个0-255 之间的整数状态码类似于命令的退出码。它不用于返回常规数据。要返回数据请使用echo输出然后在调用处用命令替换$( )捕获。get_timestamp() { date %Y%m%d_%H%M%S } current_time$(get_timestamp) echo 当前时间$current_time4.2 函数参数函数内部通过$1,$2,$,$#来访问传入的参数这些变量与脚本主体部分的参数是独立的。log_message() { local level$1 local message$2 echo [$(date %Y-%m-%d %H:%M:%S)] [$level] $message } log_message INFO 脚本开始执行。 log_message ERROR 配置文件未找到5. 文本处理三剑客grep、sed、awkShell 脚本的强大一半体现在对文本的流式处理上。grep、sed、awk是三位核心功臣它们各自擅长不同的领域。5.1grep全局搜索正则表达式并打印用于在文本中查找匹配模式的行。# 基本查找 grep error /var/log/syslog # 查找包含 error 的行 grep -i error /var/log/syslog # -i 忽略大小写 grep -v info /var/log/syslog # -v 反向选择不包含 info 的行 grep -n error /var/log/syslog # -n 显示行号 grep -c error /var/log/syslog # -c 统计匹配行数 # 正则表达式 grep ^2024 app.log # 查找以 2024 开头的行日志时间 grep finished$ app.log # 查找以 finished 结尾的行 grep -E error|warning|critical app.log # -E 扩展正则匹配多个模式之一5.2sed流编辑器用于对文本进行替换、删除、插入等编辑操作。# 替换最常用 sed s/old/new/g file.txt # 将文件中所有 old 替换为 new sed s/old/new/ file.txt # 只替换每行第一个 old sed -i s/old/new/g file.txt # -i 直接修改原文件危险先备份 # 删除行 sed /^#/d config.conf # 删除所有以 # 开头的注释行 sed 2,5d file.txt # 删除第2到第5行 # 打印特定行 sed -n 10,20p file.txt # -n 抑制默认输出只打印第10到20行5.3awk文本和数据处理语言功能最强大擅长基于列字段进行处理。它本身就是一门小型编程语言。# 基本打印 awk {print $1} data.txt # 打印每行第一列默认以空格/Tab分隔 awk -F, {print $2} data.csv # -F 指定分隔符为逗号打印第二列 # 条件过滤 awk $3 100 {print $1, $3} data.txt # 第三列大于100的行打印第一、三列 awk /error/ {print $0} app.log # 匹配包含 error 的行打印整行 # 内置变量与计算 awk {sum $1} END {print sum} numbers.txt # 计算第一列总和 awk {print NR, $0} file.txt # NR 当前行号为每行添加行号 awk {print NF, $0} file.txt # NF 当前行的字段数一个综合例子分析 Nginx 访问日志统计每个 IP 的访问次数awk {print $1} /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -10awk {print $1}提取日志第一列默认是 IP 地址。sort对 IP 进行排序为uniq做准备。uniq -c统计并去重-c显示出现次数。sort -nr按数字 (-n) 逆序 (-r) 排序。head -10取前10行。6. 从脚本到工程安全、健壮与可维护性能写出跑得通的脚本只是第一步。要让脚本能在生产环境可靠运行并能交给其他人维护你需要关注以下方面。6.1 安全第一避免毁灭性错误检查变量是否为空尤其是在rm、mv、cp等命令前。# 危险 rm -rf ${TEMP_DIR}/* # 安全做法 if [[ -z ${TEMP_DIR} ]]; then echo 错误TEMP_DIR 未设置。 exit 1 fi # 或者使用默认值并创建目录 TEMP_DIR${TEMP_DIR:-/tmp/myapp} mkdir -p ${TEMP_DIR} rm -rf ${TEMP_DIR}/*使用set -euo pipefail在脚本开头加上这行“安全咒语”。set -e脚本中任何命令失败返回非零状态就立即退出。set -u遇到未定义的变量时报错并退出。set -o pipefail管道命令中任何一个失败整个管道就视为失败。#!/bin/bash set -euo pipefail # 你的脚本正文...谨慎使用通配符rm *.log在目录为空时可能会变成rm加上-f更危险。可以先echo预览要删除的文件。6.2 增加可读性与可维护性添加注释解释复杂的逻辑、重要的参数、函数的功能。使用有意义的变量名src_dir比sd好max_retries比mr好。统一代码风格缩进建议2或4个空格、函数定义格式保持一致。模块化将独立功能封装成函数主流程清晰。日志输出使用函数统一记录日志包括时间戳、日志级别、消息。log() { local level$1 shift echo [$(date %Y-%m-%d %H:%M:%S)] [$level] $* } log INFO 开始备份数据库。 # ... 备份操作 ... if [ $? -eq 0 ]; then log SUCCESS 备份完成。 else log ERROR 备份失败 exit 1 fi6.3 处理错误与异常检查命令返回值对于关键命令检查$?。important_command if [ $? -ne 0 ]; then log ERROR important_command 执行失败。 # 执行清理或重试逻辑 exit 1 fi # 或者更简洁的写法 if ! important_command; then log ERROR important_command 执行失败。 exit 1 fi使用trap捕获信号在脚本被中断时如用户按 CtrlC执行清理工作。cleanup() { log INFO 正在清理临时文件... rm -rf $TEMP_DIR } trap cleanup EXIT INT TERM # 在脚本退出、被中断、被终止时调用 cleanup 函数 TEMP_DIR$(mktemp -d) # ... 脚本主要工作 ...6.4 一个综合实战示例备份脚本结合以上所有要点我们来看一个相对完整的、用于生产环境思维的备份脚本框架#!/bin/bash set -euo pipefail # 配置部分 readonly BACKUP_SOURCE/data/app # 源目录 readonly BACKUP_TARGET/backups # 备份目标目录 readonly BACKUP_PREFIXapp_backup # 备份文件前缀 readonly KEEP_DAYS30 # 保留最近30天的备份 # # 日志函数 log() { local level$1 shift echo [$(date %Y-%m-%d %H:%M:%S)] [$level] $* | tee -a ${LOG_FILE:-/dev/null} } # 错误处理与退出 error_exit() { log ERROR $1 exit 1 } # 检查依赖 check_dependencies() { command -v rsync /dev/null 21 || error_exit rsync 命令未找到请先安装。 command -v gzip /dev/null 21 || error_exit gzip 命令未找到。 } # 检查目录 check_directories() { [[ -d $BACKUP_SOURCE ]] || error_exit 源目录不存在$BACKUP_SOURCE mkdir -p $BACKUP_TARGET || error_exit 无法创建目标目录$BACKUP_TARGET } # 执行备份 perform_backup() { local timestamp$(date %Y%m%d_%H%M%S) local backup_name${BACKUP_PREFIX}_${timestamp}.tar.gz local backup_path$BACKUP_TARGET/$backup_name log INFO 开始备份$BACKUP_SOURCE - $backup_path tar -czf $backup_path -C $(dirname $BACKUP_SOURCE) $(basename $BACKUP_SOURCE) if [[ $? -eq 0 ]]; then log SUCCESS 备份创建成功$backup_path echo $backup_path else error_exit 备份创建失败。 fi } # 清理旧备份 cleanup_old_backups() { log INFO 清理 $KEEP_DAYS 天前的旧备份... find $BACKUP_TARGET -name ${BACKUP_PREFIX}_*.tar.gz -type f -mtime $KEEP_DAYS -delete log INFO 旧备份清理完成。 } # 主函数 main() { log INFO 备份脚本开始 check_dependencies check_directories perform_backup cleanup_old_backups log INFO 备份脚本结束 } # 脚本入口 if [[ ${BASH_SOURCE[0]} ${0} ]]; then main fi这个脚本体现了工程化脚本的多个要点安全设置set -euo pipefail。配置集中变量在开头定义易于修改。模块化每个功能封装成函数。错误处理有专门的错误退出函数。依赖检查运行前检查必要命令。日志记录有格式化的日志输出。资源清理自动删除过期备份。可重用入口最后几行确保脚本可以直接执行也能被source而不立即运行。学习 Shell 脚本语法只是地图真正的旅程是在解决实际问题的过程中不断踩坑、调试、优化和重构。从今天起试着把你每天重复三次以上的命令行操作写成一个哪怕只有 5 行的小脚本。你会发现自动化带来的不仅是效率更是一种对工作流的掌控感和可重复的确定性。