LoginSignup
143
161

More than 1 year has passed since last update.

シェルスクリプトの [ ] と [[ ]] の違いを歴史的に解説 〜 言語設計者の気持ちになって理解しよう

Last updated at Posted at 2022-11-06

はじめに

bash などのシェルには [ ... ][[ ... ]] の二種類の比較方法があります。一つはコマンド、もう一つはシェルの文法なのですが、具体的にはこの二つは一体何が違うのでしょうか? そもそもなぜ似ている機能が二つもあるのでしょうか? この記事は言語設計者の気持ちになって考えることで、その理由を解き明かそうという記事です。

なお、違いについての簡単な説明については「test と [ と [[ コマンドの違い - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog 」の記事がよくまとめられていますので紹介します。一通りの違いを素早く知りたい方はこちらを参照してください。

参考 シェルの歴史や種類については「シェルの歴史 総まとめ(種類と系統図)と POSIX の役割」に詳しくまとめています(系統図とか頑張って書いたので見ていただけると嬉しいです🙇)

基礎知識

本題の前に、少し基礎知識としてそれぞれの機能について簡単に説明します。

[ ... ] は test ... と同じ意味

以下の二つは全く同じ意味です。

[ 2 -lt 10 ]

test 2 -lt 10

[ ... ] は実際には [ コマンドです。これは「最後の引数を ] で終わらせなければいけない test コマンド」です。そのため [ コマンドと test コマンドはまったく同じというわけではありませんが、[ ... ]test ... は同じ意味になります。

[ は /bin/[ を呼び出していない

しばしば [/bin/[(または /usr/bin/[)を呼び出しているなどと言われますが、それは古い話であり、今のシェルは高速化のために [ をシェルに組み込んでいるため /bin/[ を呼び出していません。[ がシェルに組み込まれたのは 1981 年の UNIX System III の Bourne シェルです。NetBSD sh では 2000 年、FreeBSD sh では 2001 年からシェルに組み込まれているようです。

互換性や POSIX シェル以外などから呼び出すことを考慮してか、今も /bin/[ が存在していることがありますが、シェルに組み込まれている [ とは微妙に機能が異なる場合があります。基本的に /bin/[ を使う必要はありません。

[[ ... ]] は POSIX で標準化されていない

[[ ... ]] は bash、ksh、mksh、yash、zsh など多くのシェルで使うことが出来ますが、いわゆる拡張機能であり POSIX では標準化されていません。[[ ... ]] が使えないのは dash、NetBSD sh、FreeBSD sh などのいわゆる ash 系と呼ばれるシェルですが、ash 系であっても、最近の BusyBox ash では [[ ... ]] を使うことが出来ます。ただし少々問題があるので注意が必要です(この記事の後半の「注意 BusyBox ash の [[ ... ]] の問題点」を参照してください)

実は POSIX では [[ ... ]] を標準規格に含めるかの議論 (0000375: Extend test/[...] conditionals: ==, <, >, -nt, -ot, -ef) は長い間続いており、将来追加される可能性があります。全体的には追加する流れのようにも見えますが、シェルの仕様を複雑にするだけでメリットはないという意見もあり、個人的には標準化される可能性 6 割、されない可能性 4 割ぐらいではないかと思っています。

「シェルビルトインコマンド」と「シェルの文法」

[ ... ][[ ... ]] はどちらもシェルが持っている機能ですが、[ ... ] はシェルビルトインコマンドで、[[ ... ]] はシェルの文法という違いがあります。このシェルビルトインコマンドとシェルの文法はどういう違いがあるのか?がこれから説明する内容です。

歴史から [ ... ] と [[ ... ]] の違いを知る

シェルの歴史をさかのぼりながら、どのような経緯で [ ... ][[ ... ]] が誕生していったのかを見ていきましょう。それを知ることによって、この二つにどのような違いがあるのかも知ることが出来るでしょう。

最初のシェルの機能は限りなく小さく

UNIX は人が CLI インターフェースを通して対話的に使える初期の OS の一つです。シェルの起源は UNIX ではなく CTSS と言われていますが、それでも参考となるようなシェルはほとんどなく、UNIX 開発者が新しく考えだしたものです。最初のシェルは 1971 年の Thompson shell ですが、そのシェルにはフロー制御の機能すらありませんでした。ifgoto が必要だとわかった時、それらは外部コマンドとして実装されました。

シェルに限りませんが、言語自体の基本機能は最小限にするのが良いと考えられています。小さくすることで実装が簡単になり修正することも容易になります。言語の基本機能以外はライブラリとして実装されます。シェルスクリプトにとってのライブラリは外部コマンドに相当します。拡張することが可能であれば if コマンドや goto コマンドのように言語自体を修正しなくても後から拡張することが出来ます。

実は [ コマンド (正確には test コマンド)や似たような機能を持っている expr コマンドが実装されたのは 1979 年の Version 7 Unix からです(参考)。シェルにはそのような機能はありませんでした。じゃあそれまではどうしていたのか?というと「シェルにはそのような機能は必要ない」と考えられていたのだと思います。もちろん最終的には必要だとわかって実装されることになるわけですが、コマンドを実行してコマンドを連携することが目的のシェルにとっては、比較機能の優先度は低かったわけです。

ここでじゃあ実行したコマンドの正常終了判定はどうするんだ?と思うかもしれません。つまり以下のようなコードです。

cmd
if [ $? -eq 0 ]; then
  echo "cmd は正常に実行されました"
else
  echo "cmd の実行は失敗しました"
fi

実は上記の書き方は冗長な書き方で、このような書き方をする必要はありません。以下のように書くのをおすすめします。

if cmd; then
  echo "cmd は正常に実行されました"
else
  echo "cmd の実行は失敗しました"
fi

Thompson shell の場合は以下のように書くことが出来ました。

if { cmd } goto success
echo "cmd の実行は失敗しました"
exit
:success
echo "cmd は正常に実行されました"

これらの例からわかるように、シェルスクリプトの最小限の使い方としては [ ... ] は必須の存在ではないわけです。

やっぱり [ ... ] はあった方がいいよね

シェルスクリプトにとって必須ではない(優先度が低い)と考えられていた比較コマンドも、プログラミング可能な十分な機能を持ったシェルスクリプトが求められ、あると便利なものであると認識されました。そして Version 7 Unix で実装されました。最初は [ ... ] はなく test コマンドだけです。シェルの機能は最小限にするために test コマンドは外部コマンドとして実装されました。最初は [ ... ] という書き方はなく、以下のような書き方をしていました。

if test 2 -lt 10; then
  echo "success"
fi

しかし、ここであることを思いついたのでしょう。「test コマンドの名前(別名)を [ にしたら良いのではないか?」と。1981 年の System III で [ コマンドが作られました。このアイデアが今となっては良かったのかどうか判断は難しいところですが、こうしてシェルスクリプトの言語を変更することなく、C 言語の括弧のような以下のような書き方ができるようになりました。

if [ 2 -lt 10 ]; then
  echo "success"
fi

こうして外部コマンドの [ コマンドが誕生します。(注意 次の [, test のシェルへの組み込みの方が先かもしれません)

シェルに [ を組み込こんで速くしよう

Bourne シェルの誕生によってシェルスクリプトでのプログラミングが大幅に改善されました。それによってシェルスクリプトで行うことも増えていき [ コマンドはよく使われるコマンドの一つとなりました。

今も変わらずですがシェルスクリプトが遅くなる原因は外部コマンドの呼び出しです。1980 年時代のコンピュータであればなおさらでしょう。よく使うコマンドが外部コマンドだとパフォーマンスに大きな影響を与えます。そういうわけで test コマンドおよび[ コマンドは 1981 年の System III の Bourne シェルに組み込まれてシェルビルトインコマンドになります。

シェルビルトインコマンドとは、基本的には外部コマンドと同等のものをパフォーマンス上の理由でシェルに組み込んだものです。しかしシェルに組み込むことでシェルの内部情報にアクセスすることが可能になります。シェルビルトインコマンドは外部コマンドでは不可能なことを実現するためにも使われています。

単語分割とパス名展開について

[[ ... ]] が誕生した理由を理解するには、シェルが持っている単語分割とパス名展開の機能について知る必要があります。の前におそらく常識だと思いますが、シェルでは文字列をクォートしてもしなくても、特殊文字が含まれない限り同じ意味になるということの再確認です。

# 以下は全て同じ意味(同じ出力を得る)
echo foo        # => foo
echo "foo"      # => foo
echo 'foo'      # => foo
echo "f"'o'"o"  # => foo

単語分割

単語分割というのは、変数に入っている文字列を空白などの文字(正確にはシェル変数 IFS に設定されている文字)で分割し複数の引数にすることです。具体的な動作は以下のコードのようなものです。(注意 zsh ではデフォルトで単語分割が無効になっているので動きません。bash などで試してください)

var="foo bar baz"

func() { echo $#; } # 引数の数を出力する関数

# 空白で単語分割され 3 つの引数として解釈される
func $var # => 3

# ダブルクォートをすれば単語分割が行われない
func "$var" # => 1

# 以下のように一つの変数に複数のオプションを入れたいときには一応便利
opts="--foo --bar --baz"
cmd $opts

単語分割は最後の例のように、複数のオプションを一つの変数に入れるときなどに使い道はありますが、色々と罠もありこの仕組みは悪手だったとも言われています。そのため zsh ではデフォルトで無効になっています。その他のシェルで単語分割が今も有効なのは互換性上の理由からです。単語分割をなくすには前提として配列の機能がシェルに実装されていなければなりません。現在のシェルの直接の祖先である Bourne シェルには配列の機能がなく単語分割は必要悪でした。また今も ash 系のシェルに配列は実装されていません。

単語分割の罠の例

var="foo bar"

[ $var = "foo bar" ] # 「引数が多すぎます」といエラーになる
# ↑ は [ "foo" "bar" = "foo bar" ] と解釈されるため

[ "$var" = "foo bar" ] # このように書くのが正しい
file="..."
[ -f $file ] 

# もし file が空文字の場合以下のように解釈され、空文字ではないため真となる
[ "-f" ] 

# もし file が "foo bar" のような空白が含まれたパスの場合
# 以下のように解釈され「二項演算子が予期されます」というエラーになる
[ -f foo bar ] 

[ -f "$file" ] # このように書くのが正しい

単語分割の歴史をたどると Bourne シェルより前の Thompson shell から存在しており、おそらく CTSS の実装が参考になっているのではないかと思っています。基本的に単語分割の機能は必要ありません。どうしても必要でない限り、文字列に変数が含まれる場合は基本的にダブルクォーテーションで括り、単語分割が機能しないようにしてくださいShellCheck を利用してシェルスクリプトをチェックすればダブルクォート忘れのミスを簡単に検出することが出来ます。

関連する話として case の場合は、ダブルクォーテーションで括らずとも単語分割が行われません。単語分割を行う意味がないためそのようにしたのだと思います。

var="foo bar"

case $var in # ダブルクォーテーションで括る必要はない
  "foo bar") echo ok
esac

パス名展開

パス名展開は引数に *? などが含まれている場合に、パターンにマッチするファイル名に展開される機能です。

$ ls -1 /etc/ssl
certs
openssl.cnf
private

$ echo /etc/ssl/*
/etc/ssl/certs /etc/ssl/openssl.cnf /etc/ssl/private

$ echo "/etc/ssl/"* # ダブルクォーテーションで括りたい場合の書き方
/etc/ssl/certs /etc/ssl/openssl.cnf /etc/ssl/private

$ echo /etc/ssl/x* # 【注意】ファイルが見つからない場合はそのまま出力される
/etc/ssl/x*

パス名展開はコマンドを呼び出す場合などに行われます。その他の例としては for ... in でも展開されます。

for f in /etc/ssl/*; do
  echo "$f"
done

# 以下のように出力される
# /etc/ssl/certs
# /etc/ssl/openssl.cnf
# /etc/ssl/private

ただし case では展開されません。

# echo では展開される
echo /etc/ssl/cert? # => /etc/ssl/certs

# case では展開されない
case /etc/ssl/cert* in         # パス名展開は行われない
  /etc/ssl/certs) echo "match" # ここには来ない
esac

case /etc/ssl/certification in
  /etc/ssl/cert*) # パス名展開は行われない
    echo "match"  # ここに来る
    # /etc/ssl/certs に展開されるわけではないということ
esac

なぜ echofor ... in の引数では展開されて、case では展開されないのかというと、それはそのようにシェルの文法でそのように決まっているからです。なおパス名展開は set -f で無効にすることが出来ます。

ksh88 で [[ ... ]] が追加された

もしかしたら [[ ... ]] は bash の拡張機能だと思っているかもしれませんが、最初に導入されたのは ksh88 です。bash は ksh88 および ksh93 と互換性があるシェルとして開発されました。

ksh88 では [[ ... ]] の他に、(( ... ))$(( ... )) も導入されました。振り返ると Bourne シェルには [ ... ]( ... ) (サブシェル)はすでにありました。括弧一つがすでに使われていために括弧二つの文法を考えだしたのでしょう。なお括弧二つの文法は POSIX では $(( ... )) のみが標準化されており、どのシェルでも使うことが出来ます。

ksh88 で [[ ... ]][ ... ] を置き換えるシェルスクリプトの文法として導入されました。[ ... ] はシェルビルトインコマンドですが、[[ ... ]] はシェルビルトインコマンドでは不可能なことを実現しています。その一つに正規表現比較 (=~) を思いつくかもしれませんが、ksh88 には正規表現比較の機能はありません。

シェルビルトインコマンドと文法はどちらもシェルに実装されている機能ですが、コマンドと文法という違いがあります。文法にすることで何を実現したのか? どうしてそれがシェルビルトインコマンドでは実現できないのか? を見ていきましょう。

[[ expression1 -eq expression2 ]]

これは外部コマンドの [ ... ] では実現不可能ですが、シェルビルトインコマンドの [ ... ] では実現可能です。シェルの文法である必要はありません。以下のような機能です。

# 数式を書くことが出来る(外部コマンドでもこれなら実現可能)
[[ "1 + 2" -eq 3 ]] && echo "ok"
[[ "1 + 2" -eq 4-1 ]] && echo "ok"

# 変数をそのまま使える(実現するにはシェルに組み込まれていなければならない)
exp="1 + 2"
[[ exp -eq 3 ]] && echo "ok"

最初の例はやろうと思えば外部コマンド版 [ ... ] でも演算子 -eq が指定された場合に、その前後の引数を数式として解釈すればよいので実現可能でしょう。ただ外部コマンドでこれをサポートしているものがあるのかどうかは不明です。

二番目の例を実現するには、シェルビルトインコマンドかシェルの文法でなければ実現できません。なぜならシェル変数はシェルが内部で持っている情報なので、外部コマンド(別のプログラム)から読み取ることができないからです。環境変数なら外部コマンドからでも読み取れますが、計算のためだけに環境変数にしたい人はいないでしょう。

これらの使い方は「面白い」という評価はできるのですが、実際の所使う必要はありません。算術式展開を用いて、しかも POSIX 準拠で書くことが出来るからです。

[ $((1 + 2)) -eq 3 ] && echo "ok"
[ $(($exp)) -eq 3 ] && echo "ok"

[[ -o option ]]

あんまり使われたことがなさそうなこの機能は、シェルのオプションが有効かどうかを調べるための機能です。これは外部コマンドでは実現不可能ですが、シェルビルトインコマンドでは可能です。シェルの文法にする必要はありません。

set -a # set -o allexport と同じ
[[ -o allexport ]] && echo ok

# これも動作する
[ -o allexport ] && echo ok

なぜ外部コマンドで実現することができないのかというと、シェルのオプションはシェルが内部で持っている情報だからです。外部コマンドからその情報を参照することは出来ないので、少なくともシェルビルトインコマンドである必要があります。

余談ですが POSIX 準拠の方法として $- を参照することで同等のことが出来ます。

set -a
echo $- # => isam など(aが含まれている)

set +a
echo $- # => ism など(a が含まれていない)

しかしこの方法は一文字名前のオプションにしか対応できません。わかりづらく pipefail のように対応する一文字のオプションがない場合には使えません。特定のオプションに関しては -o を使わないと調べることが出来ませんが、シェルスクリプトの汎用ライブラリとかでもない限り、自分がどのように設定したかは自分で知っているわけで、あまり必要にならない気もします。

[[ string1 < string2 ]]

一部の演算子は [ ... ] の中で(そのままの形)で使うことが出来ません。例えば < はリダイレクトの記号としてみなされます。

[[ "a" < "b" ]] && echo ok

# [ "a" < "b" ] && echo ok
# は以下のように解釈される(b というファイルを開こうとする)
# [ "a"    < "b" ]    &&    echo ok

しかしこれは < をクォートまたはエスケープすることで使うことが出来ます。

[ "a" \< "b" ] && echo ok
[ "a" '<' "b" ] && echo ok

したがって、これは外部コマンドの [ ... ] であっても実現可能です。しかし、いちいちクォートやエスケープをするのは面倒ですよね? [ がコマンドである以上、コマンドの文法規則に縛られますが、[[ ... ]] は別のシェルの文法であるため、そのまま < と書くことが出来ます。

[[ expression1 && expression2 ]]

前項の例と似た話ですが &&|| はシェルの AND や OR の機能であるため、そのまま書くことが出来ません。

[[ 1 -lt 2 && 3 -lt 4 ]] && echo ok

# [ 1 -lt 2 && 3 -lt 4 ] && echo ok
# は以下のように解釈される(終わりの ] が存在しない)
# [ 1 -lt 2    &&     3 -lt 4 ]     &&    echo ok

これも同じくクォートまたはエスケープすることで使うことが出来ます。

[ 1 -lt 2 \&\& 3 -lt 4 ] && echo ok
[ 1 -lt 2 '&&' 3 -lt 4 ] && echo ok

[[ ... ]] の方が見やすく書けるという利点はありますが、[ ... ] であっても括弧の外に出すことで、同等の処理を書くことが出来ます。実際の所なくてもあまり困らなかったりもします。

{ [ 1 -lt 2 ] && [ 3 -lt 4 ]; } && echo ok

[[ $varname = string ]]

これは変数をダブルクォーテーションで括らなくて良いというものでシェルの文法でなければ実現できないものです。以下のような機能です。

var="foo bar"
[[ $var = "foo bar" ]] && echo "ok"

# 以下のコードは「引数が多すぎます」のエラーになる(注意 zsh では正しく動作します)
# [ $var = "foo bar" ] && echo "ok"

[ ... ] では実現出来ない理由は、(zsh を除き)単語分割が行われるからです。ダブルクォーテーションで括っていない変数は、単語分割が行われ複数の引数になる可能性があります。[[ .... ]] が単語分割が行われない理由はシェルの文法のルールとして単語分割を行わないという仕様に決めたからです。ダブルクォーテーションで括らなくてもいいというメリットはあるものの、括ればいいとも言えます。

[[ string = pattern ]]

これはシェルの文法でなければ実現できないものです。以下のような機能です。

[[ abc = a* ]] && echo "ok"
[[ abc = "a"* ]] && echo "ok"

# 以下のコードはマッチしない
# [[ abc = "a*" ]] && echo "ok 

これは同じくシェルの文法である case を使った以下の書き方と同等です。

case abc in
  a*) echo "ok"
esac

case abc in
  "a"*) echo "ok"
esac

# 以下のコードはマッチしない
# case abc in
#   "a*") echo "ok"
# esac

シェルの文法でなければ実現できない理由はパス名展開が行われてしまうからです。これをもし [ ... ] で書いてしまえば以下のような挙動になってしまいます。

# カレントディレクトリに a で始まるファイル apple がある場合
# [ abc = a* ] は ↓ のように解釈されマッチしない
[ abc = apple ] && echo "ok"

# たまたまカレントディレクトリに a で始まるファイル abc がある場合
# [ abc = a* ] は ↓ のように解釈されマッチする
[ abc = abc ] && echo "ok"

# カレントディレクトリに a で始まるファイル apple と apricot がある場合
# [ abc = a* ] は ↓ のように解釈され「引数が多すぎます」のエラーになる
[ abc = apple apricot ] && echo "ok"

# カレントディレクトリに a で始まるファイルがない場合
# [ abc = a* ] は ↓ のように解釈されマッチしない
[ abc = "a*" ] && echo "ok 

case はコマンドと明らかに違う文法だとわかるため、異なる処理をしていても気にならないかもしれませんが、[ ... ][[ ... ]] は同じように見えてしまうため、シェルビルトインコマンドとシェルの文法という違いを知らなければ、なぜ挙動が違うのだろう?と悩むことになるでしょう。[[ string = pattern ]] を実現するためにはコマンドではなくシェルの文法としなければなりません。

-eq, -ne, -lt, -gt, -le, -ge は廃止

ksh88 では (( ... )) が実装されたため -eq, -ne, -lt, -gt, -le, -ge を使う必要はありません。以下のように書きます。

if ((2 < 10)); then # 括弧内のスペースはなくとも良い
  echo "ok"
fi

var=2
((var < 10)) && echo "ok"

ksh88 では特に書かれていないようですが、実は ksh93 ではこれらは廃止 (obsolete) されたと明記されています。おまけで = も廃止で == を使えとなっています。(参照

The following obsolete arithmetic comparisons are also permitted:
       exp1 -eq exp2
              True, if exp1 is equal to exp2.
   ︙

これらの廃止された演算子と追加機能を総合すると ksh では数値関連の比較には ((...))を、文字列の比較や、それ以外のテストには [[ ... ]] を使って欲しいのだろうという考えが読み取れます。

POSIX 準拠、または ash 系との互換性を考えると ksh93 で廃止となっているから言って簡単にそれに従うということも出来ないのですが、少なくとも ksh に関しては(初心者が?)分かりづらいと言っている -eq などを廃止して、C 言語風に書けるようにシェルを改良しているということがわかると思います。

個人的には、[[ ... -eq ... ]] だろうが ((... == ...)) だろうが、見た目が少し違うだけじゃないかと思うのですが、POSIX および ash 系がもう少し新しい機能を取り入れるという方針をとっていたのならば、今頃はシェルへの不当な苦情も減っていたのではないかと思います。

ksh88 の機能のまとめ

まとめるとこのようになります。

外部コマンド シェルビルトインコマンド シェルの文法
[[ exp1 -eq exp2 ]] - 実現可能 実現可能
[[ -o option ]] - 実現可能 実現可能
[[ string1 < string2 ]] エスケープが必要 エスケープが必要 実現可能
[[ exp1 && exp2 ]] エスケープが必要 エスケープが必要 実現可能
[[ $varname = string ]] - - 実現可能
[[ string = pattern ]] - - 実現可能

ksh88 で追加された機能のうち、一部はシェルビルトインコマンドでも実現可能であったり、エスケープをすることで外部コマンドでも実現可能なものがあるものの、シェルの文法にすることでしか出来ないものがあったり、より可読性が高い記述ができるようになるというのがわかると思います。これが ksh88 で従来のコマンド [ ... ] から新しいシェルの文法 [[ ... ]] に置き換えようとした理由です。ただし [[ ... ]] がどうしても必要なのか?と言われれば、代替方法はあるので無くても良かったりします。

ksh93 で [[ ... ]] の機能が強化された

ここまでの話で [ ... ][[ ... ]] の違いは十分理解できると思います。この後の話は応用に過ぎませんが、ksh93 の機能についてもう少し解説したいと思います。

[[ string =~ regexp ]]

[[ ... ]] を使う理由の一つが、この正規表現比較です。以下のように書きます。

string="a123"
[[ $string =~ ^a([0-9]+$) ]] && echo ok

正規表現のパターンにはシェルのメタ文字(( ) など)が含まれるため、シェルの文法でなければ実装できません。逆に言えばシェルのメタ文字をエスケープすれば [ ... ] でも正規表現比較を実装することは可能です。これをやっているが yash と ksh93u+m です。この二つのシェルでは以下のコードが動作します。

string="a123"
[ "$string" =~ ^a\([0-9]+\$\) ] && echo ok

ここでふと [[ string = pattern ]] のことを思い出すかもしれません。正規表現比較は [ ... ] でも実装が可能ですが、パターンによる比較は [ ... ] では実装が不可能です。似たような機能なのにこのような違いがある理由は演算子にあります。=~ を使っている場合は演算子の右側が正規表現であることが明らかですが、= は文字列比較なのかパターン比較なのかが演算子からわかりません。したがって [[ string = pattern ]] はシェルの文法でなければ実装が出来ないのです。

正規表現の比較はシェルのメタ文字をエスケープすれば [ ... ] では実装が可能であるものの、正規表現でシェルのメタ文字を使うことは多いので、可読性のために [[ ... ]] を使う理由は大きいでしょう。

[[ -o ?option ]]

[[ -o option ]] はシェルのオプションが有効かどうかを調べるためのものでしたが、[[ -o ?option ]] はシェルのオプションが存在するかどうかを調べるためのものです。シェルのバージョンによって実装されているオプションが異なるため、それを調べるためのものでしょう。ksh と mksh と yash で使うことが出来ます。bash と zsh では対応していないようです。次のように使います。

[[ -o ?allexport ]] && echo ok

これも同じくシェルビルトインコマンドかシェルの文法である必要があります。また [ ... ] で使うことも出来ます。しかしパス名展開が行われる可能性があることに注意する必要があります。

# 一見動くがよくない(パス名展開が行われる可能性がある)
[ -o ?allexport ] && echo ok

# touch "xallexport"
# [ -o ?allexport ] && echo ok # xallexport に展開されるためマッチしない


# クォートかエスケープをしなければいけない
[ -o '?allexport' ] && echo ok
[ -o \?allexport ] && echo ok

大抵の場合はクォートやエスケープなしでも問題なく動いてしまいます。なぜならカレントディレクトリに「任意の一文字+オプション名」というファイルがないからです。しかしたまたまそのような名前のファイルがあった場合、誤動作してしまいます。[[ ... ]] の場合はパス名展開が行われないのでそのような問題はありません。

? を使うというアイデアは面白いももの、実際には特定の場合にのみ発生する気づきにくい新たな罠を生み出しているので、このアイデアはよくなかったと思います。[[ ... ]] のみで使えるのならまだしも、[ ... ] で使えるようにするのであれば別の演算子を使うか別の記号を使った方が良かったのではないかと思います。

[[ -v varname ]] と [[ -R varname ]]

-v は指定された変数が定義 (unset) されているかどうか?を調べるもので、-R は変数が名前参照変数(name reference) かどうかを調べるための演算子です(名前参照の説明は省略します)。使い方に難しいことはありません。

var=foo
[[ -v var ]] && echo ok

typeset -n var
[[ -R var ]] && echo ok

この二つは、シェルの文法にする必要はなく [ ... ] でも普通に使うことが出来ます。

ksh93 の機能のまとめ

まとめるとこのようになります。

外部コマンド シェルビルトインコマンド シェルの文法
[[ string =~ regexp ]] エスケープすれば可能 エスケープが必要 実現可能
[[ -o ?option ]] - エスケープが必要 実現可能
[[ -v varname ]] - 実現可能 実現可能

注意 BusyBox ash の [[ ... ]] の問題点

ここまで [[ ... ]] はシェルの文法であると説明してきました。この例外が BusyBox ash です。現時点での最新の 1.35.0 では [[ というシェルビルトインコマンドとして実装されています。すでに説明している通り、以下の機能はシェルの文法でなければ実装することができません。

外部コマンド シェルビルトインコマンド シェルの文法
[[ $varname = string ]] - - 実現可能
[[ string = pattern ]] - - 実現可能

これにより BusyBox ash は [[ ... ]] が利用可能であるものの、他のシェルとの非互換性があります。

var="foo bar"
[[ $var = "foo bar" ]] && echo ok # エラー「bar: unknown operand」になる

[[ abc = "a*" ]] && echo ok # マッチしてはいけないのにマッチしてしまう

最初の例はダブルクォーテーションで括る、二番目の例は case を使うことで置き換えが可能でしょう。

また以下の使い方もシェルビルトインコマンドで実装しようとするとエスケープが必要となり、エスケープしてしまうと他のシェルでは動かない、または違う意味となる可能性があるため、こちらも非互換性が発生するでしょう。

外部コマンド シェルビルトインコマンド シェルの文法
[[ str1 < str2 ]] エスケープが必要 エスケープが必要 実現可能
[[ exp1 && exp2 ]] エスケープが必要 エスケープが必要 実現可能
[[ string =~ regexp ]] エスケープすれば可能 エスケープが必要 実現可能
[[ -o ?option ]] - エスケープが必要 実現可能

これらはおそらくバグとして将来修正されるのではないかと思いますが、今は少し注意が必要です。

補足 本当に [ ... ] よりも [[ ... ]] を使うべきなのか?

[ ... ] よりも [[ ... ]] が優れているのは事実です。シェルビルトインコマンドからシェルの文法に変更することで、よりシンプルな記述ができるようになりました。よく見かけるシェルスクリプトのベストプラクティスでも [[ ... ]] を使えと書かれてあることが多いです。しかし本当に [[ ... ]] を使う方が良いのでしょうか?

まず一つ、[[ ... ]] は POSIX で標準化されていないために、dash など使えないシェルがあります。使えないシェルはどうしようもないので、[ ... ] を使う必要があります。

では bash などに限定した場合はどうでしょうか? 論点はどこまでちゃんとシェルスクリプトの言語を理解しているかだと思います。確かに [[ ... ]] は変数をクォートしなくても良いのですが、[ コマンドを含むすべてのコマンドはクォートしなければいけません[[ ... ]] だけ例外のように見えます。

[[ ... ]] を使えというのは、単語分割やパス名展開といったシェルの基礎知識をちゃんと学ぶことがセットになっていると思います。そしてこの記事で説明したように、シェルの文法とはどういうことなのかも正しく理解しておく必要があります。理解せずにただベストプラクティスだということで [[ ... ]] を使っているだけでは、他のコマンドの呼び出しなどでダブルクォートの必要性を見落としてしまいます。であれば「[ ... ] を使え、変数はダブルクォートしろ」をシェルスクリプトの初心者のための第一歩として教えた方がいいのではないかとも思います。

いずれにしろ、シェルスクリプトは簡単に覚えることが出来る言語ですが、まったく勉強しないで使える言語ではありませんので、ちゃんとシェルスクリプトの文法を学ぶ必要があります。

まとめ

[ ... ][[ ... ]] はコマンドとシェルの文法の違いです。その違いによって [[ ... ]] を使わなければ出来ないことができたり、簡潔に記述することが可能になりました。これは似たようなものを二つ作ったのではなく、ksh の開発者がそれまでのシェルスクリプトの言語の欠点を解決しようとした歴史的事情によって作られたものです。何もなかった所から始まり test コマンドが作られ、[ ... ] という記述ができるようになり、シェルビルトインコマンドになり、そして ksh88 で [[ ... ]] が追加されてシェルスクリプトの可読性が向上したというのが歴史の流れです。小さなものから始めて大きなものへと育てていくという流れです。

互換性を切り捨てれば、新しく [[ ... ]] を作らずとも [ ... ] を置き換えることで実現はできたでしょう。しかし互換性はもっとも重要なものであるため、そのようなことは安易に行うことは出来ません。それまでのものを残しながら新しく追加するしかなく、それが ksh88 で新しく作られた [[ ... ]](( ... ))$(( ... )) です。これらは括弧が二つという共通点があります。似たようなものがあって混乱するとは思いますが、よく調べればシェルスクリプトの文法は一貫性のある形で拡張されているということがわかると思います。

シェルの文法である [[ ... ]] はシェルにとって必要な機能だったのか?と問われれば悩むところです。可読性が高く書けるというのは事実ですが、記事本編で説明したように代替手段があるため無くてもそんなに困らなかったりもします。私個人としては機能としては必須ではないかもしれないが、すでに多くのシェルに実装されているので移植性のために標準化した方が良いという考えです。[[ ... ]] が使えなくて動かなかったという話はよく聞きます。ただし実装されてないシェルにとっては新しく文法を追加する必要があり大きめの修正となってしまいます。POSIX で標準化されてないから ash 系のシェルが実装しない。実装されないから POSIX で標準化できないというデッドロックになってシェルの改善が妨げられている感じもします。

言語設計者の気持ちになれば、シェルスクリプトの文法がどうしてこのようになっているのかをより深く理解することが出来るでしょう。シェルスクリプトの言語は完璧ではありませんが、いきあたりばったりでデタラメに設計された言語などではありません。互換性を維持しつつ改善しようとちゃんと考えて作られているのです。

143
161
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
143
161