Bashの条件式比較:test、[ ]、[[ ]]の使い分け

test、単括弧、二重括弧を比較して、Bashの条件式を移植性が高く、安全で、読みやすいものに保つ方法を解説します。

Bashの条件式比較:test、[ ]、[[ ]]の使い分け

Bashの条件式がおかしな動作をする場合、問題は選択した構文にあることがよくあります。test[ ][[ ]]は似ていますが、クォーティング、パターン、正規表現、移植性の扱いが異なります。

このガイドでは3つの形式を比較し、スクリプトが実際に動作するシェルに適した、安全で読みやすい条件式を書けるようにします。

testコマンド:基本

testコマンドは、シェルスクリプトで条件を評価する最も古く基本的な方法の1つです。ほとんどの現代的なシェルに組み込まれており、POSIX標準の一部であるため、移植性が非常に高いです。testは式を評価し、終了ステータス0(真)または1(偽)を返します。

基本的な使い方

testコマンドは1つ以上の引数を取り、評価する式を構成します。ファイル属性、文字列比較、整数比較をチェックします。

# ファイルが存在するか確認
if test -f "myfile.txt"; then
    echo "myfile.txtは存在し、通常のファイルです。"
fi

# 2つの文字列が等しいか確認
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 

# 正しい:変数をクォートして1つの引数として扱う
if [ -f "$INPUT" ]; then
    echo "'file with spaces.txt'は存在します。"
else
    echo "'file with spaces.txt'は存在しないか、通常のファイルではありません。"
fi

クォートしない場合、$INPUTfile with spaces.txtに展開され、[ -f file with spaces.txt ][コマンドによって構文エラーとして解釈されます(-fは1つのオペランドしか期待しないため)。クォートすることで、$INPUTが1つの引数"file with spaces.txt"として渡されることが保証されます。

ワード分割とパス名展開の危険性

test[はどちらも、シェルのデフォルトのワード分割とパス名展開(グロビング)の動作の影響を受けます。変数にスペースやグロブ文字(*?[ ])が含まれており、クォートされていない場合、シェルはtest[が引数を認識する前に展開を行います。これにより、グロブ文字が既存のファイルと一致した場合、構文エラーや誤った比較が発生する可能性があります。

二重括弧[[ ]]:現代的なBashキーワード

二重括弧[[ ]]構文はBashキーワード(KshやZshでもサポート)であり、外部コマンドやエイリアスではありません。この違いは重要であり、[[ ]]test[ ]とは異なる動作をし、強化された機能と安全性を提供することを可能にします。

強化された機能

[[ ]]test[では利用できないいくつかの強力な機能を導入します:

  1. ワード分割やパス名展開がない[[ ]]内の変数は、一般的にクォートする必要がありません(ただし、明確さのためにクォートすることは良い習慣です)。シェルは[[ ]]の内容を1つの単位として扱い、ワード分割やパス名展開を防ぎます。これにより、一般的なスクリプトエラーやセキュリティリスクが大幅に減少します。

    # 変数をクォートする必要はない(ただし、クォートしても安全)
    INPUT="file with spaces.txt"
    if [[ -f $INPUT ]]; then # ここでは$INPUTは1つの文字列として扱われる
        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スタイルの論理演算子`&&`(AND)と`||`(OR)をサポートしており、`!`で否定も可能です。これらの演算子は、`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ユーザーであるか、/etc/fstabに書き込み可能です。"
    fi
    ```

### Bash固有の性質

`[[ ]]`は大きな利点を提供しますが、主な欠点はBash/Ksh/Zshの拡張であり、POSIX標準の一部ではないことです。つまり、`[[ ]]`に依存するスクリプトは、`sh`、`dash`、または古い/最小限のUnix系システムに移植できない可能性があります。

## 横並び比較:`test` vs. `[` vs. `[[`

主な違いをまとめた表です:

| 機能                     | `test`                           | `[ ]`                               | `[[ ]]`                                   |
| :----------------------- | :------------------------------- | :---------------------------------- | :---------------------------------------- |
| **種類**                 | 組み込みコマンド(または外部)   | 組み込みコマンド(`test`のエイリアス)| シェルキーワード(Bash、Ksh、Zsh)        |
| **POSIX準拠**            | はい                              | はい                                 | いいえ                                    |
| **閉じ括弧`]`が必要**   | いいえ                            | はい(最後の引数として)              | はい(キーワードの一部として)            |
| **ワード分割**           | はい、クォートされていない変数で   | はい、クォートされていない変数で     | いいえ、変数は単一の文字列として扱われる  |
| **パス名展開**           | はい、クォートされていない変数で   | はい、クォートされていない変数で     | いいえ                                    |
| **グロビングパターンマッチ** | 文字列等価性ではいいえ            | 文字列等価性ではいいえ              | はい、`==`や`!=`の右辺がクォートされていない場合 |
| **正規表現**             | いいえ                            | いいえ                              | はい、`=~`で                              |
| **論理AND/OR**           | `-a`、`-o`は存在するが誤読されやすい | `-a`、`-o`は存在するが誤読されやすい | `&&`、`||`で通常のショートサーキット動作   |
| **複合コマンド**         | 別々の`test`呼び出しが必要        | 別々の`[`呼び出しが必要             | 式を直接組み合わせ可能(`&&`/`||`)       |
| **変数のクォーティング** | **必須**(安全性のため)          | **必須**(安全性のため)             | 一般的には不要だが、良い習慣             |

## いつどれを使うべきか

適切な条件構文の選択は、主に移植性の要件と条件ロジックの複雑さに依存します。

### POSIX準拠 vs. 現代的なBash機能

-   **`test`または`[ ]`を使うべき場合...**
    -   **移植性が最優先**:スクリプトがPOSIX準拠のシェル(`sh`、`dash`、古いシステムなど)で動作する必要がある場合、`test`または`[ ]`が唯一の信頼できる選択肢です。
    -   条件が単純な場合(ファイルチェック、基本的な文字列/整数比較)。
    -   すべての変数を注意深くクォートし、複合ロジックが必要な場合は括弧の外でシェルレベルの`&&`/`||`を使用することに慣れている場合。

-   **`[[ ]]`を使うべき場合...**
    -   **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.  **`=~`での正規表現**:`[[ ]]`で`=~`を使用する場合、右辺は通常クォートせず、シェルが正規表現パターンとして解釈できるようにする必要があります(リテラルな文字列としてマッチさせるのではなく)。

    ```bash
    # [[ ]]の=~ではクォートされていない正規表現パターンが正しい
    if [[ "$LINE" =~ ^Error: ]]; then echo "エラーが見つかりました"; fi
    ```

## まとめ

スクリプトがPOSIX `sh`で動作する必要がある場合は、`[ ]`または`test`を使用してください。シバンがBashで、より安全な変数処理、グロブマッチング、正規表現マッチング、よりクリーンな複合条件が必要な場合は、`[[ ]]`を使用してください。主な習慣はシンプルです:条件構文をシェルに合わせ、意図的にクォートすることです。