跳到主要内容

流程控制

流程控制是编程语言的核心特性,它决定了程序的执行顺序。Shell 提供了完整的流程控制语句,包括条件判断、循环语句和分支选择。本章详细介绍这些控制结构的语法和使用方法。

条件判断

条件判断允许脚本根据不同条件执行不同的代码分支。

if 语句

基本语法

if condition; then
commands
fi

if-else 语句

if condition; then
commands1
else
commands2
fi

if-elif-else 语句

if condition1; then
commands1
elif condition2; then
commands2
else
commands3
fi

示例

#!/bin/bash

age=18

if [ $age -ge 18 ]; then
echo "成年人"
elif [ $age -ge 12 ]; then
echo "青少年"
else
echo "儿童"
fi

test 命令与 [ ]

test 命令用于评估条件表达式,[test 的别名,] 是匹配的结束符。

数值比较

运算符含义
-eq等于
-ne不等于
-gt大于
-ge大于等于
-lt小于
-le小于等于
a=10
b=20

if [ $a -lt $b ]; then
echo "$a 小于 $b"
fi

if [ $a -eq 10 ]; then
echo "a 等于 10"
fi

字符串比较

运算符含义
=字符串相等
!=字符串不相等
-z字符串长度为 0
-n字符串长度不为 0
str1="hello"
str2="world"

if [ "$str1" = "$str2" ]; then
echo "字符串相等"
fi

if [ "$str1" != "$str2" ]; then
echo "字符串不相等"
fi

if [ -z "$str1" ]; then
echo "字符串为空"
fi

if [ -n "$str1" ]; then
echo "字符串非空"
fi

重要提示:字符串比较时,变量应该用双引号包围,防止变量为空时出现语法错误。

# 错误:如果 str 为空,变成 [ = "hello" ]
if [ $str = "hello" ]; then ... fi

# 正确:即使 str 为空,也是 [ "" = "hello" ]
if [ "$str" = "hello" ]; then ... fi

文件测试

运算符含义
-e文件存在
-f是普通文件
-d是目录
-r可读
-w可写
-x可执行
-s文件非空
-L是符号链接
-nt文件比另一个新
-ot文件比另一个旧
file="/etc/passwd"

if [ -e "$file" ]; then
echo "文件存在"
fi

if [ -f "$file" ]; then
echo "是普通文件"
fi

if [ -r "$file" ]; then
echo "文件可读"
fi

if [ -d "/tmp" ]; then
echo "是目录"
fi

# 比较文件修改时间
if [ "file1.txt" -nt "file2.txt" ]; then
echo "file1.txt 比 file2.txt 新"
fi

[[ ]] 高级条件测试

[[ ]] 是 Bash 的扩展测试语法,比 [ ] 更强大、更安全。

特点

  1. 支持模式匹配(通配符)
  2. 支持正则表达式
  3. 支持逻辑运算符 &&||
  4. 变量不需要引号保护
  5. 不会进行词分割

模式匹配

str="hello.txt"

# 通配符匹配
if [[ $str == *.txt ]]; then
echo "是 txt 文件"
fi

if [[ $str == h* ]]; then
echo "以 h 开头"
fi

正则表达式

str="hello123"

# 正则匹配
if [[ $str =~ ^[a-z]+[0-9]+$ ]]; then
echo "匹配字母+数字格式"
fi

# 提取匹配结果
if [[ $str =~ ^([a-z]+)([0-9]+)$ ]]; then
echo "字母部分: ${BASH_REMATCH[1]}"
echo "数字部分: ${BASH_REMATCH[2]}"
fi

逻辑运算

# 在 [[ ]] 中可以直接使用 && 和 ||
if [[ -f "$file" && -r "$file" ]]; then
echo "文件存在且可读"
fi

if [[ -f "$file" || -d "$file" ]]; then
echo "是文件或目录"
fi

# 在 [ ] 中需要使用 -a 和 -o
if [ -f "$file" -a -r "$file" ]; then
echo "文件存在且可读"
fi

(( )) 算术条件测试

(( )) 用于算术运算和比较,支持 C 风格的操作符。

a=10
b=20

# 数值比较(使用 < > <= >= 等符号)
if (( a < b )); then
echo "$a 小于 $b"
fi

if (( a == 10 )); then
echo "a 等于 10"
fi

# 算术运算
if (( a + b > 25 )); then
echo "a + b 大于 25"
fi

# 自增
(( a++ ))
echo $a # 输出:11

# 条件赋值
(( a > 5 ? (result=1) : (result=0) ))
echo $result

逻辑运算符

[ ] 中的逻辑运算

# -a: 逻辑与
if [ $a -gt 0 -a $a -lt 10 ]; then
echo "a 在 0 到 10 之间"
fi

