testコマンドとbash/zshの複合コマンド“[[”について

Last modified: 2021-10-08

文字列や数値の比較、ファイルやディレクトリの種類や存在の確認などを行うことのできるコマンドがtestコマンド。

これは[という名前で使用することもでき、基本的な扱いは同様だが、条件の終わりを]で閉じる必要がある点が異なる。

test[は単独の実行ファイル(外部コマンド)として存在しており、シェルに依存しない。ただしif文の文法はシェルの種類によって異なる。

test(“[”)コマンドの条件式

実際の指定方法はbashのマニュアルページ内 “条件式” を参照。

注意が必要なのは

  • 2つの文字列が同一かを比較するのには[ [文字列1] = [文字列2] ]と記述する
    • 他の言語で比較に用いられることのある==でも実際には動作することがあるが、POSIXに準拠する形の使い方ではない
    • over80氏からも移動前の記事にコメントを頂いている通り “幅広いシェル環境で動作するシェルスクリプトを記述する観点からは==の使用を勧めるべきではない” とのこと
  • 数値としての大小比較は-eq(等しい)/-ne(等しくない)/-lt(左が小さい)/-le(等しいか左が小さい)/-gt(左が大きい)/-ge(等しいか左が大きい)で行い、等号や不等号は用いない

条件式の中で!を付けると、真偽が逆になる。

下は/foo/というディレクトリが存在するかどうかをチェックする例。

([ ... ] で記述・dashでも使用可能)
$ [ -d /foo ] && echo "TRUE" || echo "FALSE"
FALSE
$ [ ! -d /foo ] && echo "TRUE" || echo "FALSE"
TRUE

([[ ... ]] で記述・bash/zsh専用)
bash$ [[ -d /foo ]] && echo "TRUE" || echo "FALSE"
FALSE
bash$ [[ ! -d /foo ]] && echo "TRUE" || echo "FALSE"
TRUE

文字列をパターンで評価する

[[での文字列比較ではglobという類のパターン表記で評価することができる。

(「*」は0文字以上の任意の文字列に一致)
bash$ [[ abcdefg = abc* ]] && echo true || echo false
true
(「?」は1文字の任意の文字に一致)
bash$ [[ abcdefg = abc??fg ]] && echo true || echo false
true
(「[」と「]」で囲んだ文字列はそのいずれかの1文字であれば一致)
bash$ [[ abcdefg = abc[asdfghjk]efg ]] && echo true || echo false
true
(「[」と「]」で囲んだ文字列は「-」で範囲指定可能)
bash$ [[ abcdefg = abc[d-f][d-f][d-f]g ]] && echo true || echo false
true

[(test)ではこのような比較はできない。

(testコマンドではパターンの比較にはならない)
$ [ abcdefg = abc* ] && echo true || echo false
false

1つの条件式内に複数の条件をAND/ORで指定

test(“[”)コマンド

test([)コマンドでの記述では2つの条件の間に

  • AND指定(前後の2つの条件を同時に満たすと0が返る): -a
  • OR指定(前後の2つの条件の片方でも満たせば0が返る): -o

のいずれかを記述することで1つの条件式内に複数の条件を指定できる。

他のプログラミング言語で見られるような “&&||を一つの条件式の中に含める書き方” はシェルの文法上できない。

if [ [条件式1] -a [条件式2] ]; then
    [条件式1と条件式2の両方を満たすときの処理]
fi

if [ [条件式1] -o [条件式2] ]; then
    [条件式1と条件式2の片方もしくは両方を満たすときの処理]
fi

以下は/foo//bar/が存在せず/opt//usr/が存在する状態での例。

$ if [ -d /opt -a -d /usr ]; then echo "TRUE"; else echo "FALSE"; fi
TRUE
$ if [ -d /foo -a -d /usr ]; then echo "TRUE"; else echo "FALSE"; fi
FALSE
$ if [ -d /foo -o -d /usr ]; then echo "TRUE"; else echo "FALSE"; fi
TRUE
$ if [ -d /foo -o -d /bar ]; then echo "TRUE"; else echo "FALSE"; fi
FALSE

条件の不必要な評価が起こりうる

複数の条件を指定したとき、場合によっては最初に評価した条件を満たしたかどうかだけで全体の結果が決定してその後の条件の評価が不要なことが起こりうる。

ところが、-oでOR指定した場合、最初に評価した条件を満たさなかったとしても全ての条件を評価してしまう。

$ [ 1 -eq 2 -a $(sleep 5; echo abc) = abc ] && echo true || echo false
(sleepコマンドが実行されて5秒待つ)
false

$ [ 1 -eq 1 -o $(sleep 5; echo abc) = abc ] && echo true || echo false
(sleepコマンドが実行されて5秒待つ)
true

複合コマンド[[

複合コマンド[[では、1つの条件式の中で条件を

  • AND指定(前後の2つの条件を同時に満たすと0が返る): &&
  • OR指定(前後の2つの条件の片方でも満たせば0が返る): ||

のいずれかで区切ることで複数の条件を指定できる。

これは他のプログラミング言語における書き方と共通しているため分かりやすいが、シェルスクリプトの中で用いるとシェル依存になってしまう。

こちらでは-a-oを使って区切ることはできない(エラー例:bash -c 'if [[ -d /opt -a -d /usr ]]; then echo "TRUE"; else echo "FALSE"; fi')。

if [[ [条件式1] && [条件式2] ]]; then
    [条件式1と条件式2の両方を満たすときの処理]
fi
if [[ [条件式1] || [条件式2] ]]; then
    [条件式1と条件式2の片方もしくは両方を満たすときの処理]
fi

以下も先ほどと同様/foo//bar/が存在せず/opt//usr/が存在する状態での例。

bash$ if [[ -d /opt && -d /usr ]]; then echo "TRUE"; else echo "FALSE"; fi
TRUE
bash$ if [[ -d /foo && -d /usr ]]; then echo "TRUE"; else echo "FALSE"; fi
FALSE
bash$ if [[ -d /foo || -d /usr ]]; then echo "TRUE"; else echo "FALSE"; fi
TRUE
bash$ if [[ -d /foo || -d /bar ]]; then echo "TRUE"; else echo "FALSE"; fi
FALSE

条件の不必要な評価は行われない

&&||を指定した場合、最初に評価した条件によって全体の結果が決まる場面ではその後の不必要な条件評価は行われない。

bash$ [[ 1 -eq 2 && $(sleep 5; echo abc) = abc ]] && echo true || echo false
(すぐに結果が出力されて終了する)
false

bash$ [[ 1 -eq 1 || $(sleep 5; echo abc) = abc ]] && echo true || echo false
(すぐに結果が出力されて終了する)
false

bash/zshの「[[ 条件式 ]] 」は「 [ 条件式 ] 」の書き方より速い?

手元の環境の/usr/share/doc/以下のファイル/ディレクトリ一覧を

$ find /usr/share/doc > list.txt

で出力して、各項目1に対して、ファイルなら “file”、ディレクトリなら “dir” を出力するという処理を行い、全てを完了するのにかかった時間を、判別部分に[を使用した場合と[[を使用した場合とでそれぞれ計測し、比較した。cpufreqは最低クロック/電圧の固定で実験した。

$ bash -c 'while read x; do if [[ -f ${x} ]]; then echo "file"; elif [[ -d ${x} ]]; then echo "dir"; fi; done < list.txt'
$ bash -c 'while read x; do if [ -f ${x} ]; then echo "file"; elif [ -d ${x} ]; then echo "dir"; fi; done < list.txt'

この結果、複合コマンド[[を使用したほうが短時間で処理が終了するということが分かった。zshでも同様の実験を行ったが、こちらも[[のほうが速かった。

ファイル/ディレクトリ判別+表示の合計処理時間(秒)

シェル [[ 条件式 ]] [ 条件式 ]
bash 25 27
zsh 22 25
  • 使用したバージョン
    • bash 3.2_p17(3.2_p17-r1)
    • zsh 4.3.4(4.3.4-r1)

  1. findで条件-type dを付けるとディレクトリのみ、-type fを付けるとファイルのみが表示されるが、いずれも指定しないと両方とも表示される ↩︎