ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Shell流程控制实战:退出码、条件判断与循环避坑全解析

Shell流程控制实战:退出码、条件判断与循环避坑全解析 写 Shell 脚本最容易翻车的地方不是记不住if和then的顺序也不是分号该打在哪儿而是很多人脑子里还停留在其他编程语言的思维方式里。我见过不少写了半年 Shell 的同学一直在用if [ $? -eq 0 ]判断上一条命令成不成功还有人想逐行统计日志结果 while 循环里加的计数器到外面就归零了。这些现象根源基本都在 Shell 流程控制的底层规则没理顺。这篇内容就围绕 if-else、for/while 循环、case 语句展开把条件判断、分支选择、循环控制的执行逻辑和典型坑点一次性讲透。我不打算让你背语法而是想说明白每条语法背后 Shell 实际做了什么遇到异常输出时它到底是怎么走过来的。刚接触 Linux 脚本的新手可以通篇读已经写过不少脚本但经常被“诡异现象”卡住的朋友重点看每个小节后面的坑位提示尤其是最后一部分的复盘清单都是我在真实脚本里踩过或者救过火的案例。1. Shell 的“真假”不是布尔值是退出码1.1 if 后面到底该放什么如果你从 Python、Go 这类语言转过来写条件判断时很容易浮现出 true/false 的概念然后写类似if [ $a 1 ]的代码。看到这段代码你会默认中括号内部是一个表达式表达式的结果是布尔值。但 Shell 不是这样运作的。Shell 里的if不看布尔值它看的是退出码0 表示真非 0 表示假。几乎所有命令执行完都会返回一个退出码命令成功返回 0失败返回非 0。if后面跟的其实是一串命令它只管这串命令最终返回的退出码是多少。刚开始你可能觉得这没什么但它能解开很多困惑。比如最常见的“判断文件中是否包含某行”if grep -q error /var/log/app.log; then echo 日志里出现了 error 关键字 figrep找到匹配内容时返回 0if 条件成立找不到时返回 1if 条件不成立。你没有写任何中括号也不需要把 grep 的输出保存起来再判断直接用命令本身作为条件。同理也适用于cdif cd /data/backup; then echo 目录切换成功 ficd成功进入目录返回 0目录不存在返回非 0。这种用法比写if [ $? -eq 0 ]干净得多也安全得多。原因很简单$?是一种“易失状态”只要中间多执行了一条命令它就被覆盖了。比如下面这种写法就很容易出隐蔽问题grep -q error file.log echo grep 执行完了 if [ $? -eq 0 ]; then echo 找到了 error fi第二条echo已经成功执行并返回 0所以$?被覆盖成了 0导致 if 判断结果完全不可信。直接用命令退出码当条件从根本上绕开这个坑。1.2 需要计算条件时test 和 [ ] 才登场刚才说 if 后面跟命令那条“命令”可以是普通命令也可以是一条专门做条件判断的命令也就是test。test命令专门用来比较文件、字符串和数字然后返回退出码。[ ]写法本质上是test的另一种形式只不过要求最后一个参数必须是]。# 等价 if test -f /etc/hosts; then echo /etc/hosts 存在 fi if [ -f /etc/hosts ]; then echo /etc/hosts 存在 fi这解释了一个新手上路必踩的语法错误中括号内部必须保留空格。if [-f /etc/hosts]; then # 错误Shell 找不到一个叫 [-f 的命令 if [ -f /etc/hosts ]; then # 正确[本身是命令名不是语法符号所以它和后面的参数之间必须有空格分隔。很多编辑器会对这行标红但初看的人不一定反应过来是空格问题。日常最常用的是下面几类条件条件含义典型判断目标[ -f $file ]是普通文件配置文件是否存在[ -d $dir ]是目录目标目录是否存在[ -e $path ]文件或目录存在通用存在性判断[ -z $var ]字符串长度为 0参数是否为空[ -n $var ]字符串长度不为 0参数是否有值[ $a $b ]字符串相等比较用户输入、状态值[ $a ! $b ]字符串不相等判断非预期状态[ $n -eq 10 ]数字等于比较数量[ $n -gt 10 ]数字大于阈值判断[ $n -lt 10 ]数字小于阈值判断注意看表格里的变量我都加了双引号。这不算谨慎过度而是必需操作。空变量如果不加引号展开后相当于那对引号消失了。比如变量a是空的[ $a x ]展开后变成[ x ]Shell 会直接报unary operator expected语法错误。加上双引号后空变量会展开成一个空参数条件只是不成立而已不会让脚本崩掉。1.3 [ ] 和 [[ ]] 有什么区别Bash 里还有一种[[ ]]写法它和[ ]在外观上很像但本质不同[[ ]]不是命令而是 Bash 的关键字。它支持的语法更丰富比如内部可以直接用、||可以用做通配符匹配还能用~做正则匹配。写成if [[ $a *.txt ]]不需要加引号也不会因为空变量崩掉体验确实好很多。但代价是兼容性。[[ ]]是 Bash、Zsh、Ksh 的扩展语法严格遵循 POSIX 的/bin/sh不认它。Debian/Ubuntu 上/bin/sh实际是 dash很多服务器脚本开头写#!/bin/sh里面却用了[[ ]]一到目标机器就报语法错误。所以我建议脚本开头是#!/usr/bin/env bash用[[ ]]没问题。脚本需要在多种精简系统、容器镜像里跑或者必须用sh执行那就老老实实用[ ]。需要做正则或通配符匹配的场景优先确保脚本跑在 Bash 环境用[[ ]]最省事。2. if-else 不只有一种形态简单到复杂按场景选2.1 单分支、双分支、多分支的完整结构最基础的三段式结构是if [ $1 start ]; then echo 启动 elif [ $1 stop ]; then echo 停止 else echo 未知参数 fi注意三件事then跟前一行同一行时前一行末尾必须有分号elif不是缩进美化而是“else if”的合并写法整个 if 块必须以fi结束。新手经常犯的一个错误是想用else if而不是elif。else if也能写但它等于在 else 分支里嵌套了一层新 if需要额外补一个fi很容易搞混层级。如果你的分支超过三个用elif永远比手动嵌套清晰。判断参数数量和变量是否为空是另一个高频场景。比如一个脚本要求至少传两个参数if [ $# -lt 2 ]; then echo 用法: $0 主机名 环境名 2 exit 1 fi if [ -z $2 ]; then echo 环境名不能为空 2 exit 1 fi$#表示参数个数-lt 2表示小于 2。把错误信息输出到标准错误2并设置非 0 退出码是脚本规范里很重要的习惯后续用脚本做自动化时这个退出码会成为上层调度程序判断成败的依据。2.2 命令组合和取反逻辑Shell 里的 if 条件可以叠加逻辑运算符。POSIX 环境里最稳的组合方式是把多个[ ]用或||连起来if [ -f $config ] [ -r $config ]; then echo 配置文件存在且可读 fi if [ ! -d $backup_dir ]; then echo 备份目录不存在立即创建 mkdir -p $backup_dir fi注意[ ]内部不能直接写比如[ -f $config -r $config ]是错的因为[是个命令它不认识这种参数组合。旧脚本里有人用-a和-o表示与和或例如[ -f $config -a -r $config ]这种写法在部分环境下还能工作但可移植性不好建议抛弃。Bash 的[[ ]]则没有这个限制可以直接写if [[ -f $config -r $config ]]; then echo 配置文件存在且可读 fi!可以放在 if 后面或测试条件前面取反。if ! grep -q error /var/log/app.log; then echo 日志干净没有 error fi这里同样不要写成先 grep 再判断$?的形式没必要给自己留一个状态被覆盖的隐患。2.3 用算术判断替代一堆文本比较Bash 里(( ))做算术求值非常顺手它同样可以作为 if 的条件。(( ))里的表达式结果如果是 0退出码是 1条件不成立结果不是 0退出码是 0条件成立。num10 if (( num % 2 0 )); then echo $num 是偶数 fi系统磁盘、内存这类带阈值的判断用算术表达式写起来很直观usage$(df /data | awk NR2 {print $5} | tr -d %) if (( usage 80 )); then echo 磁盘使用率已达 ${usage}%注意清理 else echo 磁盘状态正常当前使用率 ${usage}% fi这比写一长串[ $usage -gt 80 ]可读性好。注意不要在不
返回列表