Shell脚本入门到实战:从基础语法到自动化运维脚本编写
1. 从“黑窗口”到自动化利器为什么你需要Shell脚本如果你刚接触Linux面对那个黑色的终端窗口敲几个ls、cd命令可能觉得还挺酷。但当你需要重复执行一系列命令比如每天备份文件、批量重命名几百张照片、或者监控服务器日志时一遍遍手动输入就变得无比枯燥且容易出错。这时Shell脚本的价值就凸显出来了。它本质上是一个文本文件里面存放着一系列你原本需要手动输入的Shell命令然后让系统自动、按顺序地执行它们。你可以把它理解为给Linux系统写的“待办事项清单”或者“自动化工作流”。我刚开始做运维的时候最烦的就是每周一的例行巡检登录十几台服务器逐个检查磁盘空间、查看关键进程状态、备份特定日志。每次都要花上一两个小时还生怕漏了哪台。直到我花了半天时间把所有这些检查命令写进一个脚本里。现在我只需要运行这一个脚本它就能自动登录所有服务器当然配置了密钥对免密执行检查并把结果汇总成一个报告文件发到我邮箱。这不仅仅是节省时间更是把重复性脑力劳动转化为一次性的创造性工作并且极大地提升了准确性和可靠性。Shell脚本的用武之地远不止运维。数据分析师可以用它来定时抓取数据并清洗开发者可以用它来编译项目、运行测试、一键部署甚至普通用户也能用它来整理下载文件夹里的文件。它的门槛并不高但掌握后带来的效率提升是指数级的。这篇教程的目标就是帮你跨过“知道几个命令”到“能写出实用脚本”这道坎让你能真正驾驭这个强大的自动化工具。2. 脚本基石环境、结构与执行在动手写第一个脚本之前我们需要把基础打牢。这包括了解你用的是哪种Shell掌握脚本文件的基本结构以及知道如何让一个文本文件变成可执行程序。2.1 认识你的Shell环境打开终端你其实已经进入了一个Shell环境。输入echo $SHELL命令最常见的输出是/bin/bash。Bash (Bourne Again SHell) 是目前绝大多数Linux发行版的默认Shell也是本教程主要围绕的对象。它的前身是经典的Bourne Shell (/bin/sh)Bash兼容并增强了许多特性。虽然还有Zsh、Fish等更现代的Shell但Bash的普适性和在脚本中的稳定性是无可替代的。对于脚本编写坚持使用Bash特性可以确保你的脚本在绝大多数环境下都能正常运行。注意有些极简的Linux环境或Docker镜像可能默认使用ash或dash它们是轻量级的Shell对Bash的某些扩展语法支持不完全。如果你的脚本需要在非常广泛的环境下运行可以在脚本头部使用#!/bin/sh并严格使用POSIX标准语法。但对于入门和大多数个人使用场景直接使用Bash更简单强大。2.2 你的第一个脚本从“Hello World”开始让我们遵循经典传统创建一个输出“Hello World”的脚本。这能让你立刻看到整个流程。首先打开终端用文本编辑器创建一个新文件。我强烈建议从学习阶段就使用命令行下的编辑器如nano或vim这能让你更熟悉环境。这里我们用nanonano hello.sh在打开的编辑器里输入以下内容#!/bin/bash # 这是我的第一个Shell脚本 # 作者你的名字 echo Hello, World! echo 当前时间是$(date)我们来逐行拆解第一行#!/bin/bash 这被称为“Shebang”或“Hashbang”。它是脚本魔法开始的地方。#!告诉系统这个文件需要用一个解释器来执行而/bin/bash指定了使用Bash解释器。没有这一行系统就不知道该如何处理这个文件里的文本。第二、三行以#开头 这是注释。Shell会忽略#之后到行尾的所有内容。写注释是一个极其重要的好习惯尤其是几天、几周后你再回看自己的脚本或者别人需要阅读你的脚本时。注释能解释代码的意图而非重复代码的行为。第四行echo Hello, World!echo是一个内置命令功能就是将其后的参数这里是字符串“Hello, World!”输出到标准输出通常是你的终端屏幕。第五行echo 当前时间是$(date) 这里演示了“命令替换”。$(date)会先执行date命令该命令会输出当前日期和时间然后用这个命令的输出结果替换掉$(date)这个部分。所以这行最终会输出类似“当前时间是2023年10月27日 星期五 14:30:00 CST”的内容。输入完成后在nano中按Ctrl X然后按Y确认保存再按Enter确认文件名即可退出。2.3 赋予生命让脚本可执行创建了hello.sh文件后它目前只是一个普通的文本文件。尝试直接输入hello.sh运行系统会告诉你“命令未找到”。我们需要两步来让它变成可执行的“程序”。第一步添加可执行权限。Linux中文件是否有执行的权力是由“权限位”控制的。使用ls -l hello.sh查看你会看到类似-rw-r--r--的输出这意味着文件所有者可以读r、写w但不可以执行x。我们需要给它加上执行权限。chmod x hello.shchmod是修改权限的命令x表示给所有用户所有者、组用户、其他用户增加执行权限。再次ls -l你会看到权限变成了-rwxr-xr-x那个x就出现了。第二步执行它。现在有几种方式可以运行你的脚本使用路径执行./hello.sh。这里的./代表当前目录。因为当前目录通常不在系统的PATH环境变量里所以必须明确指定路径。使用解释器直接执行bash hello.sh。这种方式下即使文件没有执行权限甚至没有Shebang行也能运行因为显式指定了bash作为解释器。常用于调试。可选移动到PATH目录或添加PATH如果你想把脚本像系统命令一样随处调用可以将它移动到/usr/local/bin/这样的目录或者将你的脚本所在目录添加到PATH环境变量中。但入门阶段不建议以免造成管理混乱。现在输入./hello.sh你应该就能看到“Hello, World!”和当前时间输出了。恭喜你的第一个Shell脚本成功运行了实操心得养成“创建文件 - 编写内容 -chmod x-./script.sh测试”的肌肉记忆。另外在脚本开发初期可以多用bash -x script.sh来调试。-x参数会让Bash打印出它执行的每一行命令及其扩展后的参数对于理解脚本执行流程和排查错误非常有用。3. Shell脚本编程核心要素详解脚本能运行了但要让它能“思考”和“决策”我们需要学习构成脚本的基本编程元素变量、操作符和控制结构。3.1 变量数据的容器变量用来存储信息。Shell变量命名通常使用大写字母加下划线但这只是约定并非强制。定义与赋值NAMELinux User # 定义变量NAME并赋值 COUNT10 # 赋值数字注意等号两边不能有空格使用变量使用变量时需要在变量名前加美元符号$。echo $NAME # 输出Linux User echo ${NAME} # 输出Linux User花括号用于明确变量边界是好习惯 echo Hello, $NAME # 输出Hello, Linux User双引号内变量会被替换 echo Hello, $NAME # 输出Hello, $NAME单引号内所有字符原样输出环境变量与局部变量局部变量仅在当前Shell脚本中有效。如上例中的NAME。环境变量可以传递给子进程包括子Shell和其他程序。使用export命令定义。export DATABASE_URLmysql://user:passlocalhost/db像PATH,HOME,USER这些都是系统预定义的环境变量可以直接使用echo $PATH查看。特殊变量Shell提供了一些非常有用的特殊变量在脚本中经常用到$0当前脚本的文件名。$1,$2,$3...脚本执行时传入的第一个、第二个、第三个……参数。$#传递给脚本的参数个数。$所有参数列表每个参数作为独立的字符串。$*所有参数列表所有参数作为一个整体字符串。$?上一个命令的退出状态码。0通常表示成功非0表示失败。$$当前Shell进程的进程IDPID。3.2 字符串与数值操作字符串操作STRHello World echo ${#STR} # 输出 11获取字符串长度 echo ${STR:6} # 输出 “World”从索引6开始截取 echo ${STR:0:5} # 输出 “Hello”从索引0开始截取5个字符 echo ${STR/World/Linux} # 输出 “Hello Linux”替换第一个匹配项 echo ${STR//l/L} # 输出 “HeLLo WorLd”替换所有匹配项注意Shell大小写敏感数值计算Shell本身不擅长数学运算通常需要借助expr、let或$(( ))语法。A10 B3 # 使用 $(( )) SUM$((A B)) # 加法 DIV$((A / B)) # 除法结果为3整数除法 MOD$((A % B)) # 取模结果为1 echo $SUM $DIV $MOD # 使用 let let MUL A * B echo $MUL # 使用 bc 进行浮点运算需要安装bc计算器 echo scale2; $A / $B | bc # 输出 3.33scale设置小数位3.3 控制流程让脚本做出选择条件判断 (if/else)if语句的核心是判断一个“条件”的真假。条件通常由test命令或其等效的[ ]/[[ ]]符号来评估。#!/bin/bash read -p 请输入一个数字: NUM # 使用 [ ] 进行判断注意括号内两侧必须有空格 if [ $NUM -gt 100 ]; then echo 数字大于100 elif [ $NUM -eq 100 ]; then echo 数字等于100 else echo 数字小于100 fi常用比较运算符数值比较-eq(等于),-ne(不等于),-gt(大于),-lt(小于),-ge(大于等于),-le(小于等于)。字符串比较(等于),!(不等于),-z(字符串长度为0),-n(字符串长度非0)。注意在[ ]中字符串比较要用和!且变量最好用双引号括起如[ $NAME root ]以防变量为空导致语法错误。文件测试-e(文件存在),-f(是普通文件),-d(是目录),-r(可读),-w(可写),-x(可执行)。更现代且功能更强大的是[[ ]]它支持通配符匹配和更自然的逻辑运算符。if [[ $NUM 100 ]]; then # 在 [[ ]] 中可以直接使用 echo 大于100 fi if [[ $FILE *.txt ]]; then # 通配符匹配 echo 这是一个文本文件 fi if [[ -n $VAR $VAR ! ERROR ]]; then # 逻辑与 echo 变量非空且不是ERROR fi循环 (for/while/until)for循环遍历一个列表。# 遍历数字序列 for i in {1..5}; do echo 循环第 $i 次 done # 遍历命令输出结果例如遍历当前目录下所有.txt文件 for file in *.txt; do echo 处理文件: $file # 可以在这里对每个文件进行操作比如 wc -l $file done # 遍历参数列表 for arg in $; do echo 参数: $arg donewhile循环当条件为真时持续循环。COUNT1 while [ $COUNT -le 5 ]; do echo Count is $COUNT let COUNTCOUNT1 # 或者 ((COUNT))双括号内支持C语言风格的自增 done # 逐行读取文件内容 while IFS read -r line; do echo 行内容: $line done /path/to/file.txtIFS是为了防止行首行尾的空白字符被修剪-r防止反斜杠转义。until循环与while相反当条件为假时持续循环直到条件为真。COUNT1 until [ $COUNT -gt 5 ]; do echo 直到 $COUNT 大于5 ((COUNT)) done3.4 函数封装可重用代码当一段代码需要重复使用时就应该把它写成函数。#!/bin/bash # 函数定义 greet_user() { local name$1 # local 关键字声明局部变量避免污染全局 local hour$(date %H) if [ $hour -lt 12 ]; then echo 早上好$name elif [ $hour -lt 18 ]; then echo 下午好$name else echo 晚上好$name fi } # 函数调用 greet_user 张三 greet_user 李四 # 带返回值的函数通过 echo 输出调用者用 $( ) 捕获 get_file_count() { local dir$1 echo $(ls $dir | wc -l) } FILE_COUNT$(get_file_count /tmp) echo /tmp 目录下有 $FILE_COUNT 个文件函数让脚本结构更清晰也便于维护和调试。4. 实战演练构建实用脚本理解了基本语法后我们通过几个由浅入深的实战例子来融会贯通。这些脚本都是日常工作中提炼出来的稍加修改就能为你所用。4.1 实战一系统健康检查脚本这是一个简化版的服务器巡检脚本可以检查磁盘、内存和负载。#!/bin/bash # sys_check.sh - 基础系统健康检查 LOG_FILE/var/log/sys_check_$(date %Y%m%d).log # 定义日志函数 log_message() { echo [$(date %Y-%m-%d %H:%M:%S)] $1 | tee -a $LOG_FILE } log_message 开始系统健康检查 # 1. 检查磁盘使用率超过80%报警 DISK_USAGE$(df -h / | awk NR2 {print $5} | tr -d %) log_message 根分区磁盘使用率: ${DISK_USAGE}% if [ $DISK_USAGE -gt 80 ]; then log_message 警告磁盘使用率超过80% fi # 2. 检查内存使用率 MEM_INFO$(free -m | awk NR2{printf 已用: %dMB (%.1f%%), 可用: %dMB\n, $3, $3*100/$2, $4}) log_message 内存状态: $MEM_INFO # 3. 检查系统负载1分钟平均值 LOAD_1$(uptime | awk -Fload average: {print $2} | cut -d, -f1 | tr -d ) log_message 系统1分钟平均负载: $LOAD_1 # 获取CPU核心数用于判断负载是否过高 CPU_CORES$(nproc) # 负载超过核心数70%视为较高 LOAD_THRESHOLD$(echo $CPU_CORES * 0.7 | bc | awk {printf %.1f, $0}) if [ $(echo $LOAD_1 $LOAD_THRESHOLD | bc) -eq 1 ]; then log_message 注意系统负载较高。 fi # 4. 检查关键服务是否运行以sshd和nginx为例 SERVICES(sshd nginx) for service in ${SERVICES[]}; do if systemctl is-active --quiet $service; then log_message 服务 $service: 运行中 else log_message 警告服务 $service 未运行 fi done log_message 系统检查完成 echo 详细日志已保存至: $LOG_FILE脚本解析与技巧tee -a既在屏幕输出又追加到日志文件。awk和cut强大的文本处理工具用于从命令输出中精确提取所需字段。$(date %Y%m%d)在文件名中嵌入日期避免日志被覆盖。systemctl is-active --quiet安静地检查服务状态只返回退出码不输出信息。数组SERVICES的使用便于管理需要检查的服务列表。4.2 实战二日志文件分析与监控脚本假设你有一个不断增长的应用程序日志app.log需要监控其中“ERROR”出现的次数并在达到阈值时报警。#!/bin/bash # log_monitor.sh - 监控日志错误 LOG_FILE/var/log/app.log ALERT_THRESHOLD10 # 错误次数阈值 ALERT_EMAILadminexample.com # 报警邮箱 CHECK_INTERVAL60 # 检查间隔秒 # 记录上次检查到的行号 LAST_LINE_FILE/tmp/.log_monitor_last_line if [ -f $LAST_LINE_FILE ]; then LAST_LINE$(cat $LAST_LINE_FILE) else LAST_LINE0 fi while true; do # 获取当前日志总行数 CURRENT_LINES$(wc -l $LOG_FILE) # 如果日志文件被轮转或清空重置LAST_LINE if [ $CURRENT_LINES -lt $LAST_LINE ]; then LAST_LINE0 fi # 检查新增行中ERROR的数量 if [ $CURRENT_LINES -gt $LAST_LINE ]; then ERROR_COUNT$(sed -n ${LAST_LINE},${CURRENT_LINES}p $LOG_FILE | grep -c ERROR) if [ $ERROR_COUNT -gt 0 ]; then echo [$(date)] 发现 $ERROR_COUNT 个新ERROR。 # 这里可以添加更详细的错误信息提取 ERROR_SAMPLES$(sed -n ${LAST_LINE},${CURRENT_LINE}p $LOG_FILE | grep ERROR | head -5) if [ $ERROR_COUNT -ge $ALERT_THRESHOLD ]; then SUBJECT紧急应用日志错误激增警报 BODY在过去 ${CHECK_INTERVAL} 秒内在 $LOG_FILE 中检测到 $ERROR_COUNT 个 ERROR 日志。\n\n样例\n$ERROR_SAMPLES # 使用 mail 命令发送邮件需要配置系统邮件 # echo -e $BODY | mail -s $SUBJECT $ALERT_EMAIL echo -e $BODY # 暂时先输出到控制台 echo 已触发邮件警报。 fi fi # 更新最后检查的行号 echo $CURRENT_LINES $LAST_LINE_FILE fi sleep $CHECK_INTERVAL done脚本解析与技巧wc -l统计行数。sed -n ‘a,bp’打印文件的第a行到第b行。grep -c统计匹配行的数量。head -5取前5行。使用文件/tmp/.log_monitor_last_line来持久化记录上次检查的位置防止脚本重启后重复检查旧日志。sleep命令实现定时循环检查。邮件发送部分被注释掉了实际使用时需要确保系统已安装并配置好mail或sendmail等工具。4.3 实战三批量图片处理与重命名脚本这个脚本遍历指定目录下的所有图片文件根据扩展名将它们统一转换为指定格式如JPEG并按日期序列重命名。#!/bin/bash # batch_image_convert.sh - 批量图片处理 SOURCE_DIR$1 # 第一个参数源目录 TARGET_DIR${2:-./processed_images} # 第二个参数目标目录默认为./processed_images TARGET_FORMATjpg # 目标格式 SIZE800x600 # 目标尺寸保持宽高比只缩小不放大 # 检查依赖工具 ImageMagick 是否安装 if ! command -v convert /dev/null; then echo 错误未找到 ImageMagick 的 convert 命令。 echo 请安装 ImageMagick sudo apt-get install imagemagick (Debian/Ubuntu) 或 sudo yum install imagemagick (RHEL/CentOS) exit 1 fi # 检查源目录参数 if [ -z $SOURCE_DIR ] || [ ! -d $SOURCE_DIR ]; then echo 用法: $0 源图片目录 [目标目录] echo 示例: $0 ./photos ./output exit 1 fi # 创建目标目录 mkdir -p $TARGET_DIR # 支持的图片扩展名列表 IMAGE_EXTS(jpg jpeg png gif bmp tiff) INDEX1 # 遍历源目录下所有文件 find $SOURCE_DIR -type f | while read -r SRC_IMAGE; do # 获取文件扩展名小写 EXT$(echo ${SRC_IMAGE##*.} | tr [:upper:] [:lower:]) # 检查扩展名是否在支持列表中 if [[ ${IMAGE_EXTS[]} ~ ${EXT} ]]; then # 生成目标文件名日期-序号.格式 DATE_PREFIX$(date %Y%m%d) TARGET_NAME${DATE_PREFIX}-$(printf %04d $INDEX).${TARGET_FORMAT} TARGET_PATH${TARGET_DIR}/${TARGET_NAME} echo 正在处理: $(basename $SRC_IMAGE) - $TARGET_NAME # 使用 ImageMagick 的 convert 命令进行转换和缩放 convert $SRC_IMAGE -resize ${SIZE} -quality 85 $TARGET_PATH # 检查上一条命令是否成功 if [ $? -eq 0 ]; then echo 成功: $TARGET_PATH ((INDEX)) else echo 失败: 处理 $SRC_IMAGE 时出错。 fi else echo 跳过非图片文件: $SRC_IMAGE fi done echo 批量处理完成。共处理了 $((INDEX-1)) 张图片。脚本解析与技巧${2:-./processed_images}参数扩展语法。如果第二个参数$2为空或未设置则使用默认值./processed_images。command -v安全地检查一个命令是否存在。find ... -type f | while read使用find命令安全地遍历所有文件包括子目录并通过管道传递给while read循环逐行处理。这种方式比for file in *更能处理带空格或特殊字符的文件名。${SRC_IMAGE##*.}参数扩展删除从开头到最后一个.的所有字符即获取扩展名。tr ‘[:upper:]’ ‘[:lower:]’将扩展名转换为小写方便统一判断。[[ “ ${IMAGE_EXTS[]} “ ~ “ ${EXT} “ ]]检查扩展名是否在数组列表中这是一种安全的成员检查方法。convert “$SRC_IMAGE” -resize “${SIZE}”ImageMagick的转换命令。${SIZE}表示只缩小不放大即如果图片大于800x600则缩小小于则保持原尺寸。$?获取上一条命令convert的退出状态码0表示成功。5. 脚本调试与错误处理实战指南写脚本不可能一帆风顺学会调试和优雅地处理错误是进阶的必经之路。5.1 调试技巧与工具启用调试模式bash -x script.sh在脚本执行时打印出每一行命令及其扩展后的参数。这是最强大的调试工具。在脚本内部启用在脚本开头或特定段之前加入set -x在段尾加入set x。#!/bin/bash set -x # 开启调试 VARtest echo $VAR set x # 关闭调试 echo 调试结束详细输出模式bash -v script.sh。在执行前打印出脚本的每一行未经扩展的原始内容。常与-x结合使用bash -xv script.sh。语法检查bash -n script.sh。只检查语法错误不执行脚本。在运行前做一次快速检查很有用。使用trap命令捕获信号trap可以在脚本收到退出信号如用户按CtrlC时执行清理工作。#!/bin/bash TEMP_FILE/tmp/mytemp.$$ cleanup() { echo 正在清理临时文件... rm -f $TEMP_FILE echo 退出。 } # 捕获 EXIT 信号脚本正常或异常退出时、INT 信号CtrlC、TERM 信号kill命令 trap cleanup EXIT INT TERM # 脚本主体 echo 创建临时文件 $TEMP_FILE touch $TEMP_FILE # ... 一些操作 ... read -p 按回车继续或CtrlC中断...这样无论脚本如何结束cleanup函数都会被调用确保临时文件被删除。5.2 错误处理与脚本健壮性一个健壮的脚本应该能预见并妥善处理错误。立即退出 on Error在脚本开头加上set -e或set -o errexit。这会让脚本在任何命令非条件判断部分返回非零状态时立即退出。防止错误累积。#!/bin/bash set -e cd /some/nonexistent/directory # 如果目录不存在cd失败脚本立即终止 echo 这行不会被执行捕获管道命令的失败set -o pipefail。默认情况下管道命令的退出状态是最后一个命令的。加上这个选项后管道中任何一个命令失败整个管道命令的退出状态就是非零。set -e set -o pipefail some_command | grep pattern | head -5 # 如果 some_command 失败整个管道失败脚本会因 set -e 而退出。处理未定义的变量set -u或set -o nounset。尝试使用未定义的变量时脚本会报错并退出。这能避免因拼写错误导致的诡异问题。#!/bin/bash set -u echo $MY_VAR # 如果MY_VAR未定义脚本会在这里报错退出综合使用我通常在严肃的脚本开头加上这三行它们构成了“安全模式”。#!/bin/bash set -euo pipefail注意set -e在某些情况下可能过于严格比如你故意要测试一个命令是否失败if ! command; then...。这时可以在特定行暂时关闭它set e执行完后再set -e。自定义错误处理结合trap和set -e可以定义更精细的错误处理函数。#!/bin/bash set -euo pipefail handle_error() { local exit_code$? local line_no$1 echo 错误发生在第 ${line_no} 行退出码: ${exit_code} 2 echo 命令行: ${BASH_COMMAND} 2 # 可以在这里发送警报、记录详细日志等 exit $exit_code } trap handle_error $LINENO ERR当任何命令失败触发ERR信号时handle_error函数会被调用并传入行号。5.3 常见问题速查与解决下表列出了一些Shell脚本编写中常见的“坑”及其解决方法问题现象可能原因解决方案[: too many arguments]或[: unexpected operator变量未加引号且变量值为空或包含空格导致[ ]测试语法错误。始终用双引号引用变量[ “$var” “value” ]或使用[[ ]]。command not found命令不存在或命令路径不在PATH中。使用command -v cmd检查使用绝对路径或通过包管理器安装。脚本在Windows编辑后在Linux执行报错^M行尾符不兼容Windows是CRLF\r\nLinux是LF\n。使用dos2unix script.sh转换或在编辑器中设置为Unix(LF)格式。循环中文件名带空格时被拆分默认的for file in *会按空格、制表符、换行符拆分。使用find配合while readfind . -type f -name “*.txt” -print0 | while IFS read -r -d $‘\0’ file; do ...脚本执行权限正确但提示Permission denied可能脚本所在的分区挂载时加了noexec选项。检查挂载选项mount | grep /your/path或使用bash script.sh方式执行。变量赋值后在管道或子Shell中修改无效管道和( )会创建子Shell变量修改不影响父Shell。避免在管道中修改变量使用进程替换或重定向到文件传递数据。Syntax error: unexpected end of file通常是因为if/for/while缺少对应的fi/done或者引号不匹配。仔细检查代码块是否闭合。使用bash -n script.sh进行语法检查。脚本后台运行但输出混乱或日志不全标准输出和错误输出未妥善处理可能被终端中断影响。重定向输出到文件nohup script.sh output.log 21 。6. 进阶技巧与最佳实践当你掌握了基础下面这些技巧能让你的脚本从“能用”变得“专业”和“优雅”。6.1 使用配置文件分离变量将脚本中可能变化的参数如路径、阈值、邮箱地址提取到单独的配置文件中使脚本更易于维护和复用。config.cfg:# 应用配置 LOG_DIR/var/log/myapp BACKUP_DIR/backup MAX_LOG_DAYS30 ALERT_EMAILS(admin1example.com admin2example.com)script.sh:#!/bin/bash set -euo pipefail # 加载配置文件 CONFIG_FILE${1:-./config.cfg} if [ ! -f $CONFIG_FILE ]; then echo 配置文件 $CONFIG_FILE 不存在。 exit 1 fi # 安全地加载配置文件 source $CONFIG_FILE # 现在可以使用配置中的变量了 echo “日志目录: $LOG_DIR” for email in “${ALERT_EMAILS[]}”; do echo “报警邮箱: $email” done6.2 使用getopts处理命令行选项对于需要复杂参数输入的脚本getopts是标准且强大的工具。#!/bin/bash # backup.sh - 带选项的备份脚本 usage() { echo “用法: $0 [-v] [-d 目录] [-c] [-h]” echo “ -v 详细模式” echo “ -d 指定备份目录 (默认: ./backup)” echo “ -c 启用压缩” echo “ -h 显示此帮助信息” exit 1 } VERBOSE0 BACKUP_DIR./backup COMPRESS0 while getopts “:vd:ch” opt; do case ${opt} in v) VERBOSE1 echo “启用详细模式。” ;; d) BACKUP_DIR$OPTARG ;; c) COMPRESS1 ;; h) usage ;; \?) echo “无效选项: -$OPTARG” 2 usage ;; :) echo “选项 -$OPTARG 需要一个参数。” 2 usage ;; esac done shift $((OPTIND -1)) # 移除已处理的选项剩下的是位置参数 # 现在 $1, $2... 是脚本的非选项参数 TARGET_DIR$1 if [ -z “$TARGET_DIR” ]; then echo “错误必须指定要备份的源目录。” 2 usage fi echo “备份 $TARGET_DIR 到 $BACKUP_DIR” if [ $COMPRESS -eq 1 ]; then echo “启用压缩” fi # ... 备份逻辑 ...6.3 性能考量与优化避免在循环中调用外部命令在循环体内频繁调用grep、awk、sed等外部命令会极大降低性能。尽量将数据一次性读入在循环外处理或使用Shell内置的字符串操作。差for file in *; do grep “pattern” “$file”; done好grep “pattern” *如果可能或使用awk/sed一次性处理所有文件。使用$( )代替反引号$( )更清晰且支持嵌套。对于大量文本处理考虑使用awk或sed它们通常比纯Shell循环快得多。使用[[ ]]代替[ ][[ ]]是Shell关键字性能稍好功能更强。6.4 代码风格与可读性缩进统一使用4个空格进行缩进。不要使用Tab因为不同环境Tab显示宽度可能不同。命名变量和函数名使用有意义的英文常量或环境变量用大写加下划线局部变量用小写。注释在文件开头说明脚本用途、作者、参数在复杂逻辑块前说明意图对不明显的技巧或坑加以注释。函数将功能模块封装成函数函数名以动词开头清晰描述其行为。长行拆分对于过长的命令使用反斜杠\进行换行提高可读性。使用shellcheck这是一个静态分析工具可以检查Shell脚本中的常见错误、不规范的写法以及潜在问题。强烈建议在写完脚本后用它检查一遍。# 安装 shellcheck # Debian/Ubuntu: sudo apt install shellcheck # RHEL/CentOS: sudo yum install epel-release sudo yum install shellcheck shellcheck your_script.sh从我个人的经验来看Shell脚本的魅力在于它的直接和强大。它可能没有Python、Go那样丰富的库和优雅的语法但在系统管理、文件操作和流程胶水方面它依然是无可替代的利器。最关键的是它就在那里是每个Linux系统自带的工具。花时间学好Shell脚本就像学会熟练使用螺丝刀和扳手它们能让你在数字世界里更自如地搭建和修理任何东西。开始写吧从自动化你手头最烦人的那件重复工作开始。