Shell脚本入门从Hello World到自动化任务附实战案例很多刚接触Linux或macOS的朋友可能都听过“Shell脚本”这个词感觉它既神秘又强大。其实它远没有想象中那么复杂。你可以把它理解为一种写给计算机看的“菜谱”只不过这份菜谱是用一系列命令组成的。当你需要重复执行某些繁琐的操作时比如批量重命名几百个文件、定时备份数据库、或者快速分析服务器日志手动一条条敲命令不仅效率低下还容易出错。这时Shell脚本的价值就凸显出来了——它能把你的操作意图固化下来一键执行准确无误。这篇文章就是为你准备的无论你是零基础的开发者还是对自动化感兴趣的运维新手。我们不打算从枯燥的语法手册讲起而是会带你从最经典的“Hello World”开始一步步构建起对Shell脚本的直观理解。然后我们会把重点放在那些真正能帮你解决实际问题的核心概念上变量、流程控制以及cut、sed、awk这些文本处理“神器”。最后我们会通过几个贴近实战的案例比如日志分析和批量文件处理让你亲手体验如何将零散的知识点串联起来完成一个真正有用的自动化任务。准备好了吗让我们开始这段从命令行到自动化大师的旅程。1. 迈出第一步你的第一个Shell脚本学习任何编程语言第一个程序几乎都是“Hello World”。这不仅仅是一个传统更是一个完整的、可验证的起点能帮你快速建立起“编写-运行-看到结果”的正向反馈循环。1.1 创建并运行“Hello World”打开你的终端Terminal跟着我一步步操作。首先我们创建一个专门存放脚本的目录并进入它mkdir -p ~/my_scripts cd ~/my_scripts现在让我们用文本编辑器创建一个名为hello.sh的文件。这里我推荐使用nano或vim如果你不熟悉用系统自带的文本编辑器如VS Code创建也可以。nano hello.sh在打开的文件中输入以下内容#!/bin/bash # 这是我的第一个Shell脚本 echo Hello, World! echo 当前时间是$(date)让我解释一下这几行代码第一行#!/bin/bash这被称为“shebang”或“hashbang”。它告诉系统应该使用哪个解释器来执行这个脚本。/bin/bash是最常用的Bash Shell的路径。这一行必须放在脚本的第一行。第二行以#开头这是注释。Shell会忽略#之后直到行尾的所有内容。写注释是个好习惯它能帮你和他人理解代码的意图。第三行echo Hello, World!echo是一个命令它的作用就是把后面的文本输出到屏幕上。第四行echo 当前时间是$(date)这里演示了一个小技巧。$(date)被称为“命令替换”。Shell会先执行date命令获取当前时间然后用它的输出结果替换掉$(date)这个部分最后再由echo输出。保存并退出编辑器在nano中是按CtrlX然后按Y确认再按回车。现在让这个脚本跑起来。有三种常见的方法方法一使用bash命令直接解释bash hello.sh这种方法最简单你不需要关心文件权限因为你是把脚本文件作为参数传给bash命令来执行的。方法二赋予执行权限后直接运行首先给脚本加上可执行x权限chmod x hello.sh然后通过指定路径来执行它./hello.sh这里的./表示“当前目录”。这种方法更符合脚本作为独立“程序”的定位。方法三将脚本放到系统路径中进阶如果你想让你的脚本像ls、cd这些系统命令一样在任何目录下直接输入名字就能运行可以把它移动到系统路径下比如/usr/local/bin/需要管理员权限或者添加到你的个人PATH环境变量中。注意如果你在Windows上使用Git Bash或WSL上述命令同样适用。确保你的脚本文件以LFUnix格式换行而不是CRLFWindows否则可能会遇到$‘\r‘: command not found的错误。大多数现代编辑器都可以设置换行符格式。运行成功后你应该会在终端看到类似这样的输出Hello, World! 当前时间是Mon Apr 8 10:30:00 CST 2024恭喜你已经成功创建并运行了第一个Shell脚本。这个简单的过程包含了脚本的核心要素解释器声明、注释、命令执行和输出。1.2 脚本的组成部分与执行原理一个典型的Shell脚本其结构可以归纳为以下几个部分部分说明是否必需Shebang (#!)行指定脚本解释器如#!/bin/bash、#!/bin/sh、#!/usr/bin/env python3强烈建议确保执行环境一致注释以#开头解释代码逻辑、作者、日期等非必需但强烈推荐变量定义存储数据供后续命令使用视需求而定主逻辑代码一系列命令和控制结构的组合核心必需函数定义将可复用的代码块封装起来非必需但能提升代码质量退出状态使用exit N明确脚本执行结果0成功非0失败非必需但是好习惯脚本的执行原理可以简单理解为当你运行./hello.sh时操作系统内核会读取文件的第一行。发现#!/bin/bash后内核会启动/bin/bash这个程序。然后将hello.sh这个文件作为参数传递给bash程序。bash程序逐行读取文件内容将其作为命令序列来解析和执行。理解了这个流程你就明白了为什么Shebang行如此重要——它决定了你的脚本在什么样的环境中运行。不同的Shell如bash、zsh、dash在语法和特性上可能有细微差别。2. 脚本的基石变量与参数传递如果说命令是脚本的“动词”那么变量就是脚本的“名词”。它用于存储信息让你的脚本变得灵活和可配置。2.1 变量的定义、使用与作用域在Bash中定义变量非常简单等号两边不能有空格。# 定义变量 my_nameShell Learner website_urlhttps://example.com count10 # 使用变量需要在变量名前加上美元符号 $ echo 你好$my_name echo 访问 $website_url 获取更多信息。 # 也可以将变量用花括号括起来常用于明确变量边界 echo 总计有 ${count}条记录。变量名可以由字母、数字和下划线组成但不能以数字开头。按照惯例普通变量使用小写环境变量和常量使用大写这样可以提高代码的可读性。变量有几个关键特性需要掌握默认类型为字符串即使你写了count10在Bash眼里count里存的也是字符串10。直接进行数学运算会出错。引号的使用当值中包含空格时必须使用引号。# 错误等号右边有空格会被解析为命令 greeting Hello World # 错误空格导致World被当作命令 greetingHello World # 正确使用引号包裹 greetingHello World单引号‘ ‘和双引号“ ”有区别双引号会解析变量和命令替换而单引号会将所有内容原样输出。nameAlice echo Hello, $name # 输出Hello, Alice echo ‘Hello, $name‘ # 输出Hello, $name变量的作用域默认情况下变量只在当前Shell进程中有效。如果你在一个脚本中定义变量然后在另一个脚本中是无法直接访问的。要使变量在子进程比如你从脚本中启动的另一个脚本中可用需要使用export命令将其“导出”为环境变量。# script_a.sh MY_VARThis is a secret export MY_VAR # 导出为环境变量 ./script_b.sh # 在子进程中运行另一个脚本 # script_b.sh echo In script_b, MY_VAR is: $MY_VAR # 可以访问到只读变量使用readonly定义的变量不能被修改或删除。readonly PI3.14159 PI3.14 # 这行会报错bash: PI: readonly variable unset PI # 这行也会报错2.2 特殊变量与参数传递Shell提供了一系列特殊的预定义变量它们在脚本交互中极其有用尤其是处理用户输入的参数。特殊变量含义典型用途$0当前脚本的文件名在日志中记录是哪个脚本在执行$1,$2,$3...传递给脚本的第1、2、3...个参数接收用户输入如./backup.sh /path/to/source /path/to/dest$#传递给脚本的参数个数检查用户是否提供了足够参数$所有参数列表每个参数作为独立字符串遍历所有参数$*所有参数列表但将所有参数视为一个整体字符串较少使用与$有细微差别$?上一个命令的退出状态码0表示成功判断上一条命令是否执行成功$$当前Shell进程的IDPID创建临时文件时用于生成唯一文件名让我们写一个脚本args_demo.sh来感受一下#!/bin/bash # args_demo.sh - 演示特殊变量的用法 echo “脚本名称: $0” echo “第一个参数: $1” echo “第二个参数: $2” echo “参数总个数: $#” echo “所有参数 (作为列表): $” echo “所有参数 (作为整体): $*” echo “上一个命令的退出码: $?” # 遍历所有参数 echo -e “\n逐个打印参数:” for arg in “$”; do echo “ - $arg” done # 检查参数个数 if [ $# -eq 0 ]; then echo “错误请至少提供一个参数。” exit 1 # 非0退出码通常表示错误 fi运行这个脚本并传递几个参数chmod x args_demo.sh ./args_demo.sh apple banana cherry date你会看到类似这样的输出脚本名称: ./args_demo.sh 第一个参数: apple 第二个参数: banana 参数总个数: 4 所有参数 (作为列表): apple banana cherry date 所有参数 (作为整体): apple banana cherry date 上一个命令的退出码: 0 逐个打印参数: - apple - banana - cherry - date$和$*的区别是一个经典面试题。在大多数情况下它们看起来一样。但当被双引号包裹时区别就出来了“$”相当于“$1” “$2” “$3” ...保持了每个参数的独立性而“$*”相当于“$1 $2 $3 ...”把所有参数合并成了一个字符串。在需要保留参数内空格或需要逐个处理参数时应优先使用“$”。2.3 命令替换与算术运算变量里不仅能存静态字符串还能存命令执行的结果这就是命令替换。有两种语法反引号和$()。现代脚本中推荐使用$()因为它更清晰且支持嵌套。# 获取当前日期和时间 current_time$(date “%Y-%m-%d %H:%M:%S”) echo “任务开始于$current_time” # 获取目录下的文件数量 file_count$(ls -l | grep “^-” | wc -l) echo “当前目录下有 $file_count 个普通文件。” # 嵌套示例获取上个月的第一天 first_day_of_last_month$(date -d “$(date %Y-%m-01) -1 month” “%Y-%m-%d”) echo “上个月的第一天是$first_day_of_last_month”Bash本身不擅长数学计算但我们可以通过几种方式进行算术运算使用$(( ))最常用、最推荐a10 b3 sum$((a b)) # 加法 diff$((a - b)) # 减法 prod$((a * b)) # 乘法注意不需要转义 quot$((a / b)) # 除法整数除法 mod$((a % b)) # 取模 pow$((a ** b)) # 乘方Bash 4.0 echo “和$sum, 差$diff, 积$prod, 商$quot, 余数$mod, 幂$pow”使用let命令let “c a * 2 b” echo “c $c”使用expr命令较老的方式# 注意运算符两边必须有空格 result$(expr $a $b) echo “expr 计算结果$result”对于浮点数运算Bash原生不支持需要借助外部工具如bc一个高精度计算器语言# 计算 10 / 3保留2位小数 echo “scale2; 10 / 3” | bc # 输出3.333. 控制流程让脚本学会判断与循环有了变量存储数据下一步就是让脚本能根据不同的情况做出决策或者重复执行某些操作。这就是流程控制。3.1 条件判断if 与 caseif语句是分支判断的核心。其基本结构如下if [ 条件测试 ]; then # 条件为真时执行的命令 elif [ 另一个条件测试 ]; then # 上一个条件为假但此条件为真时执行 else # 所有条件都为假时执行 fi方括号[ ]是一个命令实际上是test命令的别名用于进行条件测试。因此[和后面的条件、条件与]之间必须有空格。常见的测试条件包括字符串比较if [ “$str1” “$str2” ]; then echo “字符串相等”; fi if [ “$str1” ! “$str2” ]; then echo “字符串不等”; fi if [ -z “$str” ]; then echo “字符串为空”; fi # -z: 长度为零 if [ -n “$str” ]; then echo “字符串非空”; fi # -n: 长度非零整数比较使用字母操作符if [ $a -eq $b ]; then echo “等于”; fi # equal if [ $a -ne $b ]; then echo “不等于”; fi # not equal if [ $a -gt $b ]; then echo “大于”; fi # greater than if [ $a -lt $b ]; then echo “小于”; fi # less than if [ $a -ge $b ]; then echo “大于等于”; fi # greater or equal if [ $a -le $b ]; then echo “小于等于”; fi # less or equal文件测试if [ -f “file.txt” ]; then echo “是普通文件”; fi if [ -d “/some/path” ]; then echo “是目录”; fi if [ -e “/some/file” ]; then echo “文件或目录存在”; fi if [ -r “file.txt” ]; then echo “文件可读”; fi if [ -w “file.txt” ]; then echo “文件可写”; fi if [ -x “/bin/bash” ]; then echo “文件可执行”; fi逻辑组合# 逻辑与两个条件都满足 if [ $age -gt 18 ] [ “$country” “CN” ]; then echo “符合条件” fi # 也可以用 -a但 更通用 if [ $age -gt 18 -a “$country” “CN” ]; then ... # 逻辑或满足一个即可 if [ -f “backup.tar.gz” ] || [ -f “backup.zip” ]; then echo “找到备份文件” fi # 也可以用 -o if [ -f “backup.tar.gz” -o -f “backup.zip” ]; then ...一个实用的例子检查一个目录是否存在如果不存在则创建它。#!/bin/bash backup_dir“/var/backups/myapp” if [ ! -d “$backup_dir” ]; then echo “备份目录 $backup_dir 不存在正在创建...” mkdir -p “$backup_dir” if [ $? -eq 0 ]; then echo “目录创建成功。” else echo “目录创建失败” 2 # 2 表示输出到标准错误 exit 1 fi else echo “备份目录 $backup_dir 已存在。” ficase语句非常适合处理多分支选择特别是当条件是基于一个变量的精确匹配时它比一连串的if-elif更清晰。#!/bin/bash # case_demo.sh - 根据文件扩展名决定操作 filename“$1” extension“${filename##*.}” # 获取文件后缀名 case “$extension” in txt) echo “处理文本文件$filename” # 例如用 less 查看 less “$filename” ;; jpg|jpeg|png|gif) echo “处理图片文件$filename” # 例如用图片查看器打开 xdg-open “$filename” 2/dev/null ;; sh) echo “这是Shell脚本检查语法...” bash -n “$filename” echo “语法检查通过。” ;; *) echo “不支持的文件类型: .$extension” echo “支持的格式: txt, jpg, png, gif, sh” exit 1 ;; esaccase语句以case ... in开始以esaccase倒过来写结束。每个模式以)结束对应的代码块以;;结束。*是默认匹配项相当于if-else中的else。3.2 循环for 与 while循环让你能重复执行一段代码是自动化的核心。for循环通常用于遍历一个已知的列表。# 语法1遍历值列表 for fruit in apple banana orange; do echo “我喜欢吃 $fruit” done # 语法2遍历命令输出的结果非常常用 for file in *.txt; do echo “处理文件: $file” wc -l “$file” # 统计文件行数 done # 语法3C语言风格适用于数字序列 for ((i1; i5; i)); do echo “这是第 $i 次循环” done一个实战例子批量将当前目录下所有.jpg文件重命名为image_001.jpg这样的格式。#!/bin/bash count1 for img in *.jpg; do new_name$(printf “image_%03d.jpg” “$count”) # 格式化数字3位宽度不足补零 mv “$img” “$new_name” echo “重命名: $img - $new_name” ((count)) # 另一种算术自增写法 donewhile循环则用于当某个条件为真时持续执行循环。# 基本语法 while [ 条件 ]; do # 循环体 done一个典型应用是读取文件的每一行#!/bin/bash # 逐行读取配置文件 config_file“app.conf” line_num1 while IFS read -r line; do # IFS 防止行首尾空格被修剪-r 防止反斜杠转义 echo “行号 $line_num: $line” ((line_num)) done “$config_file” # 输入重定向将文件内容喂给while循环另一个常见模式是无限循环直到满足某个条件才跳出#!/bin/bash # 猜数字游戏 target$((RANDOM % 100 1)) # 生成1-100的随机数 guess0 attempts0 echo “猜数字游戏开始目标在1到100之间。” while [ $guess -ne $target ]; do read -p “请输入你的猜测: ” guess ((attempts)) if [ $guess -lt $target ]; then echo “太小了” elif [ $guess -gt $target ]; then echo “太大了” else echo “恭喜你猜对了你用了 $attempts 次尝试。” fi doneuntil循环与while逻辑相反和select循环创建简单的菜单也是有用的工具但使用频率稍低你可以根据需要查阅文档。3.3 用户交互read 命令要让脚本与用户互动read命令必不可少。它可以暂停脚本执行等待用户输入。#!/bin/bash # read_demo.sh # -p 指定提示符 read -p “请输入您的用户名: ” username # -s 静默模式输入不显示用于密码 read -sp “请输入您的密码: ” password echo # 换行因为-s不输出回车 # -t 设置超时时间秒 if read -t 5 -p “请在5秒内输入Y确认操作 (Y/N): ” confirm; then if [ “$confirm” “Y” ] || [ “$confirm” “y” ]; then echo “操作已确认。” else echo “操作已取消。” fi else echo “输入超时操作取消。” fi # -a 将输入读入数组 echo “请输入您喜欢的几种水果用空格隔开: ” read -a fruits echo “您喜欢的水果有: ” for fruit in “${fruits[]}”; do echo “ - $fruit” done4. 文本处理三剑客cut, sed, awkShell脚本的强大很大程度上源于其丰富的文本处理工具。cut、sed、awk被誉为“文本处理三剑客”是处理日志、配置文件、数据提取等任务的利器。4.1 cut精准的字段切割器cut命令擅长从结构化的文本行中提取特定字段比如CSV文件、以固定分隔符分隔的日志等。基本语法cut [选项] [文件]常用选项-d‘分隔符’指定字段分隔符默认是制表符。-f N指定要提取的第N个字段。可以用逗号指定多个-f 1,3或用连字符指定范围-f 2-5。-c N按字符位置切割用于固定宽度的文本。假设我们有一个员工信息的文件employees.csvid,name,department,salary 101,Alice,Engineering,85000 102,Bob,Marketing,72000 103,Charlie,Sales,68000# 1. 提取姓名列第2列 cut -d‘,‘ -f2 employees.csv # 输出 # name # Alice # Bob # Charlie # 2. 提取ID和部门列第1和第3列 cut -d‘,‘ -f1,3 employees.csv # 输出 # id,department # 101,Engineering # 102,Marketing # 103,Sales # 3. 结合其他命令找出薪水最高的员工 # 先去掉标题行然后按薪水排序取最后一行再提取名字 tail -n 2 employees.csv | sort -t‘,‘ -k4 -nr | head -1 | cut -d‘,‘ -f2 # 输出Alice注意cut的缺点是只能使用单个字符作为分隔符且不能处理字段中包含分隔符的复杂情况如带引号的CSV。对于更复杂的数据awk是更好的选择。4.2 sed流编辑器批量文本替换之王sed是一个“非交互式”的流编辑器。它按行读取输入根据你提供的编辑命令进行修改如替换、删除、插入然后将结果输出。默认情况下sed不会修改原文件除非你使用-i选项。基本语法sed [选项] ‘命令‘ [输入文件]常用命令s/查找模式/替换内容/标志替换命令最常用。d删除匹配的行。p打印匹配的行通常与-n选项一起使用只打印被处理的行。a\文本在匹配行之后追加一行。i\文本在匹配行之前插入一行。让我们用一个文件demo.txt来演示Hello world. This is a test file. Hello again. Goodbye world.# 1. 将每一行的第一个“Hello”替换为“Hi” sed ‘s/Hello/Hi/‘ demo.txt # 输出 # Hi world. # This is a test file. # Hi again. # Goodbye world. # 2. 将所有的“world”替换为“universe”g标志表示全局替换 sed ‘s/world/universe/g‘ demo.txt # 输出 # Hello universe. # This is a test file. # Hello again. # Goodbye universe. # 3. 删除包含“test”的行 sed ‘/test/d‘ demo.txt # 输出 # Hello world. # Hello again. # Goodbye world. # 4. 在包含“again”的行之后追加一行“Appended line.” sed ‘/again/a\Appended line.‘ demo.txt # 输出 # Hello world. # This is a test file. # Hello again. # Appended line. # Goodbye world. # 5. 原地修改文件危险操作前建议备份 sed -i.bak ‘s/Goodbye/Farewell/‘ demo.txt # 修改demo.txt并备份原文件为demo.txt.baksed还支持使用正则表达式进行更强大的模式匹配。例如删除所有空行sed ‘/^$/d‘ file.txt。将多个命令组合在一起使用-e选项sed -e ‘s/foo/bar/‘ -e ‘/baz/d‘ file.txt。4.3 awk编程式的文本分析工具awk不仅仅是一个命令它是一门拥有自己语法类似C语言的编程语言专门为文本处理设计。它的核心思想是将输入文本视为由记录默认是行和字段默认由空格分隔组成的表格然后你可以编写程序来操作这些数据。基本语法awk ‘模式 {动作}‘ [文件]模式决定对哪些记录行执行动作。可以是正则表达式、条件表达式或者特殊模式如BEGIN在处理任何行之前执行和END处理完所有行之后执行。动作在花括号{}内是一系列用分号分隔的语句描述对匹配行要执行的操作。内置变量NR当前处理的行号Number of Records。NF当前行的字段数量Number of Fields。$0整行内容。$1,$2, ...第1个第2个...字段的内容。让我们用employees.csv文件来展示awk的强大# 1. 打印所有行 awk ‘{print}‘ employees.csv # 或 awk ‘{print $0}‘ employees.csv # 2. 打印姓名和薪水第2和第4列 awk -F‘,‘ ‘{print $2, $4}‘ employees.csv # -F‘,‘ 指定逗号为字段分隔符 # 输出 # name salary # Alice 85000 # Bob 72000 # Charlie 68000 # 3. 只打印Engineering部门的员工 awk -F‘,‘ ‘$3 “Engineering“ {print $2}‘ employees.csv # 输出Alice # 4. 计算平均薪水跳过标题行 awk -F‘,‘ ‘NR1 {sum$4; count} END {print “平均薪水:“, sum/count}‘ employees.csv # 输出平均薪水: 75000 # 5. 格式化输出 awk -F‘,‘ ‘BEGIN {printf “%-10s %-15s %s\n“, “Name“, “Department“, “Salary“} NR1 {printf “%-10s %-15s $%‘\‘‘d\n“, $2, $3, $4}‘ employees.csv # 输出 # Name Department Salary # Alice Engineering $85,000 # Bob Marketing $72,000 # Charlie Sales $68,000awk的功能远不止于此它还支持数组、循环、条件语句、自定义函数等。对于复杂的文本报表生成和数据清洗任务awk往往是最终解决方案。5. 实战案例从脚本到自动化工具理论学得再多不如动手实践。下面我们通过两个完整的实战案例将前面学到的所有知识串联起来打造真正有用的自动化脚本。5.1 案例一服务器日志分析与监控告警假设你有一台Web服务器每天都会生成访问日志access.log格式如下简化版192.168.1.1 - - [08/Apr/2024:10:15:32 0800] “GET /index.html HTTP/1.1“ 200 1234 192.168.1.2 - - [08/Apr/2024:10:15:33 0800] “GET /api/data HTTP/1.1“ 404 567 192.168.1.1 - - [08/Apr/2024:10:15:35 0800] “POST /login HTTP/1.1“ 200 2345 10.0.0.5 - - [08/Apr/2024:10:16:01 0800] “GET /index.html HTTP/1.1“ 200 1234你的任务是编写一个脚本log_analyzer.sh它能分析最近一小时的日志实现以下功能统计总请求数。统计状态码分布如200、404、500的数量。找出访问最频繁的5个IP地址。如果404错误超过一定阈值发送告警这里用打印消息模拟。#!/bin/bash # log_analyzer.sh - 简易日志分析监控脚本 LOG_FILE“/var/log/nginx/access.log“ ALERT_THRESHOLD10 # 404错误告警阈值 REPORT_FILE“/tmp/log_report_$(date %Y%m%d_%H%M%S).txt“ # 函数发送告警模拟 send_alert() { local message“$1“ echo “[ALERT] $message“ 2 # 在实际环境中这里可以替换为发送邮件、Slack消息或调用告警API的命令 # 例如mail -s “日志告警“ adminexample.com “$message“ } echo “ 日志分析报告 ($(date)) “ “$REPORT_FILE“ echo “分析文件: $LOG_FILE“ “$REPORT_FILE“ echo “分析时间范围: 最近1小时“ “$REPORT_FILE“ echo “----------------------------------------“ “$REPORT_FILE“ # 1. 统计总请求数 (假设日志时间格式规范用grep过滤最近1小时) recent_logs$(grep “$(date -d ‘1 hour ago‘ ‘[%d/%b/%Y:%H:‘)“ “$LOG_FILE“) total_requests$(echo “$recent_logs“ | wc -l) echo “总请求数: $total_requests“ | tee -a “$REPORT_FILE“ # 2. 统计状态码分布第9个字段通常是状态码 echo -e “\n状态码分布:“ | tee -a “$REPORT_FILE“ echo “$recent_logs“ | awk ‘{status$9; codes[status]} END {for (c in codes) printf “ %s: %d 次\n“, c, codes[c]}‘ | sort -rn -k2 | tee -a “$REPORT_FILE“ # 3. 找出访问最频繁的5个IP第1个字段是IP echo -e “\n访问最频繁的5个IP:“ | tee -a “$REPORT_FILE“ echo “$recent_logs“ | awk ‘{ip$1; count[ip]} END {for (i in count) printf “ %-15s: %d 次\n“, i, count[i]}‘ | sort -rn -k3 | head -5 | tee -a “$REPORT_FILE“ # 4. 检查404错误数量 not_found_count$(echo “$recent_logs“ | awk ‘$9404 {count} END {print count0}‘) # 0防止空值 echo -e “\n404错误数量: $not_found_count“ | tee -a “$REPORT_FILE“ if [ “$not_found_count“ -gt “$ALERT_THRESHOLD“ ]; then alert_msg“最近1小时内404错误数 ($not_found_count) 超过阈值 ($ALERT_THRESHOLD)请检查“ echo “$alert_msg“ “$REPORT_FILE“ send_alert “$alert_msg“ fi echo -e “\n详细报告已保存至: $REPORT_FILE“你可以通过crontab设置这个脚本每小时自动运行一次实现持续的日志监控。5.2 案例二智能批量文件整理器你的下载目录~/Downloads总是乱糟糟的各种图片、文档、压缩包混在一起。让我们写一个脚本file_organizer.sh来自动整理。#!/bin/bash # file_organizer.sh - 智能文件整理脚本 TARGET_DIR“${1:-$HOME/Downloads}“ # 支持指定目录默认为~/Downloads BACKUP_DIR“$HOME/Downloads_backup_$(date %Y%m%d)“ LOG_FILE“$HOME/file_organizer.log“ # 定义文件类型与目标目录的映射 declare -A FILE_TYPE_MAP( [“jpg|jpeg|png|gif|bmp“]“Images“ [“pdf“]“Documents/PDF“ [“doc|docx“]“Documents/Word“ [“xls|xlsx“]“Documents/Excel“ [“ppt|pptx“]“Documents/PPT“ [“zip|tar|gz|bz2|7z“]“Archives“ [“mp4|avi|mkv|mov“]“Videos“ [“mp3|wav|flac“]“Music“ [“sh|py|js|java“]“Scripts“ ) echo “$(date) - 开始整理目录: $TARGET_DIR“ “$LOG_FILE“ # 创建备份目录安全第一 if [ ! -d “$BACKUP_DIR“ ]; then mkdir -p “$BACKUP_DIR“ echo “创建备份目录: $BACKUP_DIR“ “$LOG_FILE“ fi # 遍历目标目录下的所有文件不包括目录本身 find “$TARGET_DIR“ -maxdepth 1 -type f | while read -r file; do filename$(basename “$file“) extension“${filename##*.}“ # 提取扩展名 extension_lower$(echo “$extension“ | tr ‘[:upper:]‘ ‘[:lower:]‘) # 转为小写 matched0 for pattern in “${!FILE_TYPE_MAP[]}“; do # 遍历映射的键模式 if [[ “$extension_lower“ ~ ^($pattern)$ ]]; then # 使用正则匹配 target_subdir“${FILE_TYPE_MAP[$pattern]}“ target_path“$TARGET_DIR/$target_subdir“ mkdir -p “$target_path“ # 创建分类目录如果不存在的话 # 检查目标文件是否已存在避免覆盖 if [ -e “$target_path/$filename“ ]; then # 如果存在在文件名后添加时间戳 new_filename“${filename%.*}_$(date %H%M%S).$extension“ mv -v “$file“ “$target_path/$new_filename“ “$LOG_FILE“ 21 else mv -v “$file“ “$target_path/“ “$LOG_FILE“ 21 fi echo “移动: $filename - $target_subdir/“ “$LOG_FILE“ matched1 break fi done # 如果未匹配任何已知类型移动到“Others“目录 if [ $matched -eq 0 ]; then others_dir“$TARGET_DIR/Others“ mkdir -p “$others_dir“ mv -v “$file“ “$others_dir/“ “$LOG_FILE“ 21 echo “移动: $filename - Others/“ “$LOG_FILE“ fi done # 备份原始目录可选根据需求开启 # cp -r “$TARGET_DIR“/* “$BACKUP_DIR/“ 2/dev/null echo “$(date) - 文件整理完成。“ “$LOG_FILE“ echo “整理完成日志详见: $LOG_FILE“ echo “文件已按类型分类到 $TARGET_DIR 下的子目录中。“这个脚本展示了Shell脚本的多个高级特性关联数组declare -A、循环遍历数组键、正则表达式匹配~、命令替换、条件判断以及文件操作。你可以把它加入到你的登录脚本或者设置一个每周定时任务让电脑自动保持整洁。5.3 脚本的健壮性与最佳实践写完能跑的脚本只是第一步写出健壮、可维护的脚本才是目标。这里分享几个我踩过坑后总结的经验总是检查变量和参数对于用户输入或可能为空的变量使用${变量:-默认值}语法提供默认值或在使用前检查。backup_dir“${1:-/tmp/backup}“ # 如果$1为空则使用/tmp/backup if [ -z “$important_var“ ]; then echo “错误重要变量未设置“ 2 exit 1 fi错误处理使用set -e让脚本在遇到任何错误时立即退出使用set -u遇到未定义变量时报错。使用trap命令捕获信号进行清理工作。#!/bin/bash set -euo pipefail # 严格模式遇错退出未定义变量报错管道中任意命令失败则整个管道失败 trap “echo ‘脚本被中断执行清理...‘; rm -f /tmp/temp_$$.tmp; exit 1“ INT TERM使用函数模块化将重复的代码块封装成函数让主逻辑更清晰。log_message() { local level“$1“ local msg“$2“ echo “[$(date ‘%Y-%m-%d %H:%M:%S‘)] [$level] $msg“ | tee -a “$LOG_FILE“ } # 使用 log_message “INFO“ “脚本开始执行“ log_message “ERROR“ “文件未找到“详细的日志和输出重要的操作都记录到日志文件方便事后排查。使用2将错误信息输出到标准错误流。代码风格统一缩进建议两个空格给函数和复杂逻辑写注释使用有意义的变量名。把这些习惯融入到你的脚本编写中你会发现你的Shell脚本不仅能用而且可靠、易懂甚至几个月后自己回头看也能轻松维护。Shell脚本的世界很大从简单的文件操作到复杂的系统管理、CI/CD流水线它都是不可或缺的利器。掌握这些基础后多读、多写、多思考你会越来越享受用脚本将繁琐工作自动化的乐趣。