Bash 条件语句对比:何时使用 test、[ 和 [[

比较 test、单括号和双括号,让你的 Bash 条件语句保持可移植、安全且易读。

Bash 条件语句对比:何时使用 test、[ 和 [[

当 Bash 条件语句行为异常时,问题往往出在你选择的语法结构上。test[ ][[ ]] 看起来相似,但它们在引号处理、模式匹配、正则表达式和可移植性方面存在差异。

本指南将对比这三种形式,帮助你编写安全、易读且适合脚本运行 shell 的条件语句。

test 命令:基础

test 命令是 shell 脚本中评估条件最古老、最基本的方式之一。它是大多数现代 shell 的内置命令,并且是 POSIX 标准的一部分,因此具有很高的可移植性。test 评估一个表达式,并返回退出状态 0(真)或 1(假)。

基本用法

test 命令接受一个或多个参数,这些参数构成要评估的表达式。它可以检查文件属性、字符串比较和整数比较。

# 检查文件是否存在
if test -f "myfile.txt"; then
    echo "myfile.txt 存在且是一个普通文件。"
fi

# 检查两个字符串是否相等
NAME="Alice"
if test "$NAME" = "Alice"; then
    echo "名字是 Alice。"
fi

# 检查一个数字是否大于另一个
COUNT=10
if test "$COUNT" -gt 5; then
    echo "计数大于 5。"
fi

常用 test 运算符

  • 文件运算符-f(普通文件)、-d(目录)、-e(存在)、-s(非空)、-r(可读)、-w(可写)、-x(可执行)。
  • 字符串运算符=(相等)、!=(不相等)、-z(字符串为空)、-n(字符串非空)。
  • 整数运算符-eq(相等)、-ne(不相等)、-gt(大于)、-ge(大于或等于)、-lt(小于)、-le(小于或等于)。

提示:始终对 test 中使用的变量加引号(例如 "$NAME"),以防止变量值包含空格或通配符时出现单词拆分和路径名扩展问题。

单括号 [ ]test 的形式

单括号 [ ] 结构是 test 命令的另一种语法。在许多 shell 中,[ 是 shell 内置命令,系统通常也提供外部命令 /usr/bin/[。关键区别在于 [ 要求最后一个参数是闭合的 ]。与 test 一样,它符合 POSIX 标准。

语法和语义

# 等同于 test -f "myfile.txt"
if [ -f "myfile.txt" ]; then
    echo "myfile.txt 存在且是一个普通文件(使用 [ ])。"
fi

# 等同于 test "$NAME" = "Alice"
NAME="Bob"
if [ "$NAME" != "Alice" ]; then
    echo "名字不是 Alice。"
fi

注意 [ 之后和 ] 之前必须有空格。它们被视为 [ 命令的单独参数。

变量引号:关键细节

由于 [ ] 本质上是 test 命令,因此它继承了相同的单词拆分和路径名扩展行为。这意味着未加引号的变量可能导致意外行为或安全漏洞

考虑以下示例:

#!/bin/bash

INPUT="file with spaces.txt"

# 危险:如果 INPUT 包含空格,未加引号的变量会导致问题
# shell 会进行单词拆分,将 "file" 和 "with spaces.txt" 视为单独的参数
# 导致语法错误或错误评估。
# if [ -f $INPUT ]; then echo "找到"; else echo "未找到"; fi 

# 正确:对变量加引号,将其视为单个参数
if [ -f "$INPUT" ]; then
    echo "'file with spaces.txt' 存在。"
else
    echo "'file with spaces.txt' 不存在或不是普通文件。"
fi

如果不加引号,$INPUT 会扩展为 file with spaces.txt,而 [ -f file with spaces.txt ] 会被 [ 命令解释为语法错误,因为 -f 只期望一个操作数。加引号确保 $INPUT 作为单个参数 "file with spaces.txt" 传递。

单词拆分和路径名扩展的危险

test[ 都受 shell 默认的单词拆分和路径名扩展(通配)行为影响。如果变量包含空格或通配符(*?[ ])且未加引号,shell 会在 test[ 看到参数之前对其进行扩展。当通配符匹配现有文件时,这可能导致语法错误或不正确的比较。

双括号 [[ ]]:现代 Bash 关键字

双括号 [[ ]] 结构是一个 Bash 关键字(也受 Ksh 和 Zsh 支持),而不是外部命令或别名。这一区别至关重要,因为它允许 [[ ]] 表现不同,并提供比 test[ ] 更强的功能和更高的安全性。

增强功能

[[ ]] 引入了 test[ 不具备的几个强大功能:

  1. 无单词拆分或路径名扩展[[ ]] 内的变量通常不需要加引号(尽管为了清晰起见,加引号通常是好习惯)。shell 将 [[ ]] 的内容作为一个整体处理,防止单词拆分和路径名扩展。这大大减少了常见的脚本错误和安全风险。

    # 无需对变量加引号(尽管加引号仍然安全)
    INPUT="file with spaces.txt"
    if [[ -f $INPUT ]]; then # 这里 $INPUT 被视为单个字符串
        echo "'$INPUT' 存在。"
    fi
    
  2. 字符串比较的通配匹配:在 [[ ]] 内部使用时,==!= 运算符执行模式匹配(通配),而不是严格的字符串相等。这意味着你可以使用 *?[] 作为通配符。

    FILE_NAME="my_document.txt"
    if [[ "$FILE_NAME" == *".txt" ]]; then # 检查 FILE_NAME 是否以 .txt 结尾
        echo "这是一个文本文件!"
    fi
    
    # 注意:对于不带通配的严格字符串相等,请使用 `test` 或 `[ ]` 配合 `=`
    # 或者确保 `[[ ]]` 中 `==` 的右侧没有通配符
    # (如果右侧包含要按字面匹配的通配符,则对其加引号)。
    
  3. 正则表达式匹配=~ 运算符允许你执行正则表达式匹配。

    IP_ADDRESS="192.168.1.100"
    if [[ "$IP_ADDRESS" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
        echo "有效的 IP 格式。"
    fi

    # 重要:=~ 右侧的正则表达式模式通常不应加引号
    # 如果它包含可能被解释为通配模式的字符。
    # 如果正则表达式在变量中,它也不应加引号。
    # 模式示例:^[A-Za-z]+$
    ```

4.  **逻辑运算符 `&&` 和 `||`**:`[[ ]]` 支持更直观的 C 风格逻辑运算符 `&&`(与)和 `||`(或)来组合多个条件,以及 `!` 用于取反。这些运算符具有正确的短路评估和优先级,不同于 `test` 的 `-a` 和 `-o`。

    ```bash
    AGE=25
    if [[ "$NAME" == "Alice" && "$AGE" -ge 18 ]]; then
        echo "Alice 是成年人。"
    fi

    if [[ "$USER" == "root" || -w /etc/fstab ]]; then
        echo "要么是 root,要么可以写入 fstab。"
    fi
    ```

### Bash 特定性质

虽然 `[[ ]]` 提供了显著优势,但其主要缺点是它是 Bash/Ksh/Zsh 扩展,不是 POSIX 标准的一部分。这意味着依赖 `[[ ]]` 的脚本可能无法移植到 `sh`、`dash` 或较旧/最小化的类 Unix 系统。

## 并排对比:`test` vs. `[` vs. `[[`

以下是关键差异的总结表:

| 特性                    | `test`                           | `[ ]`                               | `[[ ]]`                                   |
| :------------------------- | :------------------------------- | :---------------------------------- | :---------------------------------------- |
| **类型**                   | 内置命令(或外部命令)   | 内置命令(`test` 的别名)  | Shell 关键字(Bash、Ksh、Zsh)            |
| **符合 POSIX 标准**        | 是                              | 是                                 | 否                                        |
| **需要闭合 `]`**   | 否                               | 是(作为最后一个参数)              | 是(作为关键字的一部分)                  |
| **单词拆分**         | 是,针对未加引号的变量       | 是,针对未加引号的变量         | 否,变量被视为单个字符串 |
| **路径名扩展**     | 是,针对未加引号的变量       | 是,针对未加引号的变量         | 否 |
| **通配模式匹配** | 字符串相等时否           | 字符串相等时否             | 是,当 `==` 或 `!=` 右侧未加引号时 |
| **正则表达式**    | 否                               | 否                                  | 是,使用 `=~` |
| **逻辑与/或**         | `-a`、`-o` 存在但容易误读 | `-a`、`-o` 存在但容易误读 | `&&`、`||` 具有正常的短路行为 |
| **复合命令**      | 需要单独的 `test` 调用   | 需要单独的 `[` 调用         | 可以直接组合表达式(`&&`/`||`)|
| **变量引号**       | **强制**为了安全         | **强制**为了安全            | 通常不需要,但好习惯 |

## 何时使用哪种

选择正确的条件结构主要取决于你的可移植性需求和条件逻辑的复杂性。

### POSIX 合规性 vs. 现代 Bash 功能

-   **在以下情况下使用 `test` 或 `[ ]`:**
    -   **可移植性至关重要**:如果你的脚本需要在任何 POSIX 兼容的 shell(`sh`、`dash`、较旧系统等)上运行,`test` 或 `[ ]` 是唯一可靠的选择。
    -   你的条件很简单(文件检查、基本字符串/整数比较)。
    -   你习惯对所有变量仔细加引号,并在需要复合逻辑时在括号外使用 shell 级别的 `&&`/`||`。

-   **在以下情况下使用 `[[ ]]`:**
    -   **你专门为 Bash 编写**(或 Ksh/Zsh)且不需要 POSIX 可移植性。
    -   你需要高级功能,如通配模式匹配、正则表达式匹配或 C 风格的 `&&`/`||` 逻辑运算符。
    -   你想要增强的安全功能,防止单词拆分和路径名扩展,从而编写更健壮、更不易出错的代码。
    -   你的条件涉及复杂逻辑,使用 `test -a`/`-o` 会很繁琐。

### 最佳实践和建议

1.  **优先为 Bash 脚本使用 `[[ ]]`**:如果你的脚本是为 Bash 编写的,`[[ ]]` 通常是首选,因为它具有更高的安全性、扩展的功能以及更直观的复杂条件语法。它大大减少了与引号和特殊字符相关的常见脚本错误。

2.  **在 `test` 和 `[ ]` 中始终加引号**:如果出于 POSIX 合规性*必须*使用 `test` 或 `[ ]`,请养成**始终对变量加引号**的习惯,以防止单词拆分和路径名扩展导致的意外行为。

    ```bash
    # [ ] 和 test 的良好实践
    VAR="a string with spaces"
    if [ -n "$VAR" ]; then echo "非空"; fi
    ```

3.  **注意模式匹配**:在 `test` 和 `[ ]` 中,`=` 用于字符串相等。在 `[[ ]]` 中,当右侧未加引号时,`=` 和 `==` 都可以执行模式匹配。当需要字面字符串比较时,对右侧加引号。

4.  **使用 `=~` 的正则表达式**:在 `[[ ]]` 中使用 `=~` 时,右侧通常不应加引号,以允许 shell 将其解释为正则表达式模式,而不是要匹配的字面字符串。

    ```bash
    # 在 [[ ]] 中,未加引号的正则表达式模式对于 =~ 是正确的
    if [[ "$LINE" =~ ^Error: ]]; then echo "发现错误"; fi
    ```

## 要点

当你的脚本必须在 POSIX `sh` 下运行时,使用 `[ ]` 或 `test`。当你的 shebang 是 Bash 并且你想要更安全的变量处理、通配匹配、正则表达式匹配和更清晰的复合条件时,使用 `[[ ]]`。主要习惯很简单:将条件语法与 shell 匹配,并有意识地加引号。