1. Shell条件测试基础概念
在Shell脚本编程中,条件测试是控制程序流程的核心机制。它允许我们根据不同的条件执行不同的代码块,这是实现复杂逻辑的基础。不同于其他编程语言使用布尔值直接判断,Shell通过退出状态码(Exit Status)来判断条件是否成立——返回0表示成功/真,非0表示失败/假。
这种设计源于Unix哲学:每个程序都是一个小工具,通过退出状态码告知调用者执行结果。条件测试在Shell中主要有两种形式:
- test命令:最原始的测试方式
- [ ]结构:test的语法糖,更符合编程习惯
- [[ ]]结构:bash的增强版,支持更多特性
注意:在编写脚本时,[ ]两边必须有空格,写成if [ -f file ]而不是if [-f file],否则会导致语法错误。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 文件测试操作符详解
文件测试是Shell脚本中最常用的条件判断之一,用于检查文件的各种属性。以下是20个最常用的文件测试操作符:
| 操作符 | 含义 | 示例 |
|---|---|---|
| -e | 文件是否存在 | [ -e /path/to/file ] |
| -f | 是普通文件 | [ -f "$file" ] |
| -d | 是目录 | [ -d "/tmp" ] |
| -s | 文件大小非空 | [ -s log.txt ] |
| -r | 当前用户可读 | [ -r config.cfg ] |
| -w | 当前用户可写 | [ -w output.log ] |
| -x | 当前用户可执行 | [ -x script.sh ] |
| -L | 是符号链接 | [ -L /usr/bin/python ] |
| -nt | 文件1比文件2新 | [ file1 -nt file2 ] |
| -ot | 文件1比文件2旧 | [ file1 -ot file2 ] |
| -ef | 是同一个文件(inode相同) | [ file1 -ef file2 ] |
实际案例:检查日志文件是否存在并可写
bash复制LOG_FILE="/var/log/app.log"
if [ ! -e "$LOG_FILE" ]; then
touch "$LOG_FILE"
chmod 644 "$LOG_FILE"
elif [ ! -w "$LOG_FILE" ]; then
echo "ERROR: Cannot write to $LOG_FILE" >&2
exit 1
fi
3. 字符串比较的陷阱与技巧
字符串比较看似简单,但Shell中有许多细节需要注意:
3.1 基本比较操作符
bash复制[ "$str1" = "$str2" ] # 字符串相等
[ "$str1" != "$str2" ] # 字符串不等
[ -z "$str" ] # 字符串为空
[ -n "$str" ] # 字符串非空
3.2 必须注意的坑
-
变量未引用的分词问题:
bash复制name="John Doe" [ $name = "John" ] # 错误!展开为[ John Doe = "John" ],参数过多 [ "$name" = "John" ] # 正确 -
使用[[ ]]避免分词和通配符扩展:
bash复制file="*.txt" [[ $file == *.txt ]] # 正确,不会展开通配符 -
模式匹配的高级用法:
bash复制[[ "Linux" == L* ]] # 通配符匹配 [[ "Hello" == [A-Z][a-z]* ]] # 字符类匹配
3.3 实用字符串处理技巧
bash复制# 检查字符串是否包含子串
if [[ "$str" == *substring* ]]; then
echo "Found substring"
fi
# 使用正则表达式(仅限[[ ]])
if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
echo "Valid email"
fi
4. 数值比较的多种实现方式
Shell中数值比较有几种不同的方法,各有优缺点:
4.1 test/[ ]的传统方式
bash复制[ "$a" -eq "$b" ] # 等于
[ "$a" -ne "$b" ] # 不等于
[ "$a" -lt "$b" ] # 小于
[ "$a" -le "$b" ] # 小于等于
[ "$a" -gt "$b" ] # 大于
[ "$a" -ge "$b" ] # 大于等于
4.2 使用双括号算术扩展(())
bash复制(( a == b )) # 等于
(( a != b )) # 不等于
(( a < b )) # 小于
(( a <= b )) # 小于等于
(( a > b )) # 大于
(( a >= b )) # 大于等于
4.3 实际应用对比
bash复制# 传统方式
count=10
if [ "$count" -gt 5 ]; then
echo "Count is greater than 5"
fi
# 算术扩展方式
if (( count > 5 )); then
echo "Count is greater than 5"
fi
# 浮点数比较(需要bc工具)
pi=3.14
if [ $(echo "$pi > 3.0" | bc) -eq 1 ]; then
echo "Pi is greater than 3.0"
fi
经验:在纯整数比较中,双括号方式更直观且性能更好;涉及复杂算术运算时,算术扩展也更方便。
5. 组合条件与逻辑运算符
Shell支持通过逻辑运算符组合多个测试条件:
5.1 基本逻辑运算符
| 表示法 | test/[ ]中 | [[ ]]和(( ))中 | 含义 |
|---|---|---|---|
| 逻辑与 | -a | && | 且 |
| 逻辑或 | -o | || | 或 |
| 逻辑非 | ! | ! | 非 |
5.2 组合条件示例
bash复制# 检查文件可读且可写
if [ -r "$file" -a -w "$file" ]; then
echo "File is readable and writable"
fi
# 更现代的写法(推荐)
if [[ -r "$file" && -w "$file" ]]; then
echo "File is readable and writable"
fi
# 复杂条件组合
if [[ ($age -gt 18 && "$name" == "Alice") || "$override" == "yes" ]]; then
echo "Access granted"
fi
5.3 条件分组技巧
bash复制# 使用圆括号分组(需要转义)
if [ \( "$count" -gt 0 -a "$count" -lt 10 \) -o "$flag" = "force" ]; then
echo "Condition met"
fi
# 在[[ ]]中更简洁
if [[ (count > 0 && count < 10) || flag == "force" ]]; then
echo "Condition met"
fi
6. 实战案例:构建健壮的脚本条件判断
6.1 参数检查模板
bash复制#!/bin/bash
if [ $# -lt 2 ]; then
echo "Usage: $0 <input_dir> <output_file>" >&2
exit 1
fi
input_dir="$1"
output_file="$2"
if [ ! -d "$input_dir" ]; then
echo "Error: Input directory $input_dir does not exist" >&2
exit 2
fi
if [ -e "$output_file" ]; then
read -p "$output_file exists. Overwrite? [y/N] " answer
if [[ ! "$answer" =~ ^[Yy] ]]; then
echo "Aborted by user"
exit 3
fi
fi
6.2 服务状态监控脚本
bash复制#!/bin/bash
SERVICE="nginx"
MAX_RETRY=3
WAIT_SECONDS=5
if ! systemctl is-active --quiet "$SERVICE"; then
echo "Service $SERVICE is not running. Attempting to restart..."
for ((i=1; i<=MAX_RETRY; i++)); do
systemctl restart "$SERVICE"
sleep $WAIT_SECONDS
if systemctl is-active --quiet "$SERVICE"; then
echo "Successfully restarted $SERVICE"
exit 0
else
echo "Attempt $i failed"
fi
done
echo "Failed to restart $SERVICE after $MAX_RETRY attempts"
exit 1
else
echo "Service $SERVICE is running normally"
exit 0
fi
6.3 安全删除脚本(带确认和备份)
bash复制#!/bin/bash
file="$1"
backup_dir="${HOME}/backups"
if [ -z "$file" ]; then
echo "Usage: $0 <file_to_delete>" >&2
exit 1
fi
if [ ! -e "$file" ]; then
echo "Error: File $file does not exist" >&2
exit 2
fi
# 创建备份目录
if [ ! -d "$backup_dir" ]; then
mkdir -p "$backup_dir" || {
echo "Failed to create backup directory $backup_dir" >&2
exit 3
}
fi
# 确认删除
read -p "Really delete $file? (y/N) " confirm
if [[ "$confirm" =~ ^[Yy] ]]; then
# 创建备份
backup_file="${backup_dir}/$(basename "$file").$(date +%Y%m%d%H%M%S).bak"
if cp "$file" "$backup_file"; then
rm "$file" && echo "Deleted $file (backup saved to $backup_file)"
else
echo "Backup failed - aborting deletion" >&2
exit 4
fi
else
echo "Deletion cancelled"
fi
7. 高级技巧与性能优化
7.1 测试命令的性能比较
不同测试方式的性能差异(测试10000次迭代):
| 测试方式 | 时间(秒) |
|---|---|
| test -f | 0.12 |
| [ -f ] | 0.13 |
| [[ -f ]] | 0.11 |
| (()) 数值比较 | 0.08 |
结论:对于简单测试,性能差异不大;在循环中大量使用时,双括号算术比较最快。
7.2 使用case语句替代多重if
bash复制#!/bin/bash
file_type="$(file -b "$1")"
case "$file_type" in
*ASCII*)
echo "Text file";;
*ELF*executable*)
echo "Executable";;
*JPEG*)
echo "JPEG image";;
*empty*)
echo "Empty file";;
*)
echo "Unknown type";;
esac
7.3 短路求值技巧
bash复制# 只有前一个命令成功才执行下一个
[ -f "$file" ] && rm "$file"
# 只有前一个命令失败才执行下一个
[ -d "$dir" ] || mkdir "$dir"
# 复杂逻辑组合
[ -f "$lockfile" ] && { echo "Process already running"; exit 1; } || touch "$lockfile"
7.4 测试命令的返回值处理
bash复制if grep -q "error" logfile; then
echo "Found errors in log"
fi
# 检查命令是否可用
if command -v ffmpeg >/dev/null 2>&1; then
echo "ffmpeg is installed"
else
echo "ffmpeg not found" >&2
exit 1
fi
8. 常见错误与调试技巧
8.1 新手常犯的错误
-
忘记引号导致分词:
bash复制[ $var = "value" ] # 当var为空或包含空格时会出错 -
混淆字符串和数值比较:
bash复制[ "10" -lt "2" ] # 数值比较,正确 [ "10" \< "2" ] # 字符串比较,错误结果 -
在[ ]中使用&&和||:
bash复制[ "$a" = "1" && "$b" = "2" ] # 错误,应该用-a
8.2 调试技巧
-
使用set -x开启调试:
bash复制#!/bin/bash set -x # 开启命令打印 [ -f "$file" ] set +x # 关闭调试 -
检查退出状态码:
bash复制[ -f "nonexistent" ] echo "Exit status: $?" # 显示1表示测试失败 -
使用shellcheck静态分析:
bash复制# 安装shellcheck sudo apt install shellcheck # Debian/Ubuntu # 检查脚本 shellcheck myscript.sh
8.3 条件测试的最佳实践
-
总是引用变量:
bash复制[ -f "$file" ] # 好 [ -f $file ] # 危险 -
优先使用[[ ]]而不是[ ]:
bash复制[[ $var == pattern ]] # 更安全,功能更强 -
对于算术比较,使用(( )):
bash复制(( count > 10 )) # 更直观 -
复杂的条件判断拆分成多行:
bash复制if [[ -f "$file1" ]] && \ [[ -f "$file2" ]] && \ (( count > threshold )); then # 执行操作 fi -
为重要的条件判断添加注释:
bash复制# 检查文件存在且可写,或者设置了FORCE标志 if [[ -w "$config_file" || -n "$FORCE" ]]; then # 更新配置 fi
