拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Shell脚本核心:循环与条件分支从原理到实战

动手写Shell脚本也快十年了回头来看最核心的骨架就两块循环和条件分支。很多新手拿着一堆命令不知道怎么写进脚本里其实本质上就是没想清楚这两件事——什么时候重复做同一件事什么时候根据情况做不同的事。这篇文章就把linux shell的循环语句和条件分支语句从原理到实战彻底拆开讲一遍包括for、while、until、if、case以及循环里面套条件、条件里面套循环的组合玩法还有我在实际踩坑后总结的调试方法。适合刚入门shell脚本的人也适合写过一些脚本但总感觉不够稳的老手拿来查漏补缺。1. 为什么循环和条件分支是Shell脚本的骨架很多人学Linux命令的时候挺溜一条grep、一条awk、一条sed都能敲得飞起但一写脚本就卡壳。我见过太多的初学者代码把命令一条条往下写写一百多行中间没有任何判断和重复逻辑。这种脚本换个环境、换个数据量分分钟报废。真正的脚本应该是让机器去做判断、去做重复劳动而不是你替它把每一步都安排好。1.1 循环解决的本质问题批量重复劳动Shell脚本里最常见的使用场景就是批量处理。比如你有几百个日志文件需要扫描关键字有一堆图片需要重命名有几十台服务器需要批量执行同一个命令。这种需求写成一条条命令是不现实的所以必须靠循环。循环的本质就三件事遍历什么、每次做什么、什么时候停。你把这三点想清楚了任何循环都能写得出来。遍历的对象可以是数字、文件列表、命令行参数、文本文件的每一行甚至是一个命令的输出结果。每次做的动作可以是任意的命令组合。停下来的条件可以是遍历完了也可以是你指定的某个条件成立。1.2 条件分支解决的本质问题分流与容错条件分支解决的是选择问题。脚本里的每一步操作都可能出现不同的结果——文件存在还是不存在数值是大于还是小于命令执行成功还是失败。这些不同的结果需要走不同的逻辑这就是if和case存在的意义。我见过很多人写脚本从来不判断命令是否执行成功一条命令执行完了直接干下一步。这在小脚本里可能碰巧没问题但在正式环境中绝对不行。比如你cd到一个目录失败了结果后面的操作全都跑偏了你cp一个文件失败了结果后面还继续处理这个不存在的文件。条件分支不只是为了让程序更智能更重要的是让程序更健壮。1.3 两者结合的典型场景实际工作中循环和条件分支很少单独出现。最常见的组合模式是在循环里做条件判断筛选出符合条件的再进行下一步操作或者用条件判断控制循环的终止。举个例子你写一个日志巡检脚本要遍历所有日志文件检查是否有ERROR级别的报错。这个场景就是典型的循环里面套条件——循环负责遍历文件条件负责判断文件里有没有关键词。再比如你写一个重试逻辑某个任务失败了就重试最多重试5次。这是条件控制循环——循环负责执行重试条件负责判断要不要继续重试、要不要退出。不夸张地说如果你能把循环和条件分支这两种结构熟练组合Shell脚本的百分之八十的场景你都能搞定。剩下的百分之二十不过是在这两种结构里填充不同的命令而已。2. for循环全解列表遍历、C风格与参数处理for循环是Shell里用得最多的循环结构也是最容易上手的一个。它的核心思路就是给我一个列表我依次处理列表里的每一项。2.1 最基础的列表遍历写法先看最基本的写法for fruit in apple banana orange; do echo I like $fruit done这里in后面跟的就是列表列表项之间用空格分隔。每循环一次变量fruit就依次取到列表里的一个值然后执行do和done之间的命令块。这个写法最简单的变体是用数字范围for i in {1..10}; do echo 第 $i 次循环 done{1..10}是Bash的展开语法会生成从1到10的数字序列。注意这个把数字列表换成文件列表也很常见for file in /var/log/nginx/*.log; do echo 处理文件: $file done这里通配符*.log会被展开成所有匹配的文件名然后依次赋值给file变量。这个场景在日志分析、批量备份、批量清理里非常常用。2.2 遍历命令输出结果更骚的操作是把命令的结果作为遍历列表for ip in $(cat server_list.txt); do ping -c 1 $ip echo $ip 通了 || echo $ip 不通 done这里的$(cat server_list.txt)会先执行cat命令把文件内容作为列表传给for循环。每一行内容会被当成一个独立的列表项实际上是以空格或换行符分割。这种方式在处理主机清单、账号列表等场景下特别实用。但我必须提醒一个常见的坑如果文件里某一项包含空格它会被拆成多个列表项。比如文件里有一行内容New York实际上会被拆成New和York两个项。如果你遍历的是文件名、用户名这类可能包含空格的数据不要用这种方式改用while read后面会详细讲。2.3 C语言风格的for循环Bash还支持类C语言的for循环写法适合需要精确控制循环次数和步长的场景for ((i 0; i 100; i 2)); do echo 当前值: $i done这个写法有三个表达式初始值、循环条件、步进操作。上面这个循环会打印从0到98的所有偶数。相比列表遍历C风格的优势在于循环次数可控、步长可控适合计算类需求。比如你要计算1到100的和sum0 for ((i 1; i 100; i)); do sum$((sum i)) done echo 1加到100的结果是: $sum$((...))是算术运算语法在Shell里做整数运算都靠它。2.4 处理命令行参数$与shift的配合脚本接收外部参数的时候$表示所有参数列表$#表示参数个数。最常见的就是用$做遍历#!/bin/bash for arg in $; do echo 收到参数: $arg done注意这里$要加双引号这样每个参数即使包含空格也会被当成一个整体。再来说shift命令。它的作用是把参数列表往左移动一位即原来的$2变成了$1$#减一。这个命令经常在需要逐个消费参数的场景中使用#!/bin/bash while [ $# -gt 0 ]; do case $1 in -h|--help) echo 帮助信息 shift ;; -v|--version) echo 版本号 shift ;; *) echo 未知参数: $1 exit 1 ;; esac done上面这段是常见的命令行参数解析框架while循环配合shift逐个吃掉参数case对每个参数做分支处理。这是Shell脚本里最标准的参数解析写法值得记下来。2.5 实战案例批量重命名文件讲了这么多基础语法来一个能直接用的案例。假设你有一批jpg文件要把它们重命名为年月日_序号.jpg的格式#!/bin/bash DATE$(date %Y%m%d) count1 for file in /path/to/photos/*.jpg; do if [ -f $file ]; then newname${DATE}_${count}.jpg mv $file $(dirname $file)/$newname echo 已将 $file 重命名为 $newname count$((count 1)) fi done这里有几个细节值得注意在mv重命名时我用了$(dirname $file)来拼完整路径是为了防止文件名或路径里存在意外的特性用if [ -f $file ]判断文件确实存在避免通配符没有匹配到任何文件时把字面量*.jpg当成文件名处理。这两个小细节都是实战中容易忽略、但出了问题很折腾的地方。3. while与until按条件运转的循环控制for循环解决的是已知范围的遍历但有些场景你不知道循环具体要执行多少次只知道在某个条件成立或失效时停止这时候就要用while或者until。3.1 while read line逐行处理文件最稳的方式前面我说过用for file in $(cat ...)遍历文件内容存在空格拆分的问题遇到这种情况while read line是更稳妥的方案#!/bin/bash while IFS read -r line; do echo 处理行: $line done /etc/passwd这个写法的原理是把文件通过重定向交给while循环read命令每次从文件读取一行内容赋值给变量line然后执行循环体。这里的两层保险值得你记住IFS表示清空内部字段分隔符防止行内空格被拆分。-r表示禁止read对反斜杠转义防止路径里\被吃掉。我有一个处理CSV文件的脚本以前用for遍历遇上带空格的字段就被拆得七零八落。换成while IFS read -r line之后所有问题都消失了。这个写法的含金量用过的都懂。3.2 无限循环与break/continue的退出控制服务器巡检、实时日志监控这类场景经常需要一直跑直到满足某个条件才退出。写法是while true配合break#!/bin/bash count0 while true; do count$((count 1)) echo 第 $count 次尝试 # 假设这是一个成功就退出的操作 if [ $count -ge 5 ]; then echo 超过5次退出 break fi donebreak的作用是跳出整个循环continue的作用是跳过本次循环的剩余部分、进入下一次循环。这两个命令在嵌套循环里还可以加数字参数比如break 2就是跳出两层循环不过实战中我建议尽量少用嵌套层数多了代码很难读。典型的continue应用场景只处理不满足排除条件的文件。for file in /var/log/*.log; do if [[ $file ~ \.gz$ ]]; then continue fi echo 处理非压缩日志: $file done3.3 until逻辑反转的whileuntil的逻辑跟while正好相反——条件是假的时候才继续循环条件是真的时候退出。看个对比# while写法当 count 小于5时继续 count0 while [ $count -lt 5 ]; do echo count$count count$((count 1)) done # until写法直到 count 大于等于5才停止 count0 until [ $count -ge 5 ]; do echo count$count count$((count 1)) done这两个例子执行效果一模一样。until用得相对少但它有个独特的优势——当你想表达的是重复操作直到成功为止的时候语义上比while更自然#!/bin/bash until ping -c 1 192.168.1.1 /dev/null 21; do echo 等待网关恢复... sleep 2 done echo 网关已恢复3.4 循环嵌套与性能陷阱循环是可以嵌套的比如打印九九乘法表的经典案例#!/bin/bash for ((i 1; i 9; i)); do for ((j 1; j i; j)); do printf %d*%d%-2d $i $j $((i * j)) done echo done嵌套循环最需要注意的是性能问题。我在实战中处理过一个批量脚本两层for循环里各跑了一堆外部命令处理一万条数据跑了一下午。后来优化了逻辑把内部循环的命令合并成一次awk调用时间缩短到几分钟。Shell脚本的性能瓶颈往往不在语法本身而在频繁调用外部命令。每次调用grep、sed、awk都会开启新进程循环里调用几千次开销是惊人的。能用内建操作就用内建操作能用管道一次处理就别一条条循环处理。这是Shell脚本从能跑到跑得快的关键一步。4. if条件分支test命令、方括号与命令返回值的细节if语句是条件分支的基础语法很简单但真正写得好的人不多。大多数人栽在test命令的细节上。4.1 if-elif-else基本结构先看标准的语法#!/bin/bash score85 if [ $score -ge 90 ]; then echo 优秀 elif [ $score -ge 80 ]; then echo 良好 elif [ $score -ge 60 ]; then echo 及格 else echo 不及格 fi注意关键字的位置then和条件在同一行需要加分号或者换一行单独写也行。fi是if的反写表示结束。这个设计在Shell里很常见——if和fi成对出现case和esac成对出现英文单词倒过来就是结尾。很多人刚写的时候会忘记写fi结果脚本报语法错误。我的建议是写if的时候先把fi写上再往回填内容就像写代码先打左右括号一样。4.2 test命令的常用判断if后面的方括号[实际上是一个命令的简写形式完整的命令名是test。也就是说[ -f /etc/passwd ]等价于test -f /etc/passwd。这个细节很多人不知道理解了它你就能明白为什么方括号两侧必须有空格——因为[是一个独立的命令命令和参数之间必须有空格分隔。常用的test判断我整理一下判断类型表达式含义文件判断-f 文件是否为普通文件文件判断-d 目录是否为目录文件判断-e 路径路径是否存在文件判断-s 文件文件存在且非空文件判断-r/-w/-x 文件是否有读/写/执行权限数值比较-eq等于数值比较-ne不等于数值比较-gt大于数值比较-lt小于字符串比较等于字符串比较!不等于字符串比较-z 字符串字符串为空字符串比较-n 字符串字符串非空逻辑运算! 表达式取反逻辑运算-a逻辑与and逻辑运算-o逻辑或or看一个综合使用多个判断的案例#!/bin/bash file/etc/nginx/nginx.conf if [ -f $file ] [ -r $file ]; then echo 配置文件存在且可读 fi注意我用了两个独立方括号配合而不是在一个方括号里用-a。原因是[ ... -a ... ]在某些shell环境下有歧义而是当前shell层面的语法更可靠。同样||表示逻辑或。这两个运算符在Shell里还承担了前一个命令成功才执行后一个的语义后面会讲。4.3[ ]与[[ ]]的差异正则与容错除了单方括号[ ]Bash还提供双括号[[ ]]。在写脚本时我几乎无脑推荐双括号因为它在这几个方面优势明显不需要对变量加引号。[ $var abc ]在$var为空时会展开成[ abc ]直接报错而[[ $var abc ]]可以安全处理空值。支持和||直接写在括号内。[[ $a -gt 1 $b -lt 10 ]]一行搞定不需要拆成两个方括号。支持正则匹配。[[ $str ~ ^[0-9]$ ]]可以直接匹配字符串是不是纯数字。这在[ ]里做不到必须绕道grep。支持通配符模式匹配。[[ $file *.log ]]直接用*.log模式匹配。看个正则的实战用法判断IP是否合法#!/bin/bash ip192.168.1.1 if [[ $ip ~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then echo IP格式正确 else echo IP格式错误 fi这里我只判断了数字格式没有判断每个数字不能超过255实际使用时可以再加条件。但通过这个例子你能看到双括号配合正则的威力。需要注意[[ ]]是Bash特有的sh等POSIX shell不支持。如果脚本开头是#!/bin/sh最好用[ ]保证可移植性。但如果你面向的是纯Bash环境绝大多数Linux发行版默认就是用[[ ]]没问题。4.4 直接判断命令返回值的进阶写法Shell里一个隐藏得很深、但又极其重要的知识点任何命令本身都有返回值0表示成功非0表示失败。if后面可以直接接命令不用接test表达式#!/bin/bash if grep -q ERROR /var/log/app.log; then echo 日志中存在ERROR else echo 日志中无ERROR fi这里的关键是grep -q里面的-q参数它让grep进入静默模式不输出任何匹配内容只通过返回值表示是否匹配到。这个组合if grep -q是Shell脚本里做存在性检查最干净利落的方式比用grep ... | wc -l判断行数强多了省掉了管道性能也更好。另一个高频场景是用ping检查主机连通性#!/bin/bash if ping -c 1 -W 1 192.168.1.1 /dev/null 21; then echo 主机在线 else echo 主机离线 fi这里把ping的正常输出和错误输出都指向了/dev/null我们只关心返回值的含义。这样的写法在巡检脚本里会反复出现养成用返回值判断命令是否成功的习惯是Shell脚本从新手到进阶的一个分水岭。5. case多分支比if更清晰的多模式匹配当你要判断的情况不是两个三个而是五六个甚至更多时用if写出来会变成一串很长的elif可读性直线下降。这时候case才是合适的选择。5.1 case的基本语法与匹配模式case的语法结构长这样#!/bin/bash read -p 请输入你的选择: choice case $choice in start) echo 启动服务 ;; stop) echo 停止服务 ;; restart) echo 重启服务 ;; *) echo 无效输入请输入 start|stop|restart exit 1 ;; esacpattern写在每一项的开头后面跟)匹配后执行下面的命令直到;;结束这一分支。*是通配符匹配所有情况相当于if里的else放在最后兜底。整个case用esaccase的反写结尾。这个结构和C语言、Java里的switch很像但Shell的case更灵活——因为匹配模式走的不是简单的相等判断而是通配符和整个文件系统的glob模式。5.2 通配符匹配与组合模式Case的匹配模式可以很丰富比如#!/bin/bash read -p 输入一个文件名: fname case $fname in *.log) echo 这是日志文件 ;; *.txt|*.md) echo 这是文本文件 ;; *.sh) echo 这是Shell脚本 ;; *) echo 未知类型的文件 ;; esac注意*.txt|*.md的组合写法竖线在这里表示或。你也可以写单个字符范围case $char in [a-z]) echo 小写字母 ;; [A-Z]) echo 大写字母 ;; [0-9]) echo 数字 ;; esac5.3 服务管理脚本case的经典应用几乎所有Linux服务管理脚本比如/etc/init.d/下的脚本都是这个骨架——case根据第一个参数决定执行哪个分支#!/bin/bash # 简化版服务管理脚本 service_namemyapp pid_file/var/run/myapp.pid start() { echo 启动 $service_name ... # 这里放实际启动命令 } stop() { echo 停止 $service_name ... # 这里放实际停止命令 } status() { if [ -f $pid_file ]; then echo $service_name 正在运行 else echo $service_name 未运行 fi } case $1 in start) start ;; stop) stop ;; restart) stop sleep 1 start ;; status) status ;; *) echo 用法: $0 {start|stop|restart|status} exit 1 ;; esac我特别喜欢这个案例因为它把几个知识点串起来了函数定义、case分支、test判断。新手把这个脚本吃透基本就能看懂系统里大部分启动脚本的写法了。另外注意最后那个兜底分支不接受任何未知参数打印用法并退出这是脚本友好设计的关键——用户敲错命令时给它一个明确的提示而不是让它闷头报错。5.4 case与循环组合命令参数解析的完整方案前面讲shift的时候已经用了case和while的组合。这里再补齐一个更实用、更完整的参数解析模板#!/bin/bash verbose0 output_file port8080 while [ $# -gt 0 ]; do case $1 in -v|--verbose) verbose1 shift ;; -o|--output) output_file$2 shift 2 ;; -p|--port) port$2 shift 2 ;; -h|--help) echo 用法: $0 [-v] [-o 文件名] [-p 端口] exit 0 ;; *) echo 未知参数: $1请用 -h 查看帮助 exit 1 ;; esac done echo verbose$verbose echo output_file$output_file echo port$port这个模板我在实际项目中写了很多次值得你收藏。核心思想是while判断参数列表还有没有剩余参数case根据参数内容分类处理带值参数用shift 2一次跳过参数名和值两格。想要加新参数只需在case里多写一个分支。6. 完整实战日志关键字巡检与告警脚本前面五章把语法和基础场景拆开了这一章用一个我实际工作中经常写的日志巡检脚本把这些内容串起来。目标场景是这样的服务器上每天产生多个服务的日志文件需要一个脚本定期检查里面有没有ERROR和WARN级别的日志ERROR出现超过一定次数就触发告警并把结果生成报告。6.1 需求分析与脚本设计需求拆开就是两件事遍历日志文件循环、判断日志级别和数量条件分支。设计如下日志文件放在/var/log/myapp/目录下文件名以.log结尾。对每个文件统计ERROR出现的次数超过阈值可配置就标记异常。不管异不异常把结果写入当天的巡检报告。如果发现异常文件输出告警信息并让脚本有一个非零的退出码方便接入外部监控。6.2 完整脚本实现#!/bin/bash # 日志巡检脚本检查 /var/log/myapp/ 下的所有日志 # 用法: ./log_check.sh LOG_DIR/var/log/myapp THRESHOLD5 REPORT/tmp/log_report_$(date %Y%m%d).txt alert_flag0 # 清空上次的报告 $REPORT echo 日志巡检报告 $(date %Y-%m-%d %H:%M:%S) $REPORT for logfile in $LOG_DIR/*.log; do # 处理没有匹配文件的情况 if [ ! -f $logfile ]; then echo 警告: $logfile 不存在或不是普通文件 $REPORT continue fi filename$(basename $logfile) # 统计ERROR和WARN数量 error_count$(grep -c ERROR $logfile 2/dev/null || echo 0) warn_count$(grep -c WARN $logfile 2/dev/null || echo 0) echo 文件: $filename | ERROR: $error_count | WARN: $warn_count $REPORT # 条件判断是否超过阈值 if [ $error_count -gt $THRESHOLD ]; then echo 异常: ERROR数量超过阈值($THRESHOLD)请关注! $REPORT echo [告警] $filename 存在大量错误日志请检查 alert_flag1 fi done if [ $alert_flag -eq 1 ]; then echo 巡检完成发现异常报告见: $REPORT exit 1 else echo 巡检完成一切正常报告见: $REPORT exit 0 fi6.3 脚本中的关键细节解读这个脚本虽然不长但值得解读的细节不少第一是grep -c统计数量。-c参数让grep直接输出匹配行数不需要再用管道接wc -l更高效。但要注意如果文件里完全没有匹配内容grep的返回值是1失败配合|| echo 0让它在失败时输出一个0这样error_count变量不会因为未匹配而变成空值后面的数值比较才不会出错。第二是continue的使用。检查到文件不存在时直接continue跳到下一个文件避免后续对不存在的文件执行grep时产生一堆错误输出。第三是exit 1作为告警信号。外部监控工具比如cron任务判断、监控平台脚本可以通过检查这个脚本的退出码来决定是否触发告警。这是Shell脚本作为监控组件的标准做法。第四是报告文件用$(date %Y%m%d)按天命名每天一份不会覆盖历史记录。日常使用中还可以加个find /tmp -name log_report_* -mtime 7 -delete在开头或结尾做自动清理保留7天就够了。6.4 调试经验bash -n与set -x写脚本的过程中肯定会遇到语法错误和逻辑错误我的调试三板斧分享给大家。语法检查用bash -nbash -n log_check.sh这个命令只检查语法是否正确不实际执行脚本。如果语法有错它会指出第几行有问题没有输出就说明语法通过。这是写完脚本后的第一道关卡。逻辑调试用set -x在脚本开头加一行set -x或者在执行时用bash -x log_check.sh脚本会把每条实际执行的命令打印出来变量会展开成具体值方便你追踪执行流程和变量变化。调试完记得去掉或者临时在函数内加上set -x/set x只在某段代码里打开跟踪。变量值确认用printf而不是echo如果你的变量里可能包含换行、特殊字符等不可见内容echo打印出来你可能看不出问题。printf %s\n $var会更准确实在地显示内容。注意echo $var里的变量不加引号会导致空格和换行被折叠这个坑我踩过不少次打印变量一定加引号。7. 必须知道的Shell常见坑位与避坑经验最后这部分把我在实际项目中踩过最深的几个坑集中列一列。每个坑都是真金白银换来的教训希望你能绕开。7.1 管道导致的子Shell陷阱while里的变量丢了这是Shell脚本里知名度最高的坑。看下面这段代码#!/bin/bash count0 cat /etc/passwd | while read line; do count$((count 1)) done echo 总行数: $count你猜输出多少答案是0。原因在于管道|右侧的while循环是在一个子Shell中执行的子Shell里的count变量修改不会影响父Shell中的count。整个循环跑完后子Shell退出count回到父Shell里原来的值。解决办法有两个。一是把循环放到同一进程里执行用重定向代替管道count0 while read line; do count$((count 1)) done /etc/passwd echo 总行数: $count二是如果确实需要管道养成分组习惯count0 cat /etc/passwd | { while read line; do count$((count 1)) done echo 这个在子Shell里能读到count: $count }但第二个方法的作用域问题依然存在出来就失效了。所以我的建议是能用重定向不用管道避免不必要的子Shell。在初学阶段就建立这个意识后面能省很多排查时间。7.2 文件名带空格导致的遍历失败前面提过for循环遍历列表时按空格和换行符分隔。如果你有一个文件叫my document.txt在for循环里会被当成my和document.txt两项处理。最稳妥的方案是用find的-print0配合read -d #!/bin/bash find /path/to/files -name *.log -print0 | while IFS read -r -d file; do echo 处理文件: $file done-print0让find用空字符分隔文件名而不是换行符read -d 让read以空字符作为读取终止符。这个组合对带空格、带换行的极端文件名都能正确处理。严格来说这个写法里while也是子Shell内执行的如果循环结束后还需要用循环里的结果别忘了用7.1的重定向代替管道。7.3 grep返回值陷阱与set -e的冲突现在很多脚本喜欢开头写set -e意思是任何命令返回非零就立即退出。这本身是好习惯但和某些你预期它可能失败的命令会冲突。比如你在脚本里写了set -e if ! grep -q ERROR /var/log/app.log; then echo 没有找到ERROR日志一切正常 fi这里grep -q如果没有匹配到内容会返回1但因为它在if判断中set -e不会触发退出这是Shell专门为if条件中的命令开了免死金牌。但如果同一行命令用了|管道比如set -e grep ERROR /var/log/app.log | head -1 /tmp/error.txt管道左侧的grep返回非零在某些shell配置下set -e可能直接让脚本退出。这种问题很隐蔽排查起来头大。我的建议是如果脚本对容错要求比较高不要盲目全局开set -e或者在确实可能失败的命令后加|| true明确告诉shell这个命令失败我不要紧。7.4 多余的空格和引号问题Shell对空格极其敏感。写变量赋值时等号两边不能有空格var123是对的var 123会被解析成执行var命令、参数是和123。方括号判断时方括号和表达式之间必须有空格[ $var abc ]是对的[ $varabc ]会被当成一个字符串判断是否为空结果大相径庭。每次遇到诡异的行为先看看是不是空格问题。我调试Shell脚本的历史里大概有三分之一的问题最后都归结到空格上。这不是技术含量的问题就是心细不细的问题写多了自然会形成肌肉记忆。7.5 环境差异一个脚本在CentOS好好的在Ubuntu报错最后提醒一个容易被忽视的点Linux发行版默认shell不同。CentOS和大多数现代发行版默认是Bash但有些精简环境比如Docker容器基于Alpine的默认shell是BusyBox ash或者dash它们对[[ ]]、${var,,}这种大小写转换语法、数组特性都不支持。如果脚本将来有可能跑在不同环境写之前先确认一下目标环境有没有bash没有就安装bash或者写脚本时尽量用POSIX兼容语法用[ ]代替[[ ]]避免用数组等高级特性。我个人在实际项目中的做法是在脚本头部明确写#!/bin/bash并在部署文档里标注需要bash作为解释器。这样架构层面的问题提前暴露不会等到上线了才出幺蛾子。写Shell脚本这么多年我最深刻的体会是循环和条件分支这些语法只是骨架真正的功力在于对细节的把控——变量加不加引号、用重定向还是管道、返回值判断得对不对、子Shell陷阱躲没躲开。这些细节叠加起来决定了一个脚本是能跑一次还是稳定跑一年。建议你把今天说的这些案例亲手敲一遍改一改阈值和目录路径看到变量丢失、遍历出错的实际现象会比光看文章记忆深十倍。踩过坑才算真的会了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门