Сравнение условных конструкций Bash: когда использовать test, [ и [[

Сравните test, одинарные и двойные скобки, чтобы ваши условные конструкции Bash оставались переносимыми, безопасными и читаемыми.

Сравнение условных конструкций Bash: когда использовать test, [ и [[

Когда условная конструкция Bash ведет себя странно, проблема часто кроется в выбранной вами конструкции. test, [ ] и [[ ]] выглядят похоже, но по-разному обрабатывают кавычки, шаблоны, регулярные выражения и переносимость.

Это руководство сравнивает три формы, чтобы вы могли писать условные конструкции, которые безопасны, читаемы и подходят для оболочки, в которой фактически выполняется ваш скрипт.

Команда test: Основа

Команда test — один из старейших и наиболее фундаментальных способов оценки условий в 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 "Count больше 5."
fi

Распространенные операторы test

  • Файловые операторы: -f (обычный файл), -d (каталог), -e (существует), -s (не пустой), -r (читаемый), -w (записываемый), -x (исполняемый).
  • Строковые операторы: = (равно), != (не равно), -z (строка пуста), -n (строка не пуста).
  • Операторы для целых чисел: -eq (равно), -ne (не равно), -gt (больше), -ge (больше или равно), -lt (меньше), -le (меньше или равно).

Совет: Всегда заключайте переменные, используемые с test, в кавычки (например, "$NAME"), чтобы избежать проблем с разбиением слов и раскрытием шаблонов имен файлов, если значение переменной содержит пробелы или символы подстановки.

Одинарные скобки [ ]: Форма test

Конструкция с одинарными скобками [ ] является альтернативным синтаксисом для команды test. Во многих оболочках [ — это встроенная команда оболочки, а в системах также часто присутствует внешняя /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 содержит пробелы
# Оболочка выполнит разбиение слов, обрабатывая "file" и "with spaces.txt" как отдельные аргументы,
# что приведет к синтаксической ошибке или неверной оценке.
# if [ -f $INPUT ]; then echo "Found"; else echo "Not found"; 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, так и [ подвержены стандартному поведению оболочки по разбиению слов и раскрытию шаблонов имен файлов (globbing). Если переменная содержит пробелы или символы подстановки (*, ?, [ ]) и не заключена в кавычки, оболочка развернет ее до того, как test или [ увидят аргументы. Это может привести к синтаксическим ошибкам или неверным сравнениям, когда символы подстановки совпадают с существующими файлами.

Двойные скобки [[ ]]: Современное ключевое слово Bash

Конструкция с двойными скобками [[ ]] является ключевым словом Bash (также поддерживается в Ksh и Zsh), а не внешней командой или псевдонимом. Это различие имеет решающее значение, поскольку позволяет [[ ]] вести себя иначе и предлагает расширенную функциональность и повышенную безопасность по сравнению с test или [ ].

Расширенная функциональность

[[ ]] вводит несколько мощных функций, недоступных в test или [:

  1. Отсутствие разбиения слов и раскрытия шаблонов имен файлов: Переменные внутри [[ ]] обычно не нужно заключать в кавычки (хотя для ясности это часто хорошая практика). Оболочка обрабатывает содержимое [[ ]] как единое целое, предотвращая разбиение слов и раскрытие шаблонов имен файлов. Это значительно снижает распространенные ошибки сценариев и риски безопасности.

    # Нет необходимости заключать переменные в кавычки (хотя это все равно безопасно)
    INPUT="file with spaces.txt"
    if [[ -f $INPUT ]]; then # $INPUT здесь обрабатывается как единая строка
        echo "'$INPUT' существует."
    fi
    
  2. Подстановка для сравнения строк: Операторы == и != выполняют сопоставление с образцом (globbing), а не строгое равенство строк при использовании внутри [[ ]]. Это означает, что вы можете использовать *, ? и [] в качестве подстановочных знаков.

    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 `&&` (И) и `||` (ИЛИ) для объединения нескольких условий, а также `!` для отрицания. Эти операторы имеют правильное сокращенное вычисление и приоритет, в отличие от `-a` и `-o` в `test`.

    ```bash
    AGE=25
    if [[ "$NAME" == "Alice" && "$AGE" -ge 18 ]]; then
        echo "Алиса совершеннолетняя."
    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`)  | Ключевое слово оболочки (Bash, Ksh, Zsh)            |
| **Совместимость с POSIX**        | Да                              | Да                                 | Нет                                        |
| **Требуется закрывающая `]`**   | Нет                               | Да (как последний аргумент)              | Да (как часть ключевого слова)                  |
| **Разбиение слов**         | Да, для незаключенных в кавычки переменных       | Да, для незаключенных в кавычки переменных         | Нет, переменные обрабатываются как единые строки |
| **Раскрытие шаблонов имен файлов**     | Да, для незаключенных в кавычки переменных       | Да, для незаключенных в кавычки переменных         | Нет |
| **Сопоставление с образцом (globbing)** | Нет для равенства строк           | Нет для равенства строк             | Да с незаключенной в кавычки правой частью `==` или `!=` |
| **Регулярные выражения**    | Нет                               | Нет                                  | Да с `=~` |
| **Логические И/ИЛИ**         | `-a`, `-o` существуют, но их легко неправильно прочитать | `-a`, `-o` существуют, но их легко неправильно прочитать | `&&`, `||` с обычным сокращенным вычислением |
| **Составные команды**      | Требует отдельных вызовов `test`   | Требует отдельных вызовов `[`         | Может комбинировать выражения напрямую (`&&`/`||`)|
| **Заключение переменных в кавычки**       | **Обязательно** для безопасности         | **Обязательно** для безопасности            | Обычно не требуется, но хорошая практика |

## Когда что использовать

Выбор правильной условной конструкции зависит в первую очередь от ваших требований к переносимости и сложности вашей условной логики.

### Совместимость с POSIX против современных функций Bash

-   **Используйте `test` или `[ ]`, когда...**
    -   **Переносимость имеет первостепенное значение**: Если ваш скрипт должен работать в любой POSIX-совместимой оболочке (`sh`, `dash`, старые системы и т.д.), `test` или `[ ]` — ваши единственные надежные варианты.
    -   Ваши условия просты (проверки файлов, базовые сравнения строк/целых чисел).
    -   Вы готовы тщательно заключать все переменные в кавычки и использовать `&&`/`||` на уровне оболочки вне скобок, когда вам нужна составная логика.

-   **Используйте `[[ ]]`, когда...**
    -   **Вы пишете исключительно для Bash** (или Ksh/Zsh) и вам не нужна переносимость POSIX.
    -   Вам требуются расширенные функции, такие как сопоставление с образцом (globbing), сопоставление с регулярными выражениями или логические операторы в стиле C `&&`/`||`.
    -   Вы хотите воспользоваться расширенными функциями безопасности, которые предотвращают разбиение слов и раскрытие шаблонов имен файлов, что приводит к более надежному и менее подверженному ошибкам коду.
    -   Ваши условия включают сложную логику, которая была бы громоздкой с `test -a`/`-o`.

### Лучшие практики и рекомендации

1.  **Отдавайте приоритет `[[ ]]` для скриптов Bash**: Если ваш скрипт предназначен для Bash, `[[ ]]` обычно является предпочтительным выбором из-за повышенной безопасности, расширенной функциональности и более интуитивного синтаксиса для сложных условий. Это значительно сокращает распространенные ошибки сценариев, связанные с кавычками и специальными символами.

2.  **Всегда заключайте в кавычки в `test` и `[ ]`**: Если вы *должны* использовать `test` или `[ ]` для совместимости с POSIX, возьмите за привычку **всегда заключать переменные в кавычки**, чтобы предотвратить неожиданное поведение из-за разбиения слов и раскрытия шаблонов имен файлов.

    ```bash
    # Хорошая практика для [ ] и test
    VAR="строка с пробелами"
    if [ -n "$VAR" ]; then echo "Не пусто"; fi
    ```

3.  **Помните о сопоставлении с образцом**: В `test` и `[ ]` `=` используется для равенства строк. В `[[ ]]` `=` и `==` могут выполнять сопоставление с образцом, когда правая часть не заключена в кавычки. Заключайте правую часть в кавычки, когда вам нужно буквальное сравнение строк.

4.  **Регулярные выражения с `=~`**: При использовании `=~` в `[[ ]]` правая часть обычно должна быть без кавычек, чтобы позволить оболочке интерпретировать ее как шаблон регулярного выражения, а не как буквальную строку для сопоставления.

    ```bash
    # Шаблон регулярного выражения без кавычек правильный для =~ в [[ ]]
    if [[ "$LINE" =~ ^Error: ]]; then echo "Ошибка найдена"; fi
    ```

## Вывод

Используйте `[ ]` или `test`, когда ваш скрипт должен работать под POSIX `sh`. Используйте `[[ ]]`, когда ваш shebang — Bash, и вы хотите более безопасную обработку переменных, сопоставление с шаблонами, сопоставление с регулярными выражениями и более чистые составные условия. Главная привычка проста: подбирайте синтаксис условных конструкций под оболочку и осознанно используйте кавычки.