1. 为什么说Shell脚本是程序员的“瑞士军刀”?
如果你在Linux或macOS的终端里敲过命令,那你已经半只脚踏入了Shell脚本的世界。很多人觉得这玩意儿就是一堆命令的堆砌,没什么技术含量,但恰恰相反,Shell脚本是连接用户与操作系统内核最直接、最高效的桥梁,堪称运维、开发乃至数据分析领域的“瑞士军刀”。我见过太多同事,面对重复性的文件整理、日志分析、服务部署,还在手动一行行敲命令,或者用图形界面点点点,效率低下还容易出错。而一个几十行的脚本,就能把这些工作自动化,解放双手,把精力留给更有创造性的思考。
Shell脚本的核心价值在于“自动化”和“胶水”。它能把系统命令、文本处理工具(如grep、awk、sed)、甚至其他编程语言(如Python)的执行结果,像胶水一样粘合起来,形成一个完整的工作流。比如,你每天需要从十几台服务器上拉取日志,过滤错误信息,汇总成报告并邮件发送。手动做?半小时起步。写个Shell脚本?设置个定时任务(cron job),从此每天早晨喝咖啡时报告就在邮箱里了。这种从重复劳动中解脱出来的感觉,是提升工程师幸福感的关键。
网上教程很多,但要么过于零散,只讲几个命令;要么一上来就抛出复杂的语法,让人望而生畏。我结合自己十多年的踩坑经验,打算系统性地拆解Shell脚本,目标不是让你死记硬背语法,而是建立一种“脚本化思维”。看到任何重复性任务,第一反应就是:“能不能写个脚本搞定它?” 这篇内容会从最基础的“Hello World”开始,一直讲到脚本调试、错误处理、性能优化等高级话题,并穿插大量真实工作场景中的案例和那些官方手册里不会写的“坑”。无论你是刚接触Linux的新手,还是想系统提升自动化能力的老兵,相信都能找到你需要的东西。
2. Shell脚本学习路径全景图与核心思想
学习任何技术,最怕没有地图盲目乱撞。Shell脚本的学习,我总结为“三层金字塔”结构:基础命令层、流程控制层、工程实践层。绝大多数人卡在第一层和第二层之间,因为命令看似简单,组合起来却千变万化。
2.1 基础命令层:你的工具箱这一层是基石,目标是熟练使用二三十个最核心的命令和概念。别被上百个命令吓到,常用的就那些:
- 文件操作:
ls,cd,cp,mv,rm,mkdir,find。光是find命令配合-exec参数,就能解决大半的文件查找与批量处理问题。 - 文本处理三剑客:
grep(搜索)、sed(流编辑)、awk(文本分析)。这是Shell脚本的灵魂,数据处理能力大半靠它们。初期不必深究awk的全部语法,学会用awk ‘{print $1}’提取第一列就很有用。 - 系统状态:
ps,top,df,du,free。写监控脚本必备。 - 权限与用户:
chmod,chown,sudo。理解755、644这些数字背后的二进制含义(rwx),是安全编写脚本的前提。
注意:很多人喜欢死记硬背命令参数。我的建议是,学会用
man命令(如man grep)和--help参数。更重要的是,理解每个命令的“输入”和“输出”。在Shell的世界里,几乎所有命令都默认从“标准输入”读取数据,将结果输出到“标准输出”,将错误输出到“标准错误”。理解了这个“流”的概念,你才能明白管道符|为何如此强大。
2.2 流程控制层:赋予脚本逻辑有了工具,你需要用逻辑把它们串起来。这是Shell编程的核心。
- 变量与参数:如何定义变量(
VAR=value),如何使用它($VAR),如何传递外部参数($1,$2,$@)。这里有个大坑:等号两边不能有空格!VAR = value是错误的。 - 条件判断:
if...then...elif...else...fi。判断条件用[ ]或[[ ]](后者更强大,支持正则匹配)。记住,字符串比较用=,数字比较用-eq,文件判断用-f(是否存在文件)。 - 循环:
for i in list; do ... done和while condition; do ... done。for循环常用于遍历文件列表,while循环常用于读文件或等待某个条件。 - 函数:将一段代码封装起来,
function_name() { ... }。好的脚本一定是模块化的,用函数来提高可读性和复用性。
2.3 工程实践层:从脚本到可靠工具这是区分“能用”和“好用”的关键。很多脚本跑一次就扔,而工程化的脚本应该像产品一样可靠。
- 调试与错误处理:使用
set -e(遇到错误立即退出)、set -u(使用未定义变量时报错)、set -x(打印执行的每一行命令)。在关键操作后检查上一条命令的返回值$?,不为0则表示失败。 - 信号处理:用
trap命令捕获Ctrl+C(SIGINT)等信号,在脚本被中断时执行清理工作(如删除临时文件)。 - 代码风格与文档:统一的缩进(推荐两个空格),有意义的变量名,在文件开头用注释说明脚本用途、参数、作者和示例。这对自己和同事都是一种尊重。
- 性能考量:避免在循环中调用外部命令(特别是管道),尽量使用Shell内置功能。处理大文件时,考虑用
awk或sed替代while read line循环。
我的学习建议是:按层推进,每层都通过实际小项目巩固。比如,学完基础命令,就写个脚本统计当前目录下各种类型文件的个数;学完流程控制,就写个备份脚本,可以按日期命名备份文件;学完工程实践,就给备份脚本加上日志记录和错误报警功能。
3. 从零到一:你的第一个专业级Shell脚本剖析
光说不练假把式。我们从一个具体的、有实用价值的脚本开始,逐行拆解,把上面提到的概念具象化。假设我们有这样一个需求:监控指定目录的大小,如果超过设定阈值,则自动清理最早的文件,并发送邮件通知。这是一个非常经典的运维场景。
3.1 脚本框架与参数解析一个健壮的脚本,应该从清晰的框架开始。我们首先定义脚本的元信息和参数处理。
#!/bin/bash # ============================================ # 脚本名称:directory_cleaner.sh # 功能描述:监控目录大小,超限后清理最旧文件并告警 # 作者:Your Name # 使用方法:./directory_cleaner.sh /path/to/dir 1024 admin@example.com # 参数说明:$1 - 监控目录路径 # $2 - 目录大小阈值(单位:MB) # $3 - 告警邮件接收地址 # ============================================ set -euo pipefail # 严格模式:遇错退出,未定义变量报错,管道中任意命令失败则整个管道失败 # 参数检查 if [ $# -ne 3 ]; then echo "错误:参数数量不正确。" echo "用法:$0 <目录路径> <阈值(MB)> <邮箱地址>" exit 1 fi TARGET_DIR="$1" SIZE_LIMIT_MB="$2" ALERT_EMAIL="$3"代码解读与避坑:
- 第一行
#!/bin/bash是shebang,告诉系统用哪个解释器执行。虽然有时省略也能运行,但显式声明是好习惯,避免因默认Shell不同(如dash)导致语法错误。 set -euo pipefail是编写可靠脚本的“黄金法则”。-e确保任何命令失败(返回非0)脚本立即停止,防止错误累积。-u防止使用未赋值的变量(常见错误来源)。-o pipefail确保管道命令中任何一个失败,整个管道返回值就视为失败。这行代码能帮你避免很多隐蔽的bug。- 参数检查
if [ $# -ne 3 ]必不可少。$#代表参数个数。缺少检查,脚本可能用空变量去操作,导致破坏性后果(比如rm -rf $undefined_var/)。 - 变量赋值时,路径、邮箱这类可能包含空格的参数,一定要用双引号引起来,如
“$1”。这是防止文件名中有空格导致命令被错误分割的关键。
3.2 核心逻辑实现:监控与清理接下来,我们实现核心的目录大小检查和清理逻辑。
# 转换阈值为KB(`du`命令默认输出KB) SIZE_LIMIT_KB=$(( SIZE_LIMIT_MB * 1024 )) # 获取目录当前大小(单位:KB),仅统计文件大小,排除子目录本身占用的元数据 CURRENT_SIZE_KB=$(du -sk “$TARGET_DIR” | cut -f1) # 记录日志函数 log_message() { local log_level="$1" local message="$2" echo “[$(date ‘+%Y-%m-%d %H:%M:%S’)] [$log_level] $message” } log_message “INFO” “开始检查目录:$TARGET_DIR,当前大小:${CURRENT_SIZE_KB}KB,阈值:${SIZE_LIMIT_KB}KB” if [ “$CURRENT_SIZE_KB” -gt “$SIZE_LIMIT_KB” ]; then log_message “WARN” “目录大小超出阈值,开始清理最旧文件...” # 进入目标目录,防止后续`rm`命令因路径问题误删 cd “$TARGET_DIR” || { log_message “ERROR” “无法进入目录 $TARGET_DIR”; exit 1; } # 查找并删除最旧的一个普通文件(排除目录) OLDEST_FILE=$(find . -maxdepth 1 -type f -printf ‘%T+ %p\n’ | sort | head -n 1 | cut -d‘ ’ -f2-) if [ -n “$OLDEST_FILE” ]; then log_message “INFO” “正在删除最旧文件:$OLDEST_FILE” rm -f “$OLDEST_FILE” # 使用-f强制删除,避免交互提示 # 重新计算大小 CURRENT_SIZE_KB=$(du -sk “$TARGET_DIR” | cut -f1) log_message “INFO” “清理完成。目录新大小:${CURRENT_SIZE_KB}KB” # 准备邮件内容 MAIL_SUBJECT=“【目录清理告警】$TARGET_DIR 空间已清理” MAIL_BODY=“监控目录:$TARGET_DIR 原大小:${CURRENT_SIZE_KB}KB(超过阈值 ${SIZE_LIMIT_KB}KB) 已删除文件:$OLDEST_FILE 清理后大小:${CURRENT_SIZE_KB}KB 操作时间:$(date)” # 发送邮件(假设系统已配置好mail命令或sendmail) echo “$MAIL_BODY” | mail -s “$MAIL_SUBJECT” “$ALERT_EMAIL” 2>/dev/null || log_message “ERROR” “邮件发送失败,请检查邮件配置。” else log_message “ERROR” “未在目录中找到可清理的普通文件。” fi else log_message “INFO” “目录大小正常,无需清理。” fi关键点解析与实战技巧:
du -sk与cut的组合:du -s是总计,-k是以KB为单位。cut -f1是取第一列(即大小数值)。这是一种经典的“命令+管道”提取特定信息的模式。find命令的妙用:find . -maxdepth 1 -type f查找当前目录(非递归)下的所有普通文件。-printf ‘%T+ %p\n’是GNU find的特性,以YYYY-MM-DD+HH:MM:SS格式打印文件最后修改时间,然后空格,再跟文件名。这样排序时,时间戳在前的就是最旧的文件。sort默认按字符串升序排列,时间戳格式正好符合。head -n 1取第一行,cut -d‘ ’ -f2-以空格为分隔符,取第二列到最后一列(即文件名)。这个命令链是高效查找最旧文件的核心。cd命令的错误处理:cd “$TARGET_DIR” || { ...; exit 1; }。如果cd失败(目录不存在或无权限),||后面的命令块会执行,记录错误并退出。这是利用Shell逻辑运算符进行错误处理的典型做法。- 邮件发送的容错:
mail命令不一定在所有系统都可用。2>/dev/null将错误输出重定向到空设备,||后面跟一个日志记录,这样即使邮件发送失败,脚本也不会因此中断,只是记录一个错误。在生产环境中,你可能需要替换为更可靠的发送方式,如使用Python的smtplib库或第三方命令行工具。 - 日志函数:自定义
log_message函数,统一日志格式(时间、级别、信息),便于后续用grep等工具分析。这是脚本可维护性的重要体现。
4. 文本处理三剑客(grep, sed, awk)的深度应用与性能抉择
Shell脚本的强大,一半在于它能方便地调用像grep、sed、awk这样的专业文本处理工具。它们各有专长,用对了事半功倍,用错了或混用则可能成为性能瓶颈。
4.1 grep:模式搜索的利刃grep的核心是“过滤”。它从输入中快速找出匹配指定模式的行。
- 基础用法:
grep “error” logfile.log查找包含“error”的行。 - 常用参数:
-i:忽略大小写。-v:反向选择,输出不匹配的行。-n:显示匹配行的行号。-c:只统计匹配的行数。-r:递归搜索目录下的所有文件。-E:启用扩展正则表达式(等同于egrep),功能更强大。
- 高级技巧与避坑:
- 固定字符串搜索:当模式是简单的字符串而非正则时,使用
-F(或fgrep)速度更快,因为它不做正则解析。 - 上下文查看:
-A 5(After)显示匹配行后5行,-B 5(Before)显示前5行,-C 5(Context)显示前后各5行。这在看日志时非常有用。 - 性能陷阱:在循环中对大文件反复使用
grep是性能杀手。应该尽量将grep放在管道的最前面,或者一次性用grep -r处理多个文件。
- 固定字符串搜索:当模式是简单的字符串而非正则时,使用
4.2 sed:流编辑器,擅长“以行为单位”的编辑sed的核心是“替换”和“删除”。它按行读取输入,根据规则进行修改后输出。
- 基础替换:
sed ‘s/old/new/g’ file.txt。s表示替换,g表示全局(一行内所有匹配)。默认只替换每行第一个匹配。 - 指定行范围:
sed ‘10,20s/old/new/g’只替换第10到20行。 - 删除行:
sed ‘/pattern/d’删除匹配pattern的行。sed ‘3d’删除第3行。 - 原地编辑:
sed -i ‘s/old/new/g’ file.txt。-i选项会直接修改原文件,务必先备份或测试无误后再使用,这是血泪教训。 - 实战案例:批量修改配置文件中的IP地址。
这里# 假设要将所有192.168.1.x的IP改为10.0.0.x sed -i.bak ‘s/192\.168\.1\./10.0.0./g’ /path/to/*.conf-i.bak会在修改前为每个文件创建一个带.bak后缀的备份,是安全操作的好习惯。
4.3 awk:编程式的文本分析工具awk不仅仅是一个命令,它是一门拥有变量、条件、循环、函数的微型编程语言,特别擅长处理结构化文本(如CSV、日志)。
- 基本结构:
awk ‘pattern { action }’ file。对文件中每一行,如果匹配pattern,就执行action。 - 内置变量:
$0代表整行,$1,$2…代表第一、第二列(默认以空格或制表符分隔)。NF是当前行的字段数,NR是当前行号。 - 经典示例:
- 打印第一列和第三列:
awk ‘{print $1, $3}’ data.txt - 统计文件行数:
awk ‘END {print NR}’ data.txt。END模式在所有行处理完后执行。 - 计算第二列的总和:
awk ‘{sum += $2} END {print sum}’ data.txt - 根据条件过滤:
awk ‘$3 > 100 {print $0}’ data.txt打印第三列大于100的行。
- 打印第一列和第三列:
- 高级用法:字段分隔符与数组
# 处理CSV文件(逗号分隔) awk -F‘,’ ‘{print $2}’ data.csv # 统计每个IP出现的次数(假设第一列是IP) awk ‘{ip_count[$1]++} END {for(ip in ip_count) print ip, ip_count[ip]}’ access.log-F指定字段分隔符。ip_count[$1]++利用了awk的关联数组,是非常强大的聚合分析功能。
4.4 性能抉择与组合使用原则面对一个文本处理任务,如何选择工具?记住这个口诀:简单查找用grep,简单替换用sed,列处理与计算用awk。
- 能用内置功能就不用外部命令:在Shell脚本中,
while read循环通常比awk慢。例如,逐行处理文件并提取某列,用awk ‘{print $2}’比while read line; do echo $line | cut -d‘ ’ -f2; done快得多。 - 减少管道数量:每个管道
|都会创建一个新的子进程,有开销。例如,cat file | grep A | grep B可以合并为grep A file | grep B,甚至直接用grep -E ‘A.*B|B.*A’ file(如果逻辑允许)。awk本身功能强大,很多时候可以替代grep+sed+cut的组合。 - 处理大文件时:优先考虑
awk和sed,它们是为流式处理设计的。避免使用for i in $(cat hugefile),这会把整个文件读入内存并分词,非常低效。应该用while IFS= read -r line; do ... done < hugefile。
5. Shell脚本中的“坑”与高级调试技巧
即使语法熟练,写Shell脚本也难免踩坑。很多错误静默发生,直到造成后果才发现。这部分分享我积累的常见“坑”和调试方法,相当于给你的脚本上了保险。
5.1 变量与字符串处理的经典陷阱
未引用的变量导致分词和路径扩展:
# 错误示范 for file in $(ls *.txt); do rm $file # 如果文件名包含空格,如“my file.txt”,会被拆分成“my”和“file.txt”两个参数,导致错误 done # 正确做法:使用数组或find的-exec for file in *.txt; do rm “$file” done # 或者 find . -name “*.txt” -exec rm {} \;始终记住:变量展开、命令替换、路径名生成时,一定要用双引号。
数字与字符串比较混淆:
a=“10” b=“2” # 错误:使用字符串比较运算符 if [ “$a” > “$b” ]; then ... # 这会进行字典序比较,“10” < “2”,因为‘1’比‘2’小。 # 正确:使用算术比较 if (( a > b )); then ... # 双括号内进行算术运算和比较 # 或者 if [ “$a” -gt “$b” ]; then ... # -gt 是用于整数的“大于”命令替换中的尾随换行符:
file_count=$(ls | wc -l) # wc -l 输出结果会包含换行符,如“5\n” # 直接使用可能导致问题,可以用命令替换+echo去换行 file_count=$(ls | wc -l | tr -d ‘\n’) # 或者,在需要数字比较的算术上下文中,Shell会自动去除尾随空白 if (( $(ls | wc -l) > 10 )); then ... # 这里没问题
5.2 错误处理与信号捕获脚本可能因各种原因失败:命令不存在、权限不足、磁盘满、用户中断(Ctrl+C)。健壮的脚本必须处理这些情况。
- 检查命令返回值:关键命令执行后,立即检查
$?。cp important.txt backup/ if [ $? -ne 0 ]; then log_message “ERROR” “复制文件失败!” exit 1 fi # 更简洁的写法:利用逻辑运算符 cp important.txt backup/ || { log_message “ERROR” “复制失败”; exit 1; } - 使用
trap捕获信号:
这能防止脚本被意外中断后,留下垃圾临时文件。# 定义清理函数 cleanup() { echo “正在清理临时文件...” rm -f /tmp/myscript_temp.* exit 1 } # 注册信号处理函数:当收到SIGINT (Ctrl+C), SIGTERM (终止信号)时,执行cleanup trap cleanup INT TERM # 脚本主体... # 脚本正常退出前,也可以手动移除trap trap - INT TERM
5.3 高级调试技巧当脚本行为诡异时,你需要像侦探一样排查。
set -x与set +x:这是最直接的调试方法。set -x会让Shell打印出每一行实际执行的命令(变量已展开)。在怀疑出问题的代码块前后加上set -x和set +x。- 输出关键变量:在怀疑点插入
echo “DEBUG: var=$var”,查看变量实际值。 - 使用ShellCheck:这是一个静态分析工具,能检测出脚本中的语法问题、常见错误和不良实践。在编写完成后用
shellcheck your_script.sh检查一下,能避免很多低级错误。 - 逐段测试:将复杂脚本分解成函数,然后单独测试每个函数。可以创建一个专门的
test_函数来调用它们,验证输入输出是否符合预期。
5.4 安全注意事项Shell脚本权限很大,一个疏忽可能造成数据丢失。
- 执行删除操作前,先
echo:在写rm -rf命令时,可以先写成echo rm -rf “$file”,运行脚本看看会删除哪些文件,确认无误后再去掉echo。 - 对用户输入保持警惕:如果脚本接收用户输入作为参数或变量,一定要进行验证。特别是当输入会用于命令拼接时,有命令注入的风险。
# 危险! user_input=“/tmp; rm -rf /” rm -rf “$user_input” # 如果变量未加引号,后果不堪设想。加了引号会将其视为一个整体路径,安全很多,但仍需验证路径合法性。 # 应对:对路径参数,可以用realpath、dirname等命令规范化并检查是否在允许的目录内。 - 使用最小权限原则:不要用root权限运行所有脚本。考虑是否需要
sudo,以及是否可以通过配置/etc/sudoers文件精细控制权限。
6. 实战:构建一个带配置文件和日志的系统监控脚本
我们将前面所有知识融会贯通,写一个更接近生产环境的实用脚本:一个系统资源监控脚本,它能读取外部配置文件,监控CPU、内存、磁盘使用率,超过阈值则记录日志并发送告警(模拟),并且所有行为都可配置。
6.1 项目结构与配置文件设计一个好的脚本项目应该有清晰的结构。我们这样组织:
system_monitor/ ├── monitor.sh # 主脚本 ├── monitor.conf # 配置文件 └── logs/ # 日志目录(脚本自动创建)配置文件monitor.conf使用易于解析的key=value格式:
# 系统监控配置 ALERT_CPU_PERCENT=80 ALERT_MEM_PERCENT=85 ALERT_DISK_PERCENT=90 LOG_DIR=“./logs” LOG_FILE=“system_monitor.log” # 是否启用模拟告警(1启用,0禁用) ENABLE_ALERT=1 ALERT_METHOD=“log” # 可选 log | echo (模拟)6.2 主脚本实现(monitor.sh)这个脚本展示了模块化、配置化、日志化的完整思路。
#!/bin/bash set -euo pipefail # ============================================ # 系统资源监控脚本 # 功能:读取配置,检查CPU/内存/磁盘使用率,超阈值告警 # ============================================ SCRIPT_DIR=“$(cd “$(dirname “${BASH_SOURCE[0]}”)” && pwd)” # 获取脚本所在目录的绝对路径 CONFIG_FILE=“${SCRIPT_DIR}/monitor.conf” # 默认配置,防止配置文件缺失 ALERT_CPU_PERCENT=80 ALERT_MEM_PERCENT=85 ALERT_DISK_PERCENT=90 LOG_DIR=“${SCRIPT_DIR}/logs” LOG_FILE=“system_monitor.log” ENABLE_ALERT=1 ALERT_METHOD=“log” # 加载配置文件 load_config() { if [ ! -f “$CONFIG_FILE” ]; then echo “[$(date)] [WARN] 配置文件 $CONFIG_FILE 不存在,使用默认配置。” >&2 return 1 fi # 安全地source配置文件,避免配置文件中恶意命令执行 while IFS=‘=’ read -r key value; do # 跳过注释行和空行 [[ “$key” =~ ^[[:space:]]*# ]] && continue [[ -z “$key” ]] && continue # 去除可能的空白字符 key=“$(echo -e “${key}” | sed -e ‘s/^[[:space:]]*//’ -e ‘s/[[:space:]]*$//’)” value=“$(echo -e “${value}” | sed -e ‘s/^[[:space:]]*//’ -e ‘s/[[:space:]]*$//’)” # 使用declare动态设置变量(更安全) declare -g “$key”=“$value” 2>/dev/null || echo “[$(date)] [WARN] 无法设置配置项: $key” >&2 done < “$CONFIG_FILE” } # 初始化日志系统 init_log() { mkdir -p “$LOG_DIR” LOG_PATH=“${LOG_DIR}/${LOG_FILE}” } # 日志记录函数 log() { local level=“$1” local message=“$2” local timestamp timestamp=“$(date ‘+%Y-%m-%d %H:%M:%S’)” echo “[${timestamp}] [${level}] ${message}” | tee -a “$LOG_PATH” } # 告警函数 alert() { local resource=“$1” local usage=“$2” local threshold=“$3” local message=“${resource} 使用率 ${usage}% 超过阈值 ${threshold}%” if [ “$ENABLE_ALERT” -eq 1 ]; then case “$ALERT_METHOD” in “log”) log “ALERT” “$message” ;; “echo”) echo “【告警】$message” >&2 # 输出到标准错误,便于区分 ;; *) log “ERROR” “未知的告警方式: $ALERT_METHOD” ;; esac fi } # 获取CPU使用率(取1秒内的平均使用率,简单方法) get_cpu_usage() { # 读取/proc/stat,计算空闲时间和总时间差值的比例 local cpu_line cpu_line=$(grep ‘^cpu ‘ /proc/stat) local idle1 idle2 total1 total2 # 解析:user nice system idle iowait irq softirq steal guest guest_nice # 我们主要关注idle(第4列)和iowait(第5列)?实际上,idle+iowait算作空闲。 # 更常见的简化:使用`top`或`mpstat`,这里用`vmstat`取一个采样 # 为了可移植性,使用一个简单但近似的命令(注意:这在不同系统上可能不准,生产环境建议用更精确的方法) local usage usage=$(top -bn1 | grep “%Cpu(s)” | awk ‘{print 100 - $8}’) # 如果上述命令失败,使用备选方案 if [[ ! “$usage” =~ ^[0-9]+(\.[0-9]+)?$ ]]; then usage=$(vmstat 1 2 | tail -1 | awk ‘{print 100 - $15}’) fi echo “${usage%.*}” # 取整 } # 获取内存使用率 get_mem_usage() { local mem_info mem_info=$(free | grep Mem) local total used total=$(echo “$mem_info” | awk ‘{print $2}’) used=$(echo “$mem_info” | awk ‘{print $3}’) # 计算百分比 echo $(( used * 100 / total )) } # 获取根分区磁盘使用率 get_disk_usage() { df -h / | tail -1 | awk ‘{print $5}’ | sed ‘s/%//’ } # 主监控逻辑 main() { load_config init_log log “INFO” “===== 系统监控开始 =====" local cpu_usage mem_usage disk_usage cpu_usage=$(get_cpu_usage) log “INFO” “CPU使用率: ${cpu_usage}%” if [ “$cpu_usage” -gt “$ALERT_CPU_PERCENT” ]; then alert “CPU” “$cpu_usage” “$ALERT_CPU_PERCENT” fi mem_usage=$(get_mem_usage) log “INFO” “内存使用率: ${mem_usage}%” if [ “$mem_usage” -gt “$ALERT_MEM_PERCENT” ]; then alert “内存” “$mem_usage” “$ALERT_MEM_PERCENT” fi disk_usage=$(get_disk_usage) log “INFO” “磁盘使用率: ${disk_usage}%” if [ “$disk_usage” -gt “$ALERT_DISK_PERCENT” ]; then alert “磁盘” “$disk_usage” “$ALERT_DISK_PERCENT” fi log “INFO” “===== 系统监控结束 =====" } # 脚本入口 main “$@”6.3 关键技术与经验点解析
- 安全的配置文件加载:我们没有直接用
source monitor.conf,而是用while read循环逐行解析。这是因为source会直接执行文件中的任何Shell代码,如果配置文件被恶意篡改或编写不当,可能带来安全风险。我们的方法只进行变量赋值,更安全。 SCRIPT_DIR的获取:“$(cd “$(dirname “${BASH_SOURCE[0]}”)” && pwd)”是获取脚本所在目录绝对路径的经典且可靠的方法,无论从何处调用脚本,都能准确定位配置文件和日志目录的相对位置。- 模块化与函数化:每个功能(加载配置、初始化日志、获取指标)都封装成函数,
main函数清晰表达了业务流程。这大大提升了代码的可读性和可维护性。 - 命令的兼容性与降级方案:在
get_cpu_usage函数中,我们首先尝试用top命令,如果输出格式不符或命令不存在,则降级使用vmstat。在实际生产脚本中,这种兼容性处理非常重要,因为不同Linux发行版的命令输出可能有细微差别。 - 日志的
tee用法:log函数中使用tee -a “$LOG_PATH”,既将日志打印到标准输出(方便实时查看),又追加到日志文件中。这在调试和长期运行中非常有用。 - 模拟告警机制:通过
ENABLE_ALERT和ALERT_METHOD配置项,我们可以灵活控制告警行为。在测试阶段可以设为echo,在生产环境可以改为调用真实的邮件、短信或API接口。这种设计使得脚本的核心监控逻辑与具体的告警实现解耦。
你可以通过crontab设置这个脚本每分钟运行一次:* * * * * /path/to/system_monitor/monitor.sh。一个简单的监控系统就搭建完成了。通过修改配置文件,你可以轻松调整阈值、日志路径和告警方式,而无需修改主脚本代码。这就是Shell脚本结合配置化思想带来的灵活性。