# -o: 逻辑或
if [ $a -lt 0 -o $a -gt 10 ]; then
echo "a 不在 0 到 10 之间"
fi

# !: 逻辑非
if [ ! -f "$file" ]; then
echo "文件不存在"
fi

[[ ]] 中的逻辑运算

# &&: 逻辑与
if [[ $a -gt 0 && $a -lt 10 ]]; then
echo "a 在 0 到 10 之间"
fi

# ||: 逻辑或
if [[ $a -lt 0 || $a -gt 10 ]]; then
echo "a 不在 0 到 10 之间"
fi

# !: 逻辑非
if [[ ! -f "$file" ]]; then
echo "文件不存在"
fi

命令连接实现逻辑

# && 实现逻辑与
[ -f "$file" ] && [ -r "$file" ] && echo "文件存在且可读"

# || 实现逻辑或
[ -f "$file" ] || echo "文件不存在"

# 组合使用
[ -f "$file" ] && [ -r "$file" ] || echo "文件不存在或不可读"

case 语句

case 语句用于多分支选择,类似于其他语言的 switch 语句。

基本语法

case expression in
pattern1)
commands1
;;
pattern2)
commands2
;;
*)
default_commands
;;
esac

简单示例

#!/bin/bash

day="Monday"

case $day in
Monday)
echo "星期一"
;;
Tuesday)
echo "星期二"
;;
Wednesday)
echo "星期三"
;;
*)
echo "其他日子"
;;
esac

模式匹配

case 支持多种模式匹配:

#!/bin/bash

read -p "输入一个字符: " char

case $char in
[a-z])
echo "小写字母"
;;
[A-Z])
echo "大写字母"
;;
[0-9])
echo "数字"
;;
a|e|i|o|u)
echo "元音字母"
;;
*)
echo "其他字符"
;;
esac

多模式匹配

#!/bin/bash

file="document.txt"

case $file in
*.txt|*.md)
echo "文本文件"
;;
*.jpg|*.png|*.gif)
echo "图片文件"
;;
*.sh)
echo "Shell 脚本"
;;
*)
echo "未知文件类型"
;;
esac

范围匹配

#!/bin/bash

score=85

case $score in
9[0-9]|100)
echo "优秀"
;;
8[0-9])
echo "良好"
;;
7[0-9])
echo "中等"
;;
6[0-9])
echo "及格"
;;
*)
echo "不及格"
;;
esac

贯穿执行

使用 ;& 可以让 case 继续执行下一个模式的命令:

#!/bin/bash

var="B"

case $var in
A)
echo "A"
;;& # 继续匹配下面的模式
B)
echo "B"
;;&
C)
echo "C"
;;
esac
# 输出:B C

循环语句

Shell 提供了三种循环结构:forwhileuntil

for 循环

列表遍历语法

for var in list; do
commands
done

示例

# 遍历列表
for fruit in apple banana orange; do
echo "水果: $fruit"
done

# 遍历数组
arr=(one two three)
for item in "${arr[@]}"; do
echo "元素: $item"
done

# 遍历文件
for file in *.txt; do
echo "处理文件: $file"
done

# 遍历命令输出
for user in $(cut -d: -f1 /etc/passwd); do
echo "用户: $user"
done

# 遍历数字范围
for i in {1..5}; do
echo "数字: $i"
done

# 使用 seq
for i in $(seq 1 2 10); do
echo "奇数: $i"
done

C 风格 for 循环

for ((init; condition; update)); do
commands
done
# 基本用法
for ((i=1; i<=5; i++)); do
echo "计数: $i"
done

# 多变量
for ((i=1, j=10; i<=5; i++, j--)); do
echo "i=$i, j=$j"
done

# 步长
for ((i=0; i<=10; i+=2)); do
echo "偶数: $i"
done

while 循环

while 循环在条件为真时重复执行。

基本语法

while condition; do
commands
done

示例

# 基本计数
count=1
while [ $count -le 5 ]; do
echo "计数: $count"
((count++))
done

# 读取文件
while IFS= read -r line; do
echo "行: $line"
done < file.txt

# 读取用户输入
while true; do
read -p "输入 quit 退出: " input
if [[ $input == "quit" ]]; then
break
fi
echo "你输入了: $input"
done

# 无限循环
while :; do
echo "按 Ctrl+C 退出"
sleep 1
done

until 循环

until 循环与 while 相反,在条件为假时重复执行(直到条件为真才停止)。

基本语法

until condition; do
commands
done

示例

# 等待文件创建
until [ -f /tmp/ready ]; do
echo "等待文件创建..."
sleep 1
done
echo "文件已创建"

# 计数
count=1
until [ $count -gt 5 ]; do
echo "计数: $count"
((count++))
done

循环控制

break 语句

break 用于跳出循环。

