在掌握了Shell脚本的
变量与运算之后,
流程控制是
构建复杂和实用脚本的
关键。它
允许脚本根据
不同的条件来
选择执行路径,或
重复执行特定任务,从而
实现脚本的
灵活性与自动化。
## 思维导图
## 一、条件判断
### if 的基本结构
格式:
代码示例:检查文件是否存在
### if...else 结构
格式:
代码示例:判断目录是否存在
### if...elif...else 结构
格式:
代码示例:根据HTTP状态码判断响应
### 条件判断的实现:test 和 [ ]
在Shell中,
常见判断类型:
> 文件测试:
字符串比较:
整数比较:
## 二、循环结构
循环用于 重复执行一段代码,直到 满足某个退出条件。
### for 循环
格式 (遍历列表):
代码示例 (遍历并重命名文件):
格式 (C风格数值循环):
代码示例 (执行三次ping测试):
### while 循环
格式:
代码示例:逐行读取文件
### until 循环
格式:
代码示例:等待服务端口启动
### 循环控制:break 和 continue
代码示例:在循环中处理文件
## 三、分支选择
格式:
代码示例:脚本参数解析
---
## 练习题
## 思维导图
## 一、条件判断
if 语句是
最基本的
条件控制结构,它
评估一个命令的
退出状态码 (exit code)。如果
退出码为 0 (成功),则
条件为真;如果为
非 0 (失败),则
条件为假。
### if 的基本结构
格式:
bash if [ 条件判断 ]; then # 条件为真时执行的代码块 fi
代码示例:检查文件是否存在
bash #!/bin/bash TARGET_FILE="/etc/hosts" if [ -f "$TARGET_FILE" ]; then echo "文件 '$TARGET_FILE' 存在。" fi
### if...else 结构
格式:
bash if [ 条件判断 ]; then # 条件为真时执行的代码块 else # 条件为假时执行的代码块 fi
代码示例:判断目录是否存在
bash #!/bin/bash TARGET_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 结构
格式:
bash if [ 条件1 ]; then # 条件1为真时执行 elif [ 条件2 ]; then # 条件1为假,但条件2为真时执行 else # 以上所有条件都为假时执行 fi
代码示例:根据HTTP状态码判断响应
bash #!/bin/bash HTTP_CODE=200 if [ $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语言风格的
数值循环。
格式 (遍历列表):
bash for variable_name in item1 item2 item3 ...; do # 循环体 done
代码示例 (遍历并重命名文件):
bash #!/bin/bash # 将所有 .txt 文件重命名为 .txt.bak for filename in *.txt; do if [ -f "$filename" ]; then echo "正在备份: $filename -> ${filename}.bak" mv "$filename" "${filename}.bak" fi done
格式 (C风格数值循环):
bash for (( initialization; condition; step )); do # 循环体 done
代码示例 (执行三次ping测试):
bash #!/bin/bash TARGET_HOST="8.8.8.8" for (( i=1; i<=3; i++ )); do echo "--- 第 $i 次 PING 测试 ---" ping -c 1 "$TARGET_HOST" done
### while 循环
while 循环在
每次迭代前检查条件,只要
条件为真,就
继续执行循环体。
格式:
bash while [ 条件判断 ]; do # 循环体 done
代码示例:逐行读取文件
bash #!/bin/bash CONFIG_FILE="/etc/fstab" while read -r line; do # 忽略注释和空行 if [[ "$line" =~ ^# || -z "$line" ]]; then continue fi echo "读取到配置行: $line" done < "$CONFIG_FILE"
### until 循环
until 循环与
while
逻辑相反:只要
条件为假,就
继续执行循环体,直到
条件变为真才停止。
格式:
bash until [ 条件判断 ]; do # 循环体 done
代码示例:等待服务端口启动
bash #!/bin/bash PORT=8080 TIMEOUT=10 COUNT=0 until 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)) done echo "端口 $PORT 已成功启动!"
### 循环控制:break 和 continue
break: 立即从当前循环中完全跳出。
continue:
跳过当前循环的
剩余部分,直接
开始下一次迭代。
代码示例:在循环中处理文件
bash #!/bin/bash for file in /var/log/*; do if [ -d "$file" ]; then continue # 如果是目录,则跳过 fi echo "正在处理文件: $file" if [ -s "$file" ] && grep -q "ERROR" "$file"; then echo "在文件 '$file' 中找到错误,停止处理。" break # 找到错误后,完全停止 fi done
## 三、分支选择
case 语句提供了一种
更清晰的方式来
处理多重条件分支,是
if...elif...else 的
一种替代方案,特别适合
基于单个变量的
值进行匹配。
格式:
bash case $variable in pattern1) # 匹配 pattern1 时执行 ;; pattern2|pattern3) # 匹配 pattern2 或 pattern3 时执行 ;; *) # 默认情况,当以上模式都不匹配时执行 ;; esac
代码示例:脚本参数解析
bash #!/bin/bash ACTION=$1 case $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" 时,脚本退出。
- 文件权限检查:
```bash!/bin/bash
if [ -z "$1" ]; then
echo "用法: $0 <文件名>"
exit 1
fi
if [ -r "$1" ] && [ -w "$1" ] && [ -x "$1" ]; then
echo "Full permissions granted"
else
echo "Permissions incomplete"
fi
* **解析:** `if` 语句中的 `-r`, `-w`, `-x` 是文件测试操作符,分别检查读、写、执行权限。`&&` 是逻辑与操作符,要求<font color="teal">所有条件都为真</font>才执行 `then` 块。
2. **字符串与逻辑判断:**
```bash
#!/bin/bash
ENVIRONMENT="production"
FORCE_DEPLOY="false"
if [[ "$ENVIRONMENT" == "production" && "$FORCE_DEPLOY" != "true" ]]; then
echo "Safety check passed: Not a forced production deploy."
exit 0
else
echo "Proceeding with deployment."
fi
- 解析: 使用了
[[ ... ]]扩展测试,它内部支持&&(逻辑与) 和!=(字符串不等于) 操作符,语法更自然。
C风格
for循环与算术:#!/bin/bash for (( num=10; num<=20; num+=2 )); do echo $num done- 解析: C风格的
for循环通过初始化num=10,条件num<=20,以及步进num+=2来精确控制循环,直接打印出范围内的偶数。
- 解析: C风格的
for循环与通配符:#!/bin/bash for logfile in /var/log/*.log; do if [ -s "$logfile" ]; then echo "找到非空日志文件: $(basename "$logfile")" fi done- 解析:
*.log是一个通配符,for循环会遍历所有匹配的文件名。-s文件测试操作符用于判断文件大小是否大于零。basename命令用于提取文件名,去除路径。
- 解析:
while循环读取标准输入:#!/bin/bash echo "请输入内容 (输入 '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循环与命令退出码:
```bash!/bin/bash
LOG_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 2
done
echo "Critical error detected!"
* **解析:** `until` 循环的<font color="darkslategray">条件是命令本身</font> (`grep -q ...`)。只要 `grep` <font color="indigo">找不到</font>字符串 (退出码非0,条件为假),循环就<font color="blue">继续</font>。一旦<font color="red">找到</font> (退出码为0,条件为真),循环<font color="green">终止</font>。
7. **嵌套循环与 `break n`:**
```bash
#!/bin/bash
for (( 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
done
done
- 解析:
break n命令可以跳出指定层数的循环。break 1(或break) 只跳出当前层,break 2跳出当前层和其外一层。
case语句与通配符:
```bash!/bin/bash
FILENAME="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` 语句的模式<font color="purple">支持通配符</font>,如 `*` (匹配任意字符序列)。`|` 用于<font color="teal">分隔多个模式</font>,表示“或”。
9. **`select` 菜单:**
```bash
#!/bin/bash
PS3="请选择一个操作 (输入数字): "
options=("Start" "Stop" "Restart" "Exit")
select opt in "${options[@]}"; do
case $opt in
"Start")
echo "正在启动..."
;;
"Stop")
echo "正在停止..."
;;
"Restart")
echo "正在重启..."
;;
"Exit")
echo "退出脚本。"
break
;;
*)
echo "无效选项 '$REPLY',请重新选择。"
;;
esac
done
- 解析:
select会自动生成一个带编号的菜单。用户的输入编号会被翻译成对应的选项值 (赋给变量opt),而原始输入则保存在$REPLY中。