test / [[ ]] 入門 - シェルスクリプトの条件判定

test / [[ ]] 入門 - シェルスクリプトの条件判定

この記事で解決できること

  • test / [ ] / [[ ]]違いと使い分け が分かる
  • 文字列・数値・ファイルの 比較演算子を正しく選べる ようになる
  • [: too many arguments のような クォート事故を構造的に回避 できる

結論(実務の型)

  • 迷ったら [[ ]] を使う(bash 前提なら安全側に倒れる)
  • POSIX sh への移植性が必要なときだけ [ ](= test を使う
  • 事故の原因はほぼ ①クォート漏れ ②演算子の取り違え(-eq= の 2 つ

前提(対象環境)

  • シェル: bash([[ ]] は bash / ksh / zsh の拡張で、POSIX sh には無い)
  • スクリプト先頭が #!/bin/bash であることを想定

test・[ ]・[[]] は何が違うのか?

結論: [ ]test コマンドそのもの。[[ ]] はシェルの予約語で、クォート・演算子・パターンマッチが強化されている。

3 つの関係を一言で整理すると次のとおり。

記法 正体 移植性 主な強み
test EXPR 外部にも実体がある組み込みコマンド POSIX(高い) 最も基本的
[ EXPR ] test の別名(] は引数) POSIX(高い) if と相性が良い見た目
[[ EXPR ]] シェルの予約語(keyword) bash 等(限定) クォート安全・=~&&/||

[ ]コマンド なので、[] の前後、各演算子の前後には必ずスペースが要る。test[ は同じ動作をする。

$ test 1 -lt 2; echo $?
0
$ [ 1 -lt 2 ]; echo $?
0

一方 [[ ]] はシェルが構文として解釈するため、後述のクォート事故やパターンマッチで有利になる。

[ の正体を確認する

$ type [
[ is a shell builtin
$ ls -l /usr/bin/[
-rwxr-xr-x 1 root root ... /usr/bin/[

組み込み版が優先されるが、test/[ は外部コマンドとしても存在する歴史的経緯がある。

文字列はどう比較するのか?

結論: 文字列一致は =[[ ]] では == も可)、空判定は -z/非空は -n。変数は必ずダブルクォートで囲む。

基本の演算子

  • = … 等しい([[ ]] 内では == も同義)
  • != … 等しくない
  • -z "$s" … 文字列が空(zero length)
  • -n "$s" … 文字列が空でない
name="penguin"

if [[ "$name" = "penguin" ]]; then
    echo "match"
fi

if [[ -z "$name" ]]; then
    echo "empty"
else
    echo "not empty"
fi

=== の使い分け

== は bash の拡張。POSIX sh への移植を考えるなら [ ] では = を使うのが安全。[[ ]] 内ではどちらでも動く。

[ "$a" = "$b" ]     # POSIX 安全
[[ "$a" == "$b" ]]  # bash([[ ]] 内では == も =も可)

数値はどう比較するのか?

結論: 数値比較は -eq -ne -lt -le -gt -ge を使う。=> は文字列比較になり別物。

演算子 意味 数学記号
-eq 等しい ==
-ne 等しくない !=
-lt より小さい <
-le 以下 <=
-gt より大きい >
-ge 以上 >=
count=42

if [[ "$count" -ge 10 ]]; then
    echo "10 以上"
fi

=-eq を取り違えない

[[ "08" = "8" ]]    # false(文字列として違う)
[[ "08" -eq "8" ]]  # true(数値として等しい)

数値のつもりで = を使うと、ゼロ埋めや空白で意図しない結果になる。

算術評価が必要なら (( )) を使う手もある。(( count >= 10 )) のように数学記号がそのまま書ける。

ファイルの存在や種類はどう調べるのか?

結論: ファイルテスト演算子で存在・種類・権限を判定する。よく使うのは -e -f -d -r -w -x -s

演算子 意味
-e file 存在する(種類を問わない)
-f file 通常ファイルである
-d file ディレクトリである
-r file 読み取り可能
-w file 書き込み可能
-x file 実行可能
-s file サイズが 0 より大きい
-L file シンボリックリンクである
a -nt b a が b より新しい
config="/etc/myapp.conf"

if [[ -f "$config" && -r "$config" ]]; then
    echo "読み込める設定ファイルがある"
else
    echo "設定ファイルが無いか読めない"
fi

「存在しない」を判定する

! で否定する。スクリプトの早期リターンで頻出する型。

if [[ ! -d "$dir" ]]; then
    echo "ディレクトリがありません: $dir" >&2
    exit 1
fi

なぜ [[]] のほうが事故りにくいのか?

結論: [[ ]] は変数を展開しても単語分割・グロブ展開をしないため、クォート漏れによる構文崩壊が起きない。

[ ] はコマンドなので、変数が空や空白を含むと 引数の数が変わって 構文が壊れる。

v=""
[ $v = "x" ]      # → [ = "x" ] と解釈され: too many arguments / 構文エラー
[ "$v" = "x" ]    # → [ "" = "x" ](正しく false)クォートで回避

v="a b"
[ $v = "x" ]      # → [ a b = "x" ](引数 4 個でエラー)

[[ ]] 内では、変数を展開しても単語分割・グロブが起きない。クォートを忘れても壊れにくい。

v="a b"
[[ $v = "x" ]]    # 壊れない(false)

それでも クォートする習慣 は残すこと。[ ] でも [[ ]] でも "$var" と書いておけば、どちらに移植しても安全。

[[]] だけのパターンマッチと正規表現とは?

結論: [[ ]]== 右辺はグロブパターン、=~ は正規表現として評価される。[ ] には無い機能。

グロブによるパターンマッチ(==

右辺をクォートしないとパターン、クォートすると文字列として扱う。

file="report.txt"

if [[ "$file" == *.txt ]]; then
    echo "テキストファイル"
fi

if [[ "$file" == "*.txt" ]]; then
    echo "これはリテラルの *.txt と一致したときだけ"
fi

正規表現マッチ(=~

右辺は クォートしない。マッチ部分は BASH_REMATCH 配列に入る。

input="port=8080"

if [[ "$input" =~ ^port=([0-9]+)$ ]]; then
    echo "ポート番号: ${BASH_REMATCH[1]}"
fi

複数条件はどうつなぐのか?

結論: [[ ]] 内なら && || で連結できる。[ ] では [ ... ] && [ ... ] とコマンド単位でつなぐ。

# [[ ]] の中で連結(読みやすい)
if [[ "$age" -ge 18 && "$age" -lt 65 ]]; then
    echo "現役世代"
fi

# [ ] はコマンドを && でつなぐ
if [ "$age" -ge 18 ] && [ "$age" -lt 65 ]; then
    echo "現役世代"
fi

[ ] 内の -a(AND)・-o(OR)は 非推奨。引数の解釈が曖昧になり事故の温床になる。条件をつなぐときは [ ] を分けて && / || でつなぐこと。

まとめ:test と [[]] の使い分け

結論: bash スクリプトなら [[ ]] を基本にし、POSIX sh 互換が要る箇所だけ [ ] を使う。クォートと演算子選択を外さなければ事故はほぼ防げる。

  • [[ ]] … bash 前提。クォート安全・=~&&/|| が使えて読みやすい
  • [ ] / test#!/bin/sh や POSIX 互換が必要な場面
  • 文字列は = / -z / -n、数値は -eq 系、ファイルは -f / -d
  • 変数は常に "$var" でクォートする

コピペ用テンプレ

# 引数チェック
if [[ $# -lt 1 ]]; then
    echo "usage: $0 <file>" >&2
    exit 1
fi

target="$1"

# ファイル存在と種類
if [[ ! -f "$target" ]]; then
    echo "通常ファイルではありません: $target" >&2
    exit 1
fi

# 拡張子で分岐
if [[ "$target" == *.log ]]; then
    echo "ログファイルを処理します"
fi

次に読む