Bashシェルスクリプト上での”[..]”と”[[..]]”の違い

2021-03-08Bash,CentOS,Linux,Ubuntu

はじめに

Bash上で作業を行ったり、Bashのシェルスクリプトを書いたりする場合に条件分岐を使うケースがあります。

if [ -f $FILE ]; then と記述する場合もあれば、 if [[ -f $FILE ]]; then のように [ を2つ続けて記述する場合もあります。
両者の違いについて、知っている範囲でまとめました。

検証環境

$ uname -moi
x86_64 x86_64 GNU/Linux

$ bash -version | head -n 1
GNU bash, version 4.2.46(2)-release (x86_64-redhat-linux-gnu)

かんたんな文字列比較処理を書いてみる

変数 X の値が想定通りの場合に ok と出力するだけのかんたんな条件分岐を記述してみます。

# 変数を作成
$ X=1223

# [...] を使用するケース
$ [ $X = "1223" ] && echo ok
ok

# [[...]] を使用するケース
$ [[ $X = "1223" ]] && echo ok
ok

ちなみに、上記の処理は以下と同義です。

$ if [[ $X = "1223" ]]; then
  echo ok
fi

[...] を使用した場合も [[...]] を使用した場合もこの場合には結果は変わりません。

限られた条件における挙動の違い( 後述 )はありますが、基本的に同じように利用できます。

[[[ の実態はどこにある?何者なの?

[[[ が何者であるか?普通にLinuxを利用していると考えることは少ないでしょう。
調べてみます。

# コマンドの場合は、コマンドの在り処がわかるはず!
$ which [
/usr/bin/[

# コマンドの場合は、コマンドの在り処がわかるはず!
$ which [[
which: no [[ in (/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin)

# aliasやfunction、組み込み関数である可能性も探ってみる
$ type [
[ is a shell builtin

# aliasやfunction、組み込み関数である可能性も探ってみる
$ type [[
[[ is a shell keyword

さて、ここからわかることをまとめます。

[[[
実行ファイルはある?o
ビルトインコマンドである?o
予約語である?o

実行ファイルがあったとしても、Bashを利用している限りは「ビルトインコマンド」や「予約語」の呼び出しが優先されます。
したがって、 /usr/bin/[ が呼ばれることはほぼありません。

おそらく、 /usr/bin/[ は Bash以外のシェルを想定したPOSIX標準のコマンドなのでしょう。

ちなみに /usr/bin/[ --help を実行すると、忘れがちな条件分岐記述時に利用可能なオプションを確認できます。

( 長いので一部抜粋 )

$ /usr/bin/\[ --help
Usage: test EXPRESSION
  or:  test
  or:  [ EXPRESSION ]

  INTEGER1 -le INTEGER2   INTEGER1 is less than or equal to INTEGER2
  INTEGER1 -lt INTEGER2   INTEGER1 is less than INTEGER2
  INTEGER1 -ne INTEGER2   INTEGER1 is not equal to INTEGER2

  FILE1 -ef FILE2   FILE1 and FILE2 have the same device and inode numbers
  FILE1 -nt FILE2   FILE1 is newer (modification date) than FILE2
  FILE1 -ot FILE2   FILE1 is older than FILE2

  -b FILE     FILE exists and is block special
  -c FILE     FILE exists and is character special
  -d FILE     FILE exists and is a directory
  -e FILE     FILE exists
...

[...][[...]] で挙動が異なるケース

ここからは、パッと思いつく両者の挙動の違いについて紹介します。

1. 正規表現が利用できるかどうか

[[...]] では正規表現による変数のチェックが可能です。

$ X=1223

# 変数が1から始まり、間に2を0個以上もち、3で終わる
$ [ $X =~ ^12*3$ ] && echo ok
bash: [: =~: unary operator expected

# 変数が1から始まり、間に2を0個以上もち、3で終わる
$ [[ $X =~ ^12*3$ ]] && echo ok
ok

[...] の場合は、 =~ は不正な演算子と言われています。

2. ワイルドカードが利用できるかどうか

[[...]] ではワイルドカードによる変数のチェックが可能です。
「正規表現」とごちゃごちゃになりそうですが、「正規表現」の場合には比較演算子 =~ を使い、「ワイルドカード」の場合には通常の文字列比較と同じように比較演算子 == を使います。

$ X=1223

# 1から始まり3で終われば何でもOK
$ [ $X == 1*3 ] && echo ok

# 1から始まり3で終われば何でもOK
$ [[ $X == 1*3 ]] && echo ok
ok

否定の比較演算子 != でも利用できます。

$ X=1223

$ [[ $X != 1*3 ]] && echo ng

3. 変数が空でない判定を行う際の -n オプションの挙動

[ -d $変数 ] あるいは [[ -d $変数 ]] を使って、変数が空でない条件分岐を記述できますが、 変数の状態によって挙動が変わります

変数が空ではない場合 は同じ挙動をします。

$ X="1223"

$ [ -n $X ] && echo not empy
not empty

$ [[ -n $X ]] && echo not empty
not empty

変数が空文字(0バイト文字)の場合[...] を利用すると 変数が空ではない と判定されてしまいます。

$ X=""

$ [ -n $X ] && echo not empty
not empty

$ [[ -n $X ]] && echo not empty

変数が未定義の場合 も、やはり [...] を利用すると 変数が空ではない と判定されてしまいます。

$ [ -n $Y ] && echo not empty
not empty

$ [[ -n $Y ]] && echo not empty

[[...]] の挙動のほうが、直感的ではないでしょうか?


ちなみに [...} を利用した場合でも、変数を参照する際にダブルクォートをつけると問題が解消します。

$ [ -n "$Y" ] && echo not empty

今回のケースに限らず、変数参照時はできる限り "${Y}" のように記述する癖をつけましょう。

ひとこと

Bashでシェルスクリプトを作成している前提であれば、迷わず [[...]] 構文で条件分岐を記述したほうが良いでしょう。

2021-03-08Bash,CentOS,Linux,Ubuntu