# 跳出单层循环
for i in {1..10}; do
if [ $i -eq 5 ]; then
break
fi
echo "i=$i"
done

# 跳出多层循环
for i in {1..3}; do
for j in {1..3}; do
if [ $i -eq 2 -a $j -eq 2 ]; then
break 2 # 跳出 2 层循环
fi
echo "i=$i, j=$j"
done
done

continue 语句

continue 用于跳过当前迭代,继续下一次循环。

# 跳过偶数
for i in {1..10}; do
if (( i % 2 == 0 )); then
continue
fi
echo "奇数: $i"
done

# 跳过多层循环的当前迭代
for i in {1..3}; do
for j in {1..3}; do
if [ $j -eq 2 ]; then
continue 2 # 跳到外层循环的下一次迭代
fi
echo "i=$i, j=$j"
done
done

select 循环

select 循环用于创建交互式菜单。

基本语法

select var in list; do
commands
done

示例

#!/bin/bash

PS3="请选择一个选项: "

select option in "选项一" "选项二" "选项三" "退出"; do
case $option in
"选项一")
echo "你选择了选项一"
;;
"选项二")
echo "你选择了选项二"
;;
"选项三")
echo "你选择了选项三"
;;
"退出")
echo "再见"
break
;;
*)
echo "无效选项: $REPLY"
;;
esac
done

执行效果:

1) 选项一
2) 选项二
3) 选项三
4) 退出
请选择一个选项: 1
你选择了选项一
请选择一个选项: 4
再见

循环中的 IFS

IFS(Internal Field Separator)控制 Shell 如何分割字符串。在循环中修改 IFS 可以改变遍历行为。

# 默认 IFS 包含空格、制表符、换行符
# 遍历时会按这些字符分割

# 修改 IFS 按行遍历
IFS=$'\n'
for line in $(cat file.txt); do
echo "行: $line"
done

# 修改 IFS 按逗号分割
csv="a,b,c,d"
IFS=','
for item in $csv; do
echo "项: $item"
done

# 保存并恢复 IFS
OLDIFS=$IFS
IFS=','
# ... 操作 ...
IFS=$OLDIFS

条件表达式最佳实践

使用双引号保护变量

# 错误:变量为空时会出错
if [ $name = "John" ]; then ... fi

# 正确:变量用双引号包围
if [ "$name" = "John" ]; then ... fi

优先使用 [[ ]] 而不是 [ ]

# 推荐:更安全、功能更强
if [[ $str == *.txt ]]; then ... fi

# 不推荐:需要转义、功能有限
if [ "$str" = *.txt ]; then ... fi

数值比较使用 (( ))

# 推荐:更直观
if (( a > b )); then ... fi

# 不推荐:需要记忆特殊运算符
if [ $a -gt $b ]; then ... fi

复杂条件使用函数封装

is_valid_file() {
[[ -f "$1" && -r "$1" && -s "$1" ]]
}

if is_valid_file "$file"; then
echo "文件有效"
fi

实用示例

检查命令是否存在

check_command() {
if ! command -v "$1" &> /dev/null; then
echo "错误: $1 未安装"
exit 1
fi
}

check_command "git"
check_command "docker"

遍历目录处理文件

#!/bin/bash

for dir in */; do
dir=${dir%/} # 移除末尾斜杠
if [[ -d "$dir" ]]; then
echo "处理目录: $dir"
for file in "$dir"/*.txt; do
if [[ -f "$file" ]]; then
echo " 文件: $file"
fi
done
fi
done

监控进程

#!/bin/bash

process_name="nginx"

while true; do
if ! pgrep -x "$process_name" > /dev/null; then
echo "$(date): $process_name 未运行,正在启动..."
systemctl start "$process_name"
fi
sleep 60
done

批量重命名文件

#!/bin/bash

for file in *.txt; do
if [[ -f "$file" ]]; then
new_name="${file%.txt}.bak"
mv "$file" "$new_name"
echo "重命名: $file -> $new_name"
fi
done

参数解析

#!/bin/bash

while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
echo "用法: $0 [-h] [-f file] [-v]"
exit 0
;;
-f|--file)
file="$2"
shift 2
;;
-v|--verbose)
verbose=1
shift
;;
*)
echo "未知选项: $1"
exit 1
;;
esac
done

echo "文件: ${file:-未指定}"
echo "详细模式: ${verbose:-否}"

小结

本章介绍了 Shell 脚本的流程控制:

  • 条件判断if 语句、test 命令、[ ][[ ]](( ))
  • 分支选择case 语句及其模式匹配
  • 循环语句forwhileuntilselect
  • 循环控制breakcontinue
  • 最佳实践:使用双引号、优先使用 [[ ]]、数值比较用 (( ))

掌握流程控制是编写复杂脚本的基础。下一章将学习函数,了解如何组织和复用代码。