十、Linux Shell脚本:流程控制语句
作者:IvanCodes
日期:2025年8月10日
专栏:Linux教程
在掌握了Shell脚本的变量与运算之后,流程控制是构建复杂和实用脚本的关键。它允许脚本根据不同的条件来选择执行路径,或重复执行特定任务,从而实现脚本的灵活性与自动化。
思维导图
一、条件判断
if
语句是最基本的条件控制结构,它评估一个命令的退出状态码 (exit code)。如果退出码为 0 (成功),则条件为真;如果为非 0 (失败),则条件为假。
if 的基本结构
格式:
if [ 条件判断 ]; then # 条件为真时执行的代码块fi
代码示例:检查文件是否存在
#!/bin/bashTARGET_FILE=\"/etc/hosts\"if [ -f \"$TARGET_FILE\" ]; then echo \"文件 \'$TARGET_FILE\' 存在。\"fi
if…else 结构
格式:
if [ 条件判断 ]; then # 条件为真时执行的代码块else # 条件为假时执行的代码块fi
代码示例:判断目录是否存在
#!/bin/bashTARGET_DIR=\"/var/log/non_existent_dir\"if [ -d \"$TARGET_DIR\" ]; then echo \"目录 \'$TARGET_DIR\' 存在。\"else echo \"目录 \'$TARGET_DIR\' 不存在,将尝试创建。\" mkdir -p \"$TARGET_DIR\"fi
if…elif…else 结构
格式:
if [ 条件1 ]; then # 条件1为真时执行elif [ 条件2 ]; then # 条件1为假,但条件2为真时执行else # 以上所有条件都为假时执行fi
代码示例:根据HTTP状态码判断响应
#!/bin/bashHTTP_CODE=200if [ $HTTP_CODE -eq 200 ]; then echo \"请求成功 (OK)\"elif [ $HTTP_CODE -eq 404 ]; then echo \"资源未找到 (Not Found)\"elif [ $HTTP_CODE -eq 500 ]; then echo \"服务器内部错误 (Internal Server Error)\"else echo \"收到未知的HTTP状态码: $HTTP_CODE\"fi
条件判断的实现:test 和 [ ]
在Shell中,if
后的条件通常由 test
命令或其等价形式 [ ... ]
来实现。[[ ... ]]
是 [ ... ]
的扩展版本,提供了更多功能 (如模式匹配、逻辑与/或)。
常见判断类型:
文件测试:
-f
(是普通文件?),-d
(是目录?),-e
(存在?),-s
(大小非0?),-r
(可读?),-w
(可写?),-x
(可执行?)
字符串比较:\"$str1\" = \"$str2\"
,\"$str1\" != \"$str2\"
,-z \"$str\"
(字符串为空?),-n \"$str\"
(字符串非空?)
整数比较:-eq
(等于),-ne
(不等于),-gt
(大于),-ge
(大于等于),-lt
(小于),-le
(小于等于)
二、循环结构
循环用于重复执行一段代码,直到满足某个退出条件。
for 循环
for
循环擅长遍历一个列表 (字符串、文件名、数字序列等) 或进行C语言风格的数值循环。
格式 (遍历列表):
for variable_name in item1 item2 item3 ...; do # 循环体done
代码示例 (遍历并重命名文件):
#!/bin/bash# 将所有 .txt 文件重命名为 .txt.bakfor filename in *.txt; do if [ -f \"$filename\" ]; then echo \"正在备份: $filename -> ${filename}.bak\" mv \"$filename\" \"${filename}.bak\" fidone
格式 (C风格数值循环):
for (( initialization; condition; step )); do # 循环体done
代码示例 (执行三次ping测试):
#!/bin/bashTARGET_HOST=\"8.8.8.8\"for (( i=1; i<=3; i++ )); do echo \"--- 第 $i 次 PING 测试 ---\" ping -c 1 \"$TARGET_HOST\"done
while 循环
while
循环在每次迭代前检查条件,只要条件为真,就继续执行循环体。
格式:
while [ 条件判断 ]; do # 循环体done
代码示例:逐行读取文件
#!/bin/bashCONFIG_FILE=\"/etc/fstab\"while read -r line; do # 忽略注释和空行 if [[ \"$line\" =~ ^# || -z \"$line\" ]]; then continue fi echo \"读取到配置行: $line\"done < \"$CONFIG_FILE\"
until 循环
until
循环与 while
逻辑相反:只要条件为假,就继续执行循环体,直到条件变为真才停止。
格式:
until [ 条件判断 ]; do # 循环体done
代码示例:等待服务端口启动
#!/bin/bashPORT=8080TIMEOUT=10COUNT=0until nc -z localhost $PORT >/dev/null 2>&1; do if [ $COUNT -ge $TIMEOUT ]; then echo \"等待端口 $PORT 超时!\" exit 1 fi echo \"端口 $PORT 尚未启动,等待1秒...\" sleep 1 COUNT=$((COUNT + 1))doneecho \"端口 $PORT 已成功启动!\"
循环控制:break 和 continue
break
: 立即从当前循环中完全跳出。continue
: 跳过当前循环的剩余部分,直接开始下一次迭代。
代码示例:在循环中处理文件
#!/bin/bashfor file in /var/log/*; do if [ -d \"$file\" ]; then continue # 如果是目录,则跳过 fi echo \"正在处理文件: $file\" if [ -s \"$file\" ] && grep -q \"ERROR\" \"$file\"; then echo \"在文件 \'$file\' 中找到错误,停止处理。\" break # 找到错误后,完全停止 fidone
三、分支选择
case
语句提供了一种更清晰的方式来处理多重条件分支,是 if...elif...else
的一种替代方案,特别适合基于单个变量的值进行匹配。
格式:
case $variable in pattern1) # 匹配 pattern1 时执行 ;; pattern2|pattern3) # 匹配 pattern2 或 pattern3 时执行 ;; *) # 默认情况,当以上模式都不匹配时执行 ;;esac
代码示例:脚本参数解析
#!/bin/bashACTION=$1case $ACTION in start) echo \"正在启动服务...\" # systemctl start my_service ;; stop) echo \"正在停止服务...\" # systemctl stop my_service ;; status) echo \"检查服务状态...\" # systemctl status my_service ;; *) echo \"用法: $0 {start|stop|status}\" exit 1 ;;esac
练习题
题目:
- 文件权限检查:写一个脚本,接收一个文件名作为参数 (
$1
)。脚本需要判断当前用户对该文件是否同时拥有读、写、执行权限。如果同时拥有,打印 “Full permissions granted”;否则打印 “Permissions incomplete”。 - 字符串与逻辑判断:写一个脚本,检查变量
ENVIRONMENT
的值。如果值是production
并且 变量FORCE_DEPLOY
的值不是true
,则打印 “Safety check passed: Not a forced production deploy.” 并退出;否则,打印 “Proceeding with deployment.”。 - C风格
for
循环与算术:使用C风格的for
循环,打印出从10到20之间所有的偶数 (包括10和20)。 for
循环与通配符:写一个脚本,查找/var/log
目录下所有以.log
结尾的非空文件,并打印出它们的文件名。while
循环读取标准输入:写一个脚本,持续读取用户从键盘输入的内容,直到用户输入quit
为止。对于非quit
的输入,脚本应该将其回显到屏幕上。until
循环与命令退出码:grep
命令在找到匹配项时退出码为0,找不到时为1。写一个until
循环,每隔2秒检查一次系统日志 (/var/log/messages
或journalctl -f
的输出,为简化可检查一个普通文件) 是否出现了 “critical error” 字符串,一旦出现就打印 “Critical error detected!” 并退出。- 嵌套循环与
break n
:写一个嵌套循环。外层循环从1到3,内层循环从1到3。在内层循环中,如果内外两个循环变量 (i
和j
) 相等,则同时跳出内外两层循环。每次循环都打印当前的i
和j
的值。 case
语句与通配符:写一个case
语句,判断一个文件名变量FILENAME
的文件类型。如果文件名以.log
结尾,打印 “Log file”;如果以.tar.gz
或.tgz
结尾,打印 “Compressed archive”;如果以.sh
结尾,打印 “Shell script”;其他情况打印 “Unknown file type”。select
菜单 (高级):select
是一个特殊的循环结构,用于创建交互式菜单。写一个脚本,使用select
让用户从 “Start”, “Stop”, “Restart”, “Exit” 四个选项中选择一个操作,并根据用户的选择打印相应的信息。当用户选择 “Exit” 时,脚本退出。
答案与解析:
- 文件权限检查:
#!/bin/bashif [ -z \"$1\" ]; then echo \"用法: $0 \" exit 1fiif [ -r \"$1\" ] && [ -w \"$1\" ] && [ -x \"$1\" ]; then echo \"Full permissions granted\"else echo \"Permissions incomplete\"fi
- 解析:
if
语句中的-r
,-w
,-x
是文件测试操作符,分别检查读、写、执行权限。&&
是逻辑与操作符,要求所有条件都为真才执行then
块。
- 字符串与逻辑判断:
#!/bin/bashENVIRONMENT=\"production\"FORCE_DEPLOY=\"false\"if [[ \"$ENVIRONMENT\" == \"production\" && \"$FORCE_DEPLOY\" != \"true\" ]]; then echo \"Safety check passed: Not a forced production deploy.\" exit 0else echo \"Proceeding with deployment.\"fi
- 解析: 使用了
[[ ... ]]
扩展测试,它内部支持&&
(逻辑与) 和!=
(字符串不等于) 操作符,语法更自然。
- C风格
for
循环与算术:
#!/bin/bashfor (( num=10; num<=20; num+=2 )); do echo $numdone
- 解析: C风格的
for
循环通过初始化num=10
,条件num<=20
,以及步进num+=2
来精确控制循环,直接打印出范围内的偶数。
for
循环与通配符:
#!/bin/bashfor logfile in /var/log/*.log; do if [ -s \"$logfile\" ]; then echo \"找到非空日志文件: $(basename \"$logfile\")\" fidone
- 解析:
*.log
是一个通配符,for
循环会遍历所有匹配的文件名。-s
文件测试操作符用于判断文件大小是否大于零。basename
命令用于提取文件名,去除路径。
while
循环读取标准输入:
#!/bin/bashecho \"请输入内容 (输入 \'quit\' 退出):\"while read -r input_line; do if [ \"$input_line\" == \"quit\" ]; then break fi echo \"你输入了: $input_line\"done
- 解析:
while read -r input_line
是读取标准输入的标准模式。循环会一直持续,直到read
命令失败 (例如,用户按下Ctrl+D) 或遇到break
。
until
循环与命令退出码:
#!/bin/bashLOG_FILE_TO_CHECK=\"my_app.log\"touch $LOG_FILE_TO_CHECK # 创建一个空文件用于测试echo \"正在监控 \'$LOG_FILE_TO_CHECK\' ...\"# 在另一个终端执行 echo \"critical error\" >> my_app.log 来触发until grep -q \"critical error\" \"$LOG_FILE_TO_CHECK\"; do sleep 2doneecho \"Critical error detected!\"
- 解析:
until
循环的条件是命令本身 (grep -q ...
)。只要grep
找不到字符串 (退出码非0,条件为假),循环就继续。一旦找到 (退出码为0,条件为真),循环终止。
- 嵌套循环与
break n
:
#!/bin/bashfor (( i=1; i<=3; i++ )); do echo \"外层循环: i=$i\" for (( j=1; j<=3; j++ )); do echo \" 内层循环: j=$j\" if [ $i -eq $j ]; then echo \" i 等于 j,跳出所有循环!\" break 2 # \'2\' 表示跳出两层循环 fi donedone
- 解析:
break n
命令可以跳出指定层数的循环。break 1
(或break
) 只跳出当前层,break 2
跳出当前层和其外一层。
case
语句与通配符:
#!/bin/bashFILENAME=\"archive-2023.tar.gz\"case $FILENAME in *.log) echo \"Log file\" ;; *.tar.gz|*.tgz) echo \"Compressed archive\" ;; *.sh) echo \"Shell script\" ;; *) echo \"Unknown file type\" ;;esac
- 解析:
case
语句的模式支持通配符,如*
(匹配任意字符序列)。|
用于分隔多个模式,表示“或”。
select
菜单:
#!/bin/bashPS3=\"请选择一个操作 (输入数字): \"options=(\"Start\" \"Stop\" \"Restart\" \"Exit\")select opt in \"${options[@]}\"; do case $opt in \"Start\") echo \"正在启动...\" ;; \"Stop\") echo \"正在停止...\" ;; \"Restart\") echo \"正在重启...\" ;; \"Exit\") echo \"退出脚本。\" break ;; *) echo \"无效选项 \'$REPLY\',请重新选择。\" ;; esacdone
- 解析:
select
会自动生成一个带编号的菜单。用户的输入编号会被翻译成对应的选项值 (赋给变量opt
),而原始输入则保存在$REPLY
中。