
这类主题最值得先看的不是功能列表而是它到底能帮你解决哪些具体、重复的运维或开发问题。Shell脚本不是一门需要深究语法的“编程语言”而是一个能让你把日常敲的命令固化下来、自动执行的工具。对于运维、测试、后端开发甚至数据分析师来说学会写Shell脚本意味着能把那些需要反复登录服务器、查看日志、备份文件、批量处理数据的琐碎操作变成一键完成的任务。这篇文章会从“能用起来”的角度拆解从变量、循环到文本处理“三剑客”的核心用法重点不是罗列命令而是告诉你什么时候该用什么以及写脚本时最容易掉进去的坑。很多人觉得Shell脚本“简单”但真写起来却经常遇到权限不对、路径错误、变量作用域出问题、循环处理文件时文件名带空格就报错。这些细节才是从“知道”到“能用”的关键。我会假设你有一台Linux服务器或虚拟机Windows用WSL2也行跟着走一遍从单条命令到完整脚本的流程把那些容易忽略的环境、权限和边界条件都讲清楚。1. 先别急着写脚本搞清楚你的环境和要解决的问题在动手写第一行脚本之前有两件事比语法更重要一是明确你的运行环境二是想清楚你要自动化的是什么任务。1.1 环境准备不仅仅是“有Linux就行”你可以在真实的Linux服务器、虚拟机、WSL2甚至macOS的终端里运行Shell脚本。但不同环境有细微差别这会影响脚本的兼容性。确认你的Shell解释器最常用的是bash。在终端输入echo $SHELL或echo $0可以查看当前使用的Shell。虽然很多系统/bin/sh是bash的软链接但为了更好的兼容性特别是数组等特性脚本开头应明确指定#!/bin/bash这行叫shebang告诉系统用哪个解释器来执行下面的命令。权限是第一个拦路虎你写了一个脚本myscript.sh直接运行./myscript.sh可能会报Permission denied。这是因为文件没有执行权限。你需要chmod x myscript.sh这是新手必踩的坑。记住创建脚本文件后第一件事就是加执行权限。理解工作目录和绝对路径脚本里用的相对路径如./data/file.txt是相对于执行脚本时所在的目录而不是脚本文件所在的目录。这经常导致“文件找不到”的错误。一个稳妥的做法是在脚本开头用cd $(dirname $0)切换到脚本所在目录或者对于关键文件使用绝对路径。1.2 明确脚本要解决的任务从具体场景开始不要一上来就想写一个“万能管理脚本”。从一个小而具体的任务开始。比如场景1日志处理每天凌晨清理/var/log/下超过7天的日志文件。场景2备份将指定目录打包压缩并以“日期_目录名.tar.gz”的格式命名拷贝到备份服务器。场景3部署从Git拉取代码安装依赖重启应用服务。场景4监控检查某个进程是否在运行如果不在则发邮件报警并尝试重启。我们以**场景1日志清理**作为主线任务贯穿后续的变量、循环、函数等知识点。这样学起来有目标也知道每个语法点用在哪里。2. Shell脚本的基石变量、引号与条件判断变量是存储数据的地方但Shell的变量处理方式和其他编程语言很不一样这也是很多奇怪错误的根源。2.1 定义和使用变量等号两边不能有空格# 正确定义变量 log_dir/var/log days7 # 错误等号两边有空格 # log_dir /var/log # 这会被解释为命令log_dir参数是和/var/log # 使用变量需要加美元符号$ echo 日志目录是: $log_dir echo 清理 $days 天前的日志 # 如果变量名后面紧跟其他字符需要用花括号括起来 app_namemyapp echo 日志文件: ${app_name}_access.log # 正确输出 myapp_access.log # echo 日志文件: $app_name_access.log # 错误会尝试寻找变量 app_name_access2.2 引号的巨大差异单引号、双引号和反引号这是Shell脚本里最容易混淆也最重要的概念之一。单引号强引用。里面的所有字符包括$、\都原样输出不进行任何替换。nameWorld echo Hello $name # 输出Hello $name双引号弱引用。会进行变量替换$var和命令替换command或$(command)但其他字符如*、?通常保持原义除非在特定上下文如for循环中。nameWorld echo Hello $name # 输出Hello World反引号和$()命令替换。执行括号或反引号内的命令并用其输出结果进行替换。$()是更现代、更推荐的方式因为它可以嵌套且更清晰。# 获取当前日期 current_date$(date %Y%m%d) echo 今天是: $current_date # 统计文件行数 line_count$(wc -l /etc/passwd) echo 系统用户数行: $line_count关键点命令替换得到的是文本输出。如果命令执行失败或没有输出变量可能就是空的。实战建议除非你需要原样输出$符号否则对于包含变量的字符串一律使用双引号。这可以避免很多因空格或特殊字符导致的问题。2.3 条件判断[ ]与[[ ]]以及test命令条件判断用于控制脚本的流程。最常用的是if语句。if [ 条件 ]; then # 条件为真时执行的命令 elif [ 其他条件 ]; then # 其他条件为真时执行 else # 以上条件都不为真时执行 fi这里的[实际上是一个命令test命令的别名所以两边必须有空格。]是它的最后一个参数。文件测试if [ -f /path/to/file ]; then # 文件是否存在且是普通文件 echo 文件存在 fi if [ -d /path/to/dir ]; then # 目录是否存在 echo 目录存在 fi if [ -x /path/to/script.sh ]; then # 文件是否有执行权限 echo 脚本可执行 fi字符串比较nameAlice if [ $name Alice ]; then # 注意等号两边有空格变量用双引号括起 echo Hello Alice fi if [ -z $var ]; then # 判断变量是否为空字符串 echo 变量var是空的 fi if [ -n $var ]; then # 判断变量是否非空 echo 变量var有值 fi数值比较不能用、、要用-eq、-gt、-lt等。count10 if [ $count -gt 5 ]; then echo 数量大于5 fi更强大的[[ ]]这是bash等Shell内置的关键字比[ ]更强大、更安全。它支持通配符匹配和正则表达式并且字符串比较时不需要给变量加双引号但加了也没错是好习惯。filenametest.log if [[ $filename *.log ]]; then # 通配符匹配 echo 这是一个日志文件 fi if [[ $answer ~ ^[Yy](es)?$ ]]; then # 正则表达式匹配 echo 你选择了是 fi个人建议在写bash脚本时**优先使用[[ ]]**进行条件判断除非你需要极致的POSIX兼容性。现在我们可以为日志清理任务写一个简单的条件判断了检查目录是否存在。#!/bin/bash log_dir/var/log days7 if [ ! -d $log_dir ]; then # 注意!表示取反-d判断目录 echo 错误日志目录 $log_dir 不存在 exit 1 # 非0退出码表示脚本执行失败 fi echo 开始清理 $log_dir 下超过 $days 天的日志... # 后续清理逻辑3. 让脚本动起来循环与命令行参数单个任务用条件判断重复任务就要靠循环。Shell中主要有for循环和while循环。3.1for循环处理列表和文件for循环非常适合遍历一个已知的集合。遍历一个列表for app in nginx mysql redis; do echo 检查服务: $app systemctl status $app done遍历当前目录下的文件这里是巨坑高发区# 危险如果文件名包含空格如“my file.txt”会被拆成“my”和“file.txt”两个词处理 for file in *; do echo 处理文件: $file done更安全的做法是使用find命令配合while read循环后面讲或者确保你的脚本运行环境没有带空格的文件名。对于简单的、自己可控的目录可以暂时这么用。C语言风格的for循环for ((i0; i10; i)); do echo 这是第 $i 次循环 done回到日志清理任务我们需要找到/var/log下所有.log文件并判断其修改时间。for logfile in /var/log/*.log; do # 遍历/var/log下所有.log文件 if [ -f $logfile ]; then # 再次确认是文件 echo 检查文件: $logfile # 这里可以加入时间判断逻辑 fi done3.2while循环处理逐行输入和条件循环while循环会一直执行直到条件为假。它常用来读文件或监听状态。逐行读取文件这是处理文本文件最安全的方式之一。while IFS read -r line; do # IFS 防止行首尾空格被修剪-r 防止反斜杠转义 echo 处理行: $line done /etc/passwd # 将文件内容重定向给while循环与find命令结合安全处理所有文件包括带空格的文件名find /var/log -name *.log -type f | while IFS read -r logfile; do echo 安全处理文件: $logfile # 在这里即使$logfile包含空格它也是一个完整的字符串 done这是处理文件遍历的推荐做法。无限循环与退出while true; do echo 监控中...按CtrlC退出 sleep 5 # 休眠5秒 # 可以加入检查逻辑满足条件时用break退出循环 done3.3 接收命令行参数让脚本更灵活我们不可能每次都去改脚本里的log_dir和days变量。通过命令行参数传递更通用。位置参数$0是脚本名$1、$2...是第一个、第二个参数。# 脚本用法./clean_log.sh /var/log 7 log_dir${1:-/var/log} # $1是第一个参数如果为空则使用默认值/var/log days${2:-7} # $2是第二个参数默认7天 echo 清理目录: $log_dir echo 保留天数: $days特殊变量$#传递给脚本的参数个数。$所有参数列表每个参数都是独立的带引号字符串。遍历参数时推荐用它。$*所有参数列表但所有参数被合并成一个字符串。echo 一共传了 $# 个参数 for param in $; do # 使用$来安全遍历所有参数 echo 参数: $param done现在我们的日志清理脚本可以升级了#!/bin/bash # 用法./clean_log.sh [日志目录] [保留天数] target_dir${1:-/var/log} keep_days${2:-7} if [ ! -d $target_dir ]; then echo 错误目录 $target_dir 不存在。 exit 1 fi echo 开始清理目录 [$target_dir] 中超过 [$keep_days] 天的日志文件... # 使用find命令安全遍历文件 find $target_dir -name *.log -type f -mtime $keep_days | while IFS read -r old_log; do echo 正在删除: $old_log # 实际删除前可以先注释掉下一行用echo测试 rm -f $old_log done echo 清理完成。重要提醒rm -f命令非常危险在脚本中执行删除操作前务必先通过echo命令打印出要删除的文件列表进行确认确保逻辑正确无误。4. 文本处理“三剑客”grep, sed, awkShell脚本的强大一半体现在它能方便地调用像grep、sed、awk这样的文本处理工具。它们被称为“三剑客”是处理日志、配置文件、命令输出的利器。4.1grep全局正则表达式打印用于搜索过滤grep用来在文本中查找匹配特定模式的行。基本搜索# 在nginx日志中查找包含“404”的行 grep 404 /var/log/nginx/access.log # 忽略大小写 grep -i error /var/log/syslog # 显示匹配行的行号 grep -n panic /var/log/dmesg # 反向匹配显示不包含“INFO”的行通常用来找错误 grep -v INFO /var/log/myapp.log正则表达式grep默认支持基础正则表达式BRE-E选项支持扩展正则表达式ERE-P选项支持Perl正则部分系统支持。# 查找IP地址 grep -E ([0-9]{1,3}\.){3}[0-9]{1,3} /var/log/auth.log # 查找以“ERROR”或“FATAL”开头的行 grep -E ^(ERROR|FATAL) /var/log/myapp.log递归搜索目录# 在当前目录及子目录的所有.java文件中查找“TODO” grep -r TODO . --include*.java4.2sed流编辑器用于文本替换和编辑sed擅长对文本进行替换、删除、插入等操作尤其适合批量处理。基本替换s/原模式/新模式/标志# 将文件中的“foo”替换为“bar”只替换每行第一个 sed s/foo/bar/ file.txt # 替换所有全局 sed s/foo/bar/g file.txt # 替换第2行到第10行中的内容 sed 2,10s/foo/bar/g file.txt # 原地修改文件危险先备份-i 选项 sed -i.bak s/old/new/g config.ini # 先备份为config.ini.bak再修改原文件删除行# 删除所有空行 sed /^$/d file.txt # 删除包含“debug”的行 sed /debug/d file.txt打印特定行# 打印第5行 sed -n 5p file.txt # 打印第10到20行 sed -n 10,20p file.txt实战场景批量修改配置文件中的IP地址。# 假设config.ini里有一行 old_ip192.168.1.100 new_ip10.0.0.10 sed -i s/old_ip.*/old_ip$new_ip/ config.ini4.3awk强大的文本分析工具更像一门编程语言awk将文本视为由字段列组成的记录行非常适合处理表格型数据如ps、netstat、df的输出。基本结构awk 模式 {动作} 文件模式决定对哪些行执行动作如/error/、$1 100。动作对匹配行执行的操作如print,printf。内置变量NR当前行号、NF当前行的字段数、$0整行内容、$1、$2...第1、2...个字段。常用示例# 打印/etc/passwd的第一列用户名和第三列用户ID awk -F: {print $1, $3} /etc/passwd # -F: 指定冒号为字段分隔符 # 打印行号超过10的行 awk NR 10 file.txt # 计算第二列的总和 awk {sum $2} END {print 总和:, sum} data.txt # 处理ps aux输出打印CPU使用率超过5%的进程名和PID ps aux | awk $3 5.0 {print $2, $11} # $3是CPU%$2是PID$11是命令更复杂的逻辑awk支持if、for、while等语句。# 统计不同HTTP状态码的出现次数假设日志第9列是状态码 awk {status_count[$9]} END {for(code in status_count) print code, status_count[code]} access.log“三剑客”组合使用管道|是它们的粘合剂。# 找出访问量最高的前5个IP假设日志第一列是IP awk {print $1} /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -5 # 分解 # 1. awk {print $1}提取第一列IP # 2. sort排序让相同IP相邻 # 3. uniq -c统计每个IP出现的次数 # 4. sort -nr按次数倒序排序 # 5. head -5取前5行5. 函数、错误处理与脚本健壮性当脚本变长或者同一段代码需要重复使用时就该引入函数了。同时一个好的脚本必须能妥善处理错误。5.1 定义和使用函数函数让脚本结构更清晰也便于复用。#!/bin/bash # 定义一个函数打印带时间戳的日志 log_info() { local message$1 # local关键字声明局部变量避免污染全局 local timestamp$(date %Y-%m-%d %H:%M:%S) echo [$timestamp] INFO: $message } # 定义另一个函数检查目录是否存在不存在则创建 ensure_dir() { local dir_path$1 if [ ! -d $dir_path ]; then log_info 目录 $dir_path 不存在正在创建... mkdir -p $dir_path if [ $? -ne 0 ]; then # $? 获取上一条命令的退出状态码0表示成功 log_info 创建目录失败 return 1 # 函数返回非0表示失败 else log_info 目录创建成功。 fi else log_info 目录 $dir_path 已存在。 fi return 0 } # 主脚本逻辑 log_info 脚本启动。 backup_dir/backup/data ensure_dir $backup_dir # 调用函数 # 可以根据函数返回值做进一步判断 if [ $? -eq 0 ]; then log_info 目录准备就绪开始备份... # ... 备份逻辑 else log_info 目录准备失败退出脚本。 exit 1 fi log_info 脚本执行完毕。关键点local变量在函数内使用避免与外部变量冲突。函数参数通过$1,$2...获取。返回值使用return返回一个整数通常0成功非0失败调用者通过$?获取。函数必须先定义后调用。5.2 错误处理set -e,set -u,trap默认情况下Shell脚本会忽略命令的错误继续执行。这在生产环境是灾难。set -e(errexit)任何命令的返回值非0失败脚本就立即退出。#!/bin/bash set -e # 放在脚本开头 rm /tmp/somefile # 如果文件不存在rm会报错脚本会立即终止 echo 这行不会被执行到对于某些你预期会失败的命令可以用|| true来忽略错误rm /tmp/somefile || true # 即使失败脚本也不会退出 echo 这行会被执行set -u(nounset)遇到未定义的变量时当作错误并退出。这能防止因变量名拼写错误导致的诡异问题。#!/bin/bash set -u echo $my_var # 如果my_var未定义脚本会在这里退出并报错set -o pipefail在管道命令中只要有一个命令失败整个管道就视为失败。默认只取最后一个命令的退出状态。# 默认grep失败返回1但sort成功返回0整个管道返回0成功 cat file.txt | grep pattern | sort # 设置pipefail后grep失败整个管道返回非0失败 set -o pipefailtrap命令用于捕获信号执行清理操作。比如脚本被CtrlC中断时删除临时文件。#!/bin/bash temp_file/tmp/myscript.$$.tmp # $$是当前进程PID用于生成唯一文件名 cleanup() { echo 正在清理临时文件... rm -f $temp_file echo 清理完成。 } # 捕获EXIT信号脚本正常退出或异常退出都会执行 trap cleanup EXIT # 捕获INT信号CtrlC trap echo 用户中断; cleanup; exit 1 INT # 脚本主逻辑 echo 创建临时文件 $temp_file touch $temp_file # ... 其他操作可能会用到临时文件 sleep 10 # 模拟长时间运行此时按CtrlC会触发清理推荐做法在严肃的脚本开头加上这几行#!/bin/bash set -euo pipefail # 开启所有严格模式 trap echo 错误发生在第 $LINENO 行命令: $BASH_COMMAND; exit 1 ERR # 捕获ERR信号打印错误行和命令5.3 日志与调试记录日志不要只靠echo。将重要信息输出到文件。log_file/var/log/myscript.log exec $log_file 21 # 将脚本后续所有标准输出和错误输出都重定向到日志文件 # 或者只重定向部分 echo 开始任务 $(date) | tee -a $log_file # tee命令同时输出到屏幕和文件调试脚本bash -x script.sh以调试模式运行会打印出每一行执行的命令及其参数。在脚本中临时加入set -x和set x只调试特定代码块。#!/bin/bash echo 正常输出 set -x # 开启调试 complex_command_1 complex_command_2 set x # 关闭调试 echo 恢复正常输出6. 综合实战一个健壮的日志清理与归档脚本现在我们把前面所有知识点整合起来写一个更健壮、更实用的日志管理脚本。这个脚本将接收参数指定日志目录和保留天数。检查必要权限和目录。查找并删除过期日志文件。将删除的文件记录到日志中。可选地将要删除的文件先归档备份。具有良好的错误处理和日志功能。#!/bin/bash # 文件名log_rotate.sh # 功能清理指定目录下的过期日志文件支持归档备份。 # 用法./log_rotate.sh [-d 目录] [-k 天数] [-a 归档目录] [-h] # 示例./log_rotate.sh -d /var/log/app -k 30 -a /backup/logs # 严格模式与初始化 set -euo pipefail trap handle_error $LINENO ERR # 全局变量与默认值 LOG_DIR KEEP_DAYS7 ARCHIVE_DIR SCRIPT_LOG/var/log/log_rotate.log DRY_RUNfalse # 干跑模式只显示不执行 # 函数定义 # 错误处理函数 handle_error() { local line_num$1 echo [$(date %Y-%m-%d %H:%M:%S)] [ERROR] 脚本在行号 $line_num 处失败: $BASH_COMMAND | tee -a $SCRIPT_LOG exit 1 } # 日志函数 log_message() { local level$1 local msg$2 local timestamp$(date %Y-%m-%d %H:%M:%S) echo [$timestamp] [$level] $msg | tee -a $SCRIPT_LOG } # 显示用法 usage() { cat EOF 用法: $0 [选项] 选项: -d 目录 要清理的日志目录 (必需) -k 天数 保留最近多少天的日志 (默认: 7) -a 目录 归档目录如果提供则移动文件至此而非删除 (可选) -n 干跑模式只显示要执行的操作不实际执行 -h 显示此帮助信息 示例: $0 -d /var/log/myapp -k 30 $0 -d /var/log/nginx -k 14 -a /backup/nginx_logs EOF exit 0 } # 检查目录是否存在且可访问 check_directory() { local dir$1 local desc$2 if [ ! -d $dir ]; then log_message ERROR $desc $dir 不存在。 return 1 fi if [ ! -r $dir ]; then log_message ERROR $desc $dir 不可读。 return 1 fi if [ $desc ! 目标日志目录 ] [ ! -w $dir ]; then # 对于归档目录需要写权限 log_message ERROR $desc $dir 不可写。 return 1 fi log_message INFO $desc $dir 检查通过。 return 0 } # 解析命令行参数 while getopts d:k:a:nh opt; do case $opt in d) LOG_DIR$OPTARG ;; k) KEEP_DAYS$OPTARG ;; a) ARCHIVE_DIR$OPTARG ;; n) DRY_RUNtrue ;; h) usage ;; *) usage ;; esac done # 检查必需参数 if [ -z $LOG_DIR ]; then log_message ERROR 必须通过 -d 参数指定日志目录。 usage fi # 主逻辑开始 log_message INFO 日志清理任务开始 log_message INFO 日志目录: $LOG_DIR log_message INFO 保留天数: $KEEP_DAYS log_message INFO 归档目录: ${ARCHIVE_DIR:-无直接删除} log_message INFO 干跑模式: $DRY_RUN # 1. 检查目录 check_directory $LOG_DIR 目标日志目录 || exit 1 if [ -n $ARCHIVE_DIR ]; then check_directory $ARCHIVE_DIR 归档目录 || exit 1 fi # 2. 查找过期文件 log_message INFO 正在查找超过 $KEEP_DAYS 天的日志文件... # 使用find按时间筛选排除可能正在写入的当前日志例如access.log file_list$(find $LOG_DIR -name *.log -type f -mtime $KEEP_DAYS ! -name *.log.current) if [ -z $file_list ]; then log_message INFO 未找到需要清理的过期日志文件。 log_message INFO 任务结束 exit 0 fi file_count$(echo $file_list | wc -l) log_message INFO 找到 $file_count 个过期文件。 # 3. 处理文件 processed0 skipped0 echo $file_list | while IFS read -r file_path; do if [ ! -f $file_path ]; then log_message WARN 文件不存在或已被处理: $file_path ((skipped)) || true continue fi file_name$(basename $file_path) if [ -n $ARCHIVE_DIR ]; then # 归档模式 archive_path$ARCHIVE_DIR/$(date %Y%m%d)_${file_name} if [ $DRY_RUN true ]; then log_message INFO [干跑] 将移动: $file_path - $archive_path else mv $file_path $archive_path if [ $? -eq 0 ]; then log_message INFO 已归档: $file_path - $archive_path else log_message ERROR 归档失败: $file_path ((skipped)) || true continue fi fi else # 直接删除模式 if [ $DRY_RUN true ]; then log_message INFO [干跑] 将删除: $file_path else rm -f $file_path if [ $? -eq 0 ]; then log_message INFO 已删除: $file_path else log_message ERROR 删除失败: $file_path ((skipped)) || true continue fi fi fi ((processed)) || true done log_message INFO 处理完成。总计: $file_count, 成功: $processed, 跳过: $skipped. log_message INFO 日志清理任务结束 这个脚本的要点解析健壮性使用了set -euo pipefail和trap进行严格的错误处理。灵活性通过命令行参数-d、-k、-a、-n控制行为并提供了详细的usage函数。安全性有-n干跑模式可以先预览要执行的操作。对目录进行了存在性、可读性、可写性检查。使用while IFS read -r安全处理可能包含空格的文件名。每次操作都有成功/失败判断和日志记录。可维护性功能拆分成独立的函数handle_error,log_message,check_directory。关键步骤都有日志输出便于事后排查。逻辑清晰易于扩展例如可以增加压缩归档、邮件通知等功能。你可以将这个脚本保存为log_rotate.sh赋予执行权限chmod x log_rotate.sh然后通过cron定时任务如每天凌晨2点自动执行# 编辑当前用户的cron任务 crontab -e # 添加一行例如每天凌晨2点执行并保留30天日志 0 2 * * * /path/to/your/log_rotate.sh -d /var/log/myapp -k 30 -a /backup/logs /var/log/log_rotate_cron.log 217. 脚本编写中的常见“坑”与最佳实践最后总结一些Shell脚本编程中高频出现的陷阱和对应的避坑指南。7.1 路径与空格问题坑变量引用不加双引号导致路径中的空格被拆分成多个参数。# 错误 rm -f $my_file # 如果my_file是“my document.txt”会被拆成“my”和“document.txt” # 正确 rm -f $my_file实践所有变量引用只要代表一个整体尤其是路径、文件名都加上双引号。7.2 命令执行结果与错误处理坑直接使用未检查的命令结果。# 错误如果find没找到文件files为空for file in $files会变成for file in可能不会报错但逻辑错误。 files$(find . -name *.tmp) for file in $files; do ... done # 稍好使用数组但依然要处理空情况 mapfile -t files (find . -name *.tmp 2/dev/null) if [ ${#files[]} -gt 0 ]; then ... fi # 更好使用find -exec或while read循环如前文所示实践对于可能失败的命令检查其退出状态$?。对于查找文件优先使用find ... -exec或管道到while read。7.3 数值计算坑在[ ]或[[ ]]中使用、进行数值比较它们在这里是字符串比较。a10 b2 # 错误这会进行字符串比较10 2 在字典序上成立结果为真 if [[ $a $b ]]; then echo a小于b; fi # 正确使用算术比较 if (( a b )); then echo a小于b; fi # 或者在[ ]中使用 -lt, -gt if [ $a -lt $b ]; then echo a小于b; fi实践进行数值计算和比较时使用(( ))算术表达式。7.4 子Shell与变量作用域坑在管道|中创建的变量在管道结束后就消失了。total0 find . -type f | while read file; do ((total)) # 这个total是在子Shell中不会改变外层的total done echo 找到文件数: $total # 输出永远是0实践避免在管道右侧的循环中修改外部变量。可以使用进程替换或临时文件来传递数据。# 使用进程替换和重定向输入 total0 while read file; do ((total)) done (find . -type f) # 注意 (...)的语法 echo 找到文件数: $total7.5 脚本的可移植性坑脚本中使用了特定Shell的特性如bash的数组[[ ]]却在sh环境下运行。实践脚本开头明确指定#!/bin/bash。如果追求最大兼容性如需要在不同Unix-like系统上运行使用#!/bin/sh并只使用POSIX标准语法避免数组、[[ ]]等。使用command -v而不是which来检查命令是否存在which不是POSIX标准命令。if ! command -v git /dev/null 21; then echo git命令未找到请先安装。 exit 1 fi7.6 最佳实践清单开头使用#!/bin/bash和set -euo pipefail。注释为函数和复杂逻辑添加注释说明目的和参数。变量使用全大写命名只读常量小写命名普通变量。引用时加双引号。函数使用函数组织代码用local声明局部变量。错误处理检查重要命令的返回值使用trap进行清理。输入验证永远不要相信外部输入参数、文件内容进行必要的验证。日志重要的操作和错误要记录日志便于排查。测试使用-n干跑模式或在小范围测试后再全量运行特别是涉及rm、mv、chmod等危险命令时。代码风格保持一致的缩进建议2或4个空格一行不要太长。版本控制将脚本纳入Git等版本控制系统管理。Shell脚本的威力在于将零散的命令串联成自动化的工作流。从一行命令开始逐步封装成函数最后组合成带参数、错误处理和日志的健壮脚本这个迭代过程本身就是运维和开发能力的体现。真正掌握Shell脚本不是背下所有命令而是理解如何让计算机可靠、自动地替你完成那些重复劳动。