144
159

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

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 年からシェルに組み込まれているようです。Solaris 10 / 11 にはそもそも /bin/[(または /usr/bin/[)がありません。

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

実際のシェルでは [ ... ] はシェルに組み込まれていますが、POSIX では [ ... ] はシェルに組み込む必要がないとなっており、理屈上は [ ... ] をシェルに組み込まない POSIX 準拠のシェルの実装は可能でした。これは POSIX.1-2024 でも同じなのですが、POSIX.1-2024 TC1(Technical Corrigenda 1: 細かい間違いの修正)で [ ... ] をシェルに組み込むことが要求される見通しです(参照)。変更の理由はシェルに組み込まなければいけない、見落としていた理由が見つかったためです。シェルに組み込まない案もありましたが、実際のシェルにはすでに [ ... ] は組み込まれているわけで、その事実をそのまま標準化したほうが簡単であるという意見が採用されました。したがって近い将来(本年度中の予定)に「POSIX でも test[ ... ] はシェルに組み込むことが規定されている」となる予定です。

[[ ... ]] は 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) が長い間続いていました。しかし POSIX はシェルスクリプトの移植性を高めることができるかどうかで、標準化するかしないかを決めています。[[ ... ]] の方が優れているというのは事実であるものの、[[ ... ]] のシェルの実装に細かい違いがあり、標準化しても移植性は向上しないという結論に達し、標準化しないことになりました。

なぜそういう結論になるのか、よくわからない人のための補足すると、POSIX はシェルの実装者に仕様を大幅に変更してくれとは言わないからです。シェルの仕様を大きく変更すると互換性が保てなくなり過去のシェルスクリプトが動かなくなってしまいます。可能な限り現状の仕様のまま、移植性があるシェルスクリプトを書くにはどうすればよいか?を標準化しています。つまり [[ ... ]] の仕様を共通のものに変更するのではなく、違いがある [[ ... ]] を使ってシェルスクリプトの移植性は高められるか?が焦点でした。議論の結果は高められないという結論なので、シェル開発者に開発の負担を与えることの無いよう標準化しないことになりました。したがってこの状況が変わらない限り、つまり多くのシェル実装者が [[ ... ]] の互換性や移植性を高めない限り [[ ... ]] が POSIX で標準化されることはないでしょう。

[ ... ]と[[ ... ]]は機能的にはほとんど同じ

[[ ... ]] は POSIX で標準化されないことになりましたが、[[ ... ]] の標準化が望まれていた理由の一つは機能の多さです。POSIX で標準化されていた [ ... ] の機能は古い時代のものであり、[[ ... ]] の方が高機能でした。しかし実際のシェルの [ ... ] は POSIX の機能よりも高機能になっており、[[ ... ]] でできることの多くがすでにシェルに追加されていました。つまり現実の多くのシェル(dash や FreeBSD sh など)の [ ... ][[ ... ]] とほぼ同等の機能を持っていたのです。

そのため [[ ... ]] を標準化しない代わりに、すでに多くの [ ... ] に実装されていた拡張機能を標準化しました。したがって、以前より [ ... ][[ ... ]] の機能はほぼ同じであり、POSIX.1-2024 の改定により、標準規格の内容もほぼ同等になっています。ただし、それでもわずかに(あまり使わない機能に)[[ ... ]] だけしかできないことが残っています。

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

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

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

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

Bourne シェル時代

[ ... ] の誕生は 1979 年の Bourne シェルの誕生までさかのぼります。Bourne シェルはシェルが真のプログラミング言語として使えるように大きく進化したシェルです。

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

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 は 1989 年に、ksh88(のちに ksh93)と互換性があるシェルとして開発されました。多くの bash の拡張機能というのは、Bourne シェルの事実上の後継である ksh88 代替の互換シェルと開発されたものです。当時 Bourne シェルと ksh88 はクローズドソースでした。ちなみに POSIX でシェルが標準化されたのは 1992 年です。POSIX 標準規格よりも ksh88 の方が高機能であるため、現実のシェルスクリプトを動かすには POSIX で標準化されたシェルの機能を実装するだけでは役に立ちません

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

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

[[ 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

理屈上は外部コマンドでも実現可能だと考えられますが、この書き方をサポートしているシェルはおそらく存在しせず、POSIX.1-2024 でも標準化されていません。ただし [ ... ] であっても括弧の外に出すことで、同等の処理を書くことが出来ます。実際の所なくてもあまり困らなかったりもします。

{ [ 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.
  exp1 -ne exp2
         True, if exp1 is not equal to exp2.
  exp1 -lt exp2
         True, if exp1 is less than exp2.
  exp1 -gt exp2
         True, if exp1 is greater than exp2.
  exp1 -le exp2
         True, if exp1 is less than or equal to exp2.
  exp1 -ge exp2
         True, if exp1 is greater than or 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.37.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 で標準化できないというデッドロックになってシェルの改善が妨げられている感じもします。

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

144
159
1

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
144
159

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?