流程控制
流程控制是编程语言的核心特性,它决定了程序的执行顺序。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 的扩展测试语法,比 [ ] 更强大、更安全。
特点
- 支持模式匹配(通配符)
- 支持正则表达式
- 支持逻辑运算符
&&和|| - 变量不需要引号保护
- 不会进行词分割
模式匹配
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 提供了三种循环结构:for、while 和 until。
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语句及其模式匹配 - 循环语句:
for、while、until、select - 循环控制:
break、continue - 最佳实践:使用双引号、优先使用
[[ ]]、数值比较用(( ))
掌握流程控制是编写复杂脚本的基础。下一章将学习函数,了解如何组织和复用代码。