尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Shell脚本编程实战:从零掌握自动化运维与文本处理

Shell脚本编程实战:从零掌握自动化运维与文本处理 在日常运维和开发工作中你是否经常需要重复执行一系列命令比如批量重命名文件、定时备份数据、监控系统状态、自动化部署应用。手动操作不仅效率低下还容易出错。Shell脚本正是解决这类问题的利器。它就像一把瑞士军刀能将零散的Linux命令组合成自动化的工作流极大地提升工作效率。本文将从零开始手把手带你掌握Shell脚本编程的核心技能。无论你是刚接触Linux的新手还是希望系统化提升自动化能力的运维工程师都能通过这篇实战指南学会如何编写健壮、高效的Shell脚本。我们将从最基础的变量、循环、函数讲起一直深入到文本处理“三剑客”grep、sed、awk的实战应用并分享编写脚本时那些必须注意的“坑”和最佳实践。1. Shell脚本基础概念、环境与第一个脚本1.1 什么是Shell脚本简单来说Shell脚本就是一个包含了一系列Shell命令的文本文件。Shell壳是用户与Linux内核Kernel之间的桥梁它接收用户输入的命令解释后交给内核执行并将结果返回给用户。我们常用的BashBourne Again SHell就是最流行的一种Shell。Shell脚本的本质是自动化。它将需要手动输入的命令预先写好保存为文件然后一次性执行。这特别适合处理重复性、流程化的任务。常见应用场景系统管理自动化用户创建、软件安装、服务启停。文件操作批量重命名、查找并清理日志、备份重要数据。监控告警定期检查系统负载、磁盘空间、服务状态异常时发送通知。数据处理对日志文件进行提取、过滤、统计和格式化。CI/CD在自动化部署流水线中执行编译、测试、打包、发布等步骤。1.2 环境准备与脚本创建操作系统任何主流的Linux发行版如Ubuntu, CentOS, Rocky Linux或macOS均可。Windows用户可以通过WSLWindows Subsystem for Linux获得完整的Linux Shell环境。Shell版本本文以最常用的Bash为例。你可以通过以下命令查看当前Shell及其版本echo $SHELL bash --version输出可能类似GNU bash version 5.1.16(1)-release...。确保你的Bash版本在4.x以上以获得更好的功能支持。编辑器你可以使用任何文本编辑器如vim、nano、VS Code等。创建第一个脚本Hello World打开终端创建一个新文件vim hello.sh输入以下内容#!/bin/bash # 这是一个注释我的第一个Shell脚本 echo Hello, World! echo 当前时间是$(date)第一行#!/bin/bash称为shebang它告诉系统使用哪个解释器来执行这个脚本。这是Shell脚本的标准开头必不可少。#开头的行表示注释用于解释代码不会被执。echo命令用于输出文本到屏幕。$(date)这是命令替换的语法会先执行date命令然后用其输出结果替换$(date)这部分。保存并退出编辑器在vim中按Esc然后输入:wq并回车。1.3 脚本的执行与权限刚创建的脚本文件默认没有执行权限。直接运行会报错Permission denied。方法一使用bash解释器执行无需执行权限bash hello.sh或sh hello.sh这种方式显式指定了解释器脚本文件本身不需要有执行权限。方法二赋予执行权限后直接运行推荐添加执行权限chmod x hello.shchmod命令用于修改文件权限x表示给所有用户增加执行(execute)权限。执行脚本./hello.sh注意./表示当前目录。Linux系统出于安全考虑默认不会在当前目录搜索可执行文件所以必须加上./来指明路径。执行后你将看到类似输出Hello, World! 当前时间是2023年 10月 27日 星期五 15:30:00 CST2. Shell脚本核心语法变量与参数变量是存储数据的容器是脚本编程的基石。2.1 变量的定义与使用定义变量变量名变量值。等号两边不能有空格nameCSDN version1.0变量名由字母、数字、下划线组成且不能以数字开头。默认情况下变量值都是字符串。即使你写了version1.0在Bash中它也被视为字符串“1.0”直到进行算术运算。使用变量需要在变量名前加上美元符号$。echo $name echo ${name}${name}是更规范的写法它能明确变量名的边界避免歧义。例如skillShell echo I am good at ${skill}Script # 输出I am good at ShellScript echo I am good at $skillScript # 尝试输出变量$skillScript但其未定义输出为空或报错只读变量使用readonly声明防止被修改。readonly pi3.14 pi3.1415 # 执行此行会报错bash: pi: readonly variable删除变量使用unset命令但无法删除只读变量。unset name echo $name # 输出为空2.2 特殊变量与脚本参数Shell提供了一系列特殊的预定义变量用于获取脚本运行时的上下文信息。变量含义$0当前脚本的文件名。$1,$2,$3...脚本的第1、2、3...个参数。$#传递给脚本的参数个数。$*以一个单字符串显示所有参数。如$*用括起来的情况、以$1 $2 … $n的形式输出所有参数。$与$*类似但在被双引号包围时每个参数仍被单独对待。如$用括起来的情况、以$1 $2 … $n的形式输出所有参数。这是处理参数列表更安全的方式。$?上一个命令的退出状态。0表示成功非0表示失败。$$当前Shell进程的IDPID。$!最后一个后台运行的进程的PID。实战示例创建带参数的脚本args_demo.sh#!/bin/bash # args_demo.sh echo 脚本名称$0 echo 第一个参数$1 echo 第二个参数$2 echo 参数总个数$# echo 所有参数作为整体$* echo 所有参数逐个列出 for arg in $; do echo - $arg done运行并观察结果chmod x args_demo.sh ./args_demo.sh arg1 arg2 arg3输出脚本名称./args_demo.sh 第一个参数arg1 第二个参数arg2 参数总个数3 所有参数作为整体arg1 arg2 arg3 所有参数逐个列表 - arg1 - arg2 - arg32.3 变量替换与默认值在处理可能未定义的变量时设置默认值是一种好习惯。#!/bin/bash # 假设这个变量可能来自外部配置也可能没有设置 custom_path${MY_CUSTOM_PATH:-/home/user/default} # 如果MY_CUSTOM_PATH未设置或为空则使用默认值 echo 使用的路径是$custom_path # 另一种形式如果变量为空则将其设置为默认值并赋值给原变量 read_input${1:-} # 获取第一个参数如果没有则为空 read_input${read_input:-guest} # 如果read_input为空则设置为‘guest’ echo 输入是$read_input3. 流程控制条件判断与循环流程控制让脚本具备逻辑判断和重复执行的能力。3.1 条件判断if语句if语句的基本结构if [ condition ]; then # 条件为真时执行的命令 elif [ another_condition ]; then # 另一个条件为真时执行的命令 else # 所有条件都为假时执行的命令 fi关键点[ condition ]中的方括号是test命令的简写方括号内两侧必须有空格。常用条件判断运算符字符串比较或等于!不等于-z字符串长度为0空则为真-n字符串长度非0则为真整数比较使用-eq,-ne等不要用、它们在[ ]中有特殊含义-eq等于-ne不等于-gt大于-lt小于-ge大于等于-le小于等于文件测试-e文件/目录存在-f存在且为普通文件-d存在且为目录-r可读-w可写-x可执行逻辑运算符!非-a与 (and)-o或 (or)示例检查文件是否存在并判断类型#!/bin/bash # file_check.sh file_path$1 if [ -z $file_path ]; then echo 错误请提供一个文件路径作为参数。 exit 1 # 非0退出码表示脚本执行失败 fi if [ -e $file_path ]; then echo 路径 $file_path 存在。 if [ -f $file_path ]; then echo 它是一个普通文件。 elif [ -d $file_path ]; then echo 它是一个目录。 else echo 它是其他类型的文件。 fi else echo 路径 $file_path 不存在。 fi运行./file_check.sh /etc/passwd3.2 循环for, while, until1. for循环遍历一个列表或命令的输出。#!/bin/bash # for_demo.sh # 遍历值列表 echo 遍历列表 for fruit in apple banana orange; do echo 水果$fruit done # 遍历命令输出例如当前目录下的.txt文件 echo -e \n遍历当前目录下的.txt文件 for file in *.txt; do if [ -f $file ]; then echo 找到文件$file # 可以在这里对每个文件进行操作比如 wc -l $file fi done # C语言风格的for循环用于数字序列 echo -e \n数字序列 for ((i1; i5; i)); do echo 数字$i done2. while循环当条件为真时持续循环。#!/bin/bash # while_demo.sh count1 while [ $count -le 5 ]; do echo 循环次数$count # count$((count 1)) # 方法一算术运算 ((count)) # 方法二使用双括号进行算术运算更简洁 done # 从文件中逐行读取内容经典用法 echo -e \n读取文件内容 while IFS read -r line; do echo 行内容$line done /etc/hostname # 将文件内容重定向给while循环IFS用于防止行首尾的空白字符被修剪-r防止反斜杠转义。3. until循环与while相反当条件为假时持续循环直到条件为真。#!/bin/bash # until_demo.sh num10 until [ $num -le 0 ]; do echo 倒计时$num ((num--)) sleep 1 # 等待1秒 done echo 时间到3.3 循环控制break与continuebreak立即终止整个循环。continue跳过本次循环的剩余语句直接进入下一次循环。#!/bin/bash # loop_control.sh for i in {1..10}; do if [ $i -eq 3 ]; then continue # 跳过数字3 fi if [ $i -eq 8 ]; then break # 当i等于8时终止循环 fi echo 当前数字$i done echo 循环结束。输出当前数字1 当前数字2 当前数字4 当前数字5 当前数字6 当前数字7 循环结束。4. 函数模块化与代码复用函数将一段代码逻辑封装起来可以多次调用使脚本结构更清晰。4.1 函数的定义与调用定义语法function function_name { # 函数体 commands... } # 或者更简洁推荐 function_name() { # 函数体 commands... }调用语法直接写函数名无需括号。function_name示例一个简单的问候函数#!/bin/bash # function_demo.sh # 定义函数 greet() { local name$1 # 使用local声明局部变量避免污染全局作用域 local hour$(date %H) local greeting if [ $hour -lt 12 ]; then greeting早上好 elif [ $hour -lt 18 ]; then greeting下午好 else greeting晚上好 fi echo $greeting, $name! } # 调用函数 greet 张三 greet 李四4.2 函数参数与返回值参数函数内部使用$1,$2...来获取传入的参数。$#、$等特殊变量在函数内也适用但作用域仅限于该函数。返回值Shell函数通过return语句返回一个退出状态码0-2550表示成功。真正的“数据”返回需要通过echo输出然后由调用者用命令替换$(...)捕获。示例计算两数之和的函数#!/bin/bash # function_return.sh add() { local num1$1 local num2$2 local sum$((num1 num2)) echo $sum # 输出结果 return 0 # 返回成功状态 } # 调用函数并捕获其输出 result$(add 10 20) exit_status$? # 获取函数return的状态码 echo 加法结果$result echo 函数退出状态$exit_status5. 文本处理“三剑客”grep, sed, awk在Shell脚本中处理文本文件是家常便饭。grep、sed、awk是三个功能强大且必不可少的文本处理工具合称“三剑客”。5.1 grep强大的文本搜索工具grep用于在文件或标准输入中搜索匹配指定模式的行。基本语法grep [选项] ‘模式’ 文件...常用选项-i忽略大小写。-v反向选择显示不匹配的行。-n显示匹配行的行号。-c只显示匹配的行数。-r或-R递归搜索目录下的所有文件。-E使用扩展正则表达式等同于egrep。-l只列出包含匹配项的文件名。示例# 在/etc/passwd中查找包含‘root’的行 grep root /etc/passwd # 忽略大小写查找‘error’ grep -i error /var/log/syslog # 使用正则表达式查找以‘ssh’开头的行 grep ^ssh /etc/services # 递归查找当前目录下所有.py文件中包含‘import os’的行并显示行号 grep -rn import os . --include*.py5.2 sed流编辑器用于文本替换、删除、插入等sed以行为单位进行处理可以将数据行进行替换、删除、新增、选取等操作。基本语法sed [选项] ‘命令’ 文件...常用命令s/pattern/replacement/flags替换。s表示替换pattern是要匹配的模式replacement是替换后的文本。flagsg表示全局替换一行中所有匹配数字表示替换第N个匹配p表示打印该行。d删除行。p打印行通常与-n选项一起使用只打印被处理的行。常用选项-n安静模式只显示被sed处理过的行。-i直接修改文件内容危险操作务必先备份或测试。-i.bak可以在修改前备份原文件。示例# 将文件中的‘apple’替换为‘orange’只替换每行第一个 sed s/apple/orange/ fruit.txt # 全局替换一行中所有‘apple’ sed s/apple/orange/g fruit.txt # 删除包含‘#’注释符号的行 sed /^#/d config.conf # 删除第2到第5行 sed 2,5d data.txt # 在每一行行首添加‘INFO: ’ sed s/^/INFO: / log.txt # 直接修改文件备份原文件为log.txt.bak sed -i.bak s/old/new/g log.txt5.3 awk强大的文本分析工具支持编程awk不仅仅是一个命令更是一门编程语言。它擅长处理结构化文本如表格数据、日志按字段处理。基本语法awk ‘模式 {动作}’ 文件...模式用于筛选行如/error/、$1 100。省略则对所有行执行动作。动作对匹配的行执行的操作如print,printf, 计算。默认以空格或制表符作为字段分隔符每行的字段依次为$1,$2...$0代表整行。NF代表字段数NR代表当前行号。示例# 打印/etc/passwd文件的第一列用户名和第三列用户ID awk -F: {print $1, $3} /etc/passwd # -F: 指定冒号为字段分隔符 # 打印文件行号及内容 awk {print NR, $0} data.txt # 统计文件行数、单词数、字符数模拟wc命令 awk BEGIN {lines0; words0; chars0} {lines; wordsNF; charslength($0)1} END {print Lines:, lines, Words:, words, Chars:, chars} data.txt # 更简洁的写法 awk {lines; wordsNF; charslength($0)1} END {print Lines:, lines, Words:, words, Chars:, chars} data.txt # 条件处理打印第二列大于50的行 awk $2 50 {print $0} score.txt三剑客组合使用管道|将它们连接起来威力巨大。# 查找包含‘ERROR’的日志行提取时间戳和错误信息并统计每种错误出现的次数 grep ERROR app.log | awk {print $1, $2, $3, $5} | sort | uniq -c | sort -nr6. 实战案例自动化日志分析与备份脚本现在我们将前面所学的知识综合起来编写一个实用的脚本。这个脚本将完成以下任务分析指定目录下的日志文件统计错误ERROR和警告WARN的数量。将超过7天的旧日志文件打包压缩并移动到备份目录。清理原始目录中已备份的旧日志文件。生成一份简单的报告。脚本文件log_manager.sh#!/bin/bash # log_manager.sh - 日志分析与备份脚本 # 用法./log_manager.sh 日志目录 [备份目录] # 配置部分 LOG_DIR${1:-/var/log/myapp} # 第一个参数为日志目录默认为/var/log/myapp BACKUP_DIR${2:-/backup/logs} # 第二个参数为备份目录默认为/backup/logs RETENTION_DAYS7 # 保留天数 REPORT_FILE/tmp/log_report_$(date %Y%m%d_%H%M%S).txt # 报告文件 # 函数定义 # 检查目录是否存在不存在则尝试创建 check_dir() { local dir$1 if [ ! -d $dir ]; then echo 目录 $dir 不存在尝试创建... mkdir -p $dir || { echo 错误无法创建目录 $dir exit 1 } fi } # 分析日志统计ERROR和WARN analyze_logs() { local log_dir$1 local report$2 echo 日志分析报告 $(date) $report echo 分析目录$log_dir $report echo ---------------------------------------- $report error_count0 warn_count0 # 遍历.log文件 for log_file in $log_dir/*.log; do if [ -f $log_file ]; then echo 正在分析$(basename $log_file) $report errors_in_file$(grep -c ERROR $log_file) warns_in_file$(grep -c WARN $log_file) echo ERROR: $errors_in_file $report echo WARN : $warns_in_file $report echo $report ((error_count errors_in_file)) ((warn_count warns_in_file)) fi done echo 统计汇总 $report echo 总ERROR数$error_count $report echo 总WARN 数$warn_count $report echo 报告已生成$report } # 备份旧日志 backup_old_logs() { local log_dir$1 local backup_dir$2 local days$3 local report$4 echo -e \n 开始备份超过${days}天的日志 $report # 使用find查找超过指定天数的.log文件 # -mtime $days 表示修改时间在$days天以前 # -exec 对找到的文件执行后续命令 find $log_dir -name *.log -mtime $days -print0 | while IFS read -r -d $\0 old_log; do if [ -f $old_log ]; then log_name$(basename $old_log) backup_file$backup_dir/${log_name}.$(date %Y%m%d).tar.gz echo 备份: $log_name - $(basename $backup_file) $report # 打包压缩单个文件到备份目录 tar -czf $backup_file -C $log_dir $log_name 2/dev/null if [ $? -eq 0 ]; then echo 备份成功。 $report # 备份成功后删除原文件谨慎操作生产环境建议先注释掉rm测试无误后再启用 # rm $old_log echo 模拟已标记删除原文件$log_name $report else echo 备份失败 $report fi fi done } # 主程序 main() { echo 日志管理脚本启动... echo 日志目录$LOG_DIR echo 备份目录$BACKUP_DIR # 1. 检查目录 check_dir $LOG_DIR check_dir $BACKUP_DIR # 2. 分析日志 analyze_logs $LOG_DIR $REPORT_FILE # 3. 备份旧日志 backup_old_logs $LOG_DIR $BACKUP_DIR $RETENTION_DAYS $REPORT_FILE echo -e \n所有操作完成。详细报告见$REPORT_FILE # 打印报告尾部 tail -20 $REPORT_FILE } # 执行主函数 main运行与测试创建测试日志目录和文件mkdir -p /tmp/test_logs echo 2023-10-25 INFO Application started /tmp/test_logs/app1.log echo 2023-10-26 ERROR Failed to connect to DB /tmp/test_logs/app1.log echo 2023-10-27 WARN Disk usage above 80% /tmp/test_logs/app1.log echo 2023-10-20 INFO Old log entry /tmp/test_logs/app2.log给脚本添加执行权限并运行chmod x log_manager.sh ./log_manager.sh /tmp/test_logs /tmp/log_backup观察终端输出和/tmp/log_report_*.txt报告文件的内容。脚本要点解析参数处理使用${1:-default}语法提供默认值。函数化将不同功能模块封装成函数提高可读性和可维护性。健壮性使用check_dir函数检查并创建目录使用||处理命令失败。文件查找使用find命令的-mtime选项按时间查找文件-print0与read -d $\0组合安全处理含空格的文件名。模拟操作脚本中删除原文件的rm命令被注释掉了这是非常重要的安全实践。在实际运行前务必先测试备份功能是否正常确认无误后再取消注释。报告生成将关键操作和结果输出到报告文件便于追溯。7. Shell脚本编程常见“坑”与最佳实践编写Shell脚本时一些细节问题可能导致脚本行为异常甚至危险。以下是一些高频“坑”和规避建议。7.1 常见“坑”点变量引用未加引号# 错误示范如果文件名包含空格循环会出错 for file in $(ls *.txt); do echo $file done # 正确做法使用引号或glob for file in *.txt; do if [ -f $file ]; then # 变量引用一定要加双引号 echo $file fi done数字比较误用符号在[ ]测试中应使用-eq,-gt等而非,。# 错误 if [ $a $b ]; then ... # 正确 if [ $a -gt $b ]; then ... # 或者在双括号(( ))中可以使用C语言风格的运算符 if (( a b )); then ...命令执行成功判断不严谨仅用$?判断有时不够。# 不严谨 rm important_file if [ $? -eq 0 ]; then echo 删除成功; fi # 更推荐的方式直接if判断命令 if rm important_file; then echo 删除成功 else echo 删除失败错误码$? fi脚本未指定解释器忘记写#!/bin/bash脚本在不同Shell环境下可能行为不一致。路径中有空格或特殊字符总是用双引号包裹变量。# 危险 rm $MY_FILE # 安全 rm $MY_FILE使用未初始化的变量导致意外行为。使用set -u可以让脚本在遇到未定义变量时立即报错退出。# 在脚本开头加入 set -u # 或者使用默认值 echo ${UNDEFINED_VAR:-默认值}7.2 最佳实践与工程建议脚本开头设置安全选项#!/bin/bash set -euo pipefail # -e: 任何命令失败返回值非0则脚本立即退出。 # -u: 使用未定义的变量时报错。 # -o pipefail: 管道中任何一个命令失败整个管道返回值就为失败。这是编写健壮脚本的“黄金法则”。添加详细的注释和帮助信息在脚本开头说明用途、参数、作者、版本。#!/bin/bash # 脚本名称log_manager.sh # 用途分析并备份指定目录的日志文件 # 作者YourName # 版本1.0 # 用法./log_manager.sh 日志目录 [备份目录] # 示例./log_manager.sh /var/log/app /backup/logs使用函数和局部变量将功能模块化使用local关键字声明函数内变量避免全局污染。输入验证对用户输入或外部参数进行合法性检查。if [ $# -lt 1 ]; then echo 错误缺少参数。 echo 用法$0 目录 exit 1 fi if [ ! -d $1 ]; then echo 错误$1 不是一个有效的目录。 exit 1 fi谨慎处理文件删除和修改对rm、mv、重定向等操作尤其是带有通配符*的要格外小心。可以先echo出要操作的命令确认无误后再执行。对于重要数据备份是第一要务。记录日志重要的操作和错误信息不仅输出到屏幕也应记录到文件或系统日志使用logger命令。log() { local level$1 local msg$2 echo [$(date %Y-%m-%d %H:%M:%S)] [$level] $msg | tee -a $LOG_FILE } log INFO 脚本开始执行。性能考虑在循环中尽量避免调用外部命令特别是处理大量数据时。例如用Shell内置的字符串操作代替sed或awk处理简单任务。代码风格统一保持一致的缩进推荐2或4个空格、命名规范变量小写常量大写使用shellcheck等工具进行静态检查。掌握Shell脚本编程是每一位Linux使用者进阶的必经之路。它让你从被动的命令执行者转变为主动的自动化流程设计者。本文从最基础的变量、循环、函数到核心的文本处理三剑客最后通过一个综合实战案例为你构建了一个从入门到实用的学习路径。记住学习Shell脚本的关键在于多动手、多实践、多踩坑。从简单的自动化任务开始逐步增加复杂度并时刻牢记安全与健壮性原则。当你能够熟练地运用Shell脚本解决工作中的重复劳动时你会发现效率和掌控感都得到了质的提升。
返回列表