- Unix 系では、名前が 1 文字の記号である
/bin/[ という実行ファイルが存在することがあり、シェルの条件式のように見える構文も、実際にはコマンド実行と終了コードの上に成り立っている
test は式を評価し、真なら 0、偽なら 1 を返し、[ として呼び出された場合は最後の引数が ] かどうかまで確認する
- 多くのシェルは
test と [ を 組み込みコマンド としても提供しているため、外部の /bin/test とシェル組み込み実装では、エラーメッセージや動作が異なることがある
- Bash 拡張の
[[ は外部コマンドではなく組み込み構文であり、例のように long* をグロブ展開せずリテラル文字列として比較するなど、[ とは異なる規則を適用する
- 移植性のあるスクリプトでは
[ を使うのが適切で、Bash 専用なら [[ を一貫して使うほうがよいが、両者の展開規則の違いを理解したうえで選ぶ必要がある
/bin/[ と /bin/test の正体
- Unix システムには、名前が 1 文字の記号である実行ファイル
/bin/[ が存在することがある
- 例のコマンド
ls /bin/? は /bin/[ を表示する
/bin/[ と /bin/test は同じバイナリを指している場合がある
- 例では、2 つのパスは同じ inode とサイズを持つファイルとして表示される
- ただし、すべてのシステムでハードリンクである必要はない
test はシェルで 式を評価 するプログラムである
- 評価結果が真なら終了コード 0、偽なら 1 を返す
なぜ [ がコマンドのように動作するのか
test a = b は条件式のようには見えにくいが、同じロジックを [ a = b ] のように書くと、より見慣れた形になる
[ は別個の構文に見えるが、実際にはコマンド呼び出しである
if [ a = b ]; then ... fi は [ コマンドを実行し、その終了コードを確認する
[ として呼び出された場合、プログラムは最後の引数が閉じ角括弧 ] かどうかを検査する
if 文は条件式を直接解釈するのではなく、渡された コマンドの終了コード に基づいて分岐する
test a = a; echo $? は 0
test a = b; echo $? は 1
[ a = a ]; echo $? は 0
[ a = b ]; echo $? は 1
- 同じ文脈で
true と false も、終了コードを返す補助バイナリと見なせる
外部バイナリとシェル組み込みコマンドの違い
test と [ はシェルスクリプトで頻繁に使われるため、ほとんどのシェルはこれらを 組み込みコマンド としても実装している
- 同じ入力でも、外部バイナリとシェル組み込みコマンドでは出力が異なることがある
/bin/test a b は test: a: unexpected operator
test a b は dash: 2: test: a: unexpected operator
- このような違いは
test や [ だけでなく、echo のような単純に見えるコマンドでも起こりうる
- シェルごとに組み込み実装が異なるため、スクリプトの動作が実行するシェルによって変わる余地がある
Bash 拡張 [[ が適用する別ルール
[[ は Bash 拡張であり、[ の代わりに使える
- 最大の違いは、
[[ が 常に組み込み構文 であるという点だ
- 外部バイナリとして実行されうる
[ とは異なり、[[ では Bash が式の内部に対する言語規則を変更できる
- グロブの例では、
[ と [[ は異なる動作をする
touch long-name の後、[ long* = long-name ] && echo match は match を出力する
[ コマンドの引数には通常のシェル展開規則が適用されるため、long* はディレクトリ内の long-name に展開される
[[ long* = long-name ]] && echo match は何も出力しない
[[ は long* をリテラル文字列として扱い、long-name とそのまま比較するため失敗する
- Bash 専用スクリプトであれば、
[[ で正規表現マッチ =~ のような機能も利用できる
スクリプトでは何を選ぶべきか
- 移植性のあるシェルスクリプトでは
[ を使うのが適切である
test も使えるが、一般的な選択ではない
- スクリプトが Bash 専用なら、
[[ を一貫して使うほうがよい
- シェル自体にも
!、&&、|| のような式演算子がある
- これらの演算子はコマンドの終了状態を基準に動作する
grep ^hello$ ... && grep ^bye$ ... は、2 つのコマンドがどちらも成功すれば全体の終了コードが 0 になる
- 最初の
grep が失敗すると、&& の後ろのコマンドまで成功しないため、全体の終了コードは 1 になる
- したがって、
test/[ の式とシェルの論理演算子は、同じ条件文の中で組み合わせることができる
- 例:
[ a = b ] || grep -q ^hello$ /usr/share/dict/words
/bin/[ と /bin/test は、POSIX ではハードリンクであることを要求されていない
- NetBSD ではハードリンクだった
- macOS Catalina では、同じバイナリの別個のコピーが提供されている
- Debian testing では、互いに異なるバイナリが提供されている
- POSIX 仕様 は、2 つのファイルがリンクされていることを要求していない
1件のコメント
Hacker News のコメント
原文の著者です。共有してくれてありがとう。フロントページまで上がってうれしいです。タイトルにはおそらく (2020) を付けるのが正しく、「test」は実際にコマンドを指す語なので、大文字にしないほうがよいと思います
2021年に書いた関連する記事もあり、bash の
[[演算子まで扱っているので、この文脈では面白く読めると思います: https://jmmv.dev/2021/08/useless-use-of-gnu.html[[は厳密に言えば組み込みコマンドではなく、根本的には 構文要素に近いものです。おそらく内部的には、ほとんどアクセスできない組み込みコマンドを使っているのでしょうが、面白いのは]]も、予約語が意味を持てる位置に来られないにもかかわらず予約語だという点です一部の非 bash シェルでは、特定の種類の関数を宣言するには
functionキーワードが必要です。makeの$(shell)は、多くのターゲットをビルドする際に性能差が測定できる場合があります。それでも何もしない場合には損なので、通常は再生成を促すにはincludeを使うほうが正しいです。GNU が POSIX を無視するのは完全に妥当で、POSIX は現実の問題の大半を解くうえであまり役に立ちません.なしで検索するのも便利ですし、直前に入力したコマンドへオプションを後ろから付け足すのも本当に楽です。そういう機能をサポートしていないコマンドを見ると、いつもいら立ちますスクリプトでは POSIX
shに合わせるのが、おおむね合理的です。少なくとも Bash 専用構文を使っているかどうかは把握しておくべきです--ignore-caseやset -o pipefailのような GNU 拡張を使うとスクリプトの移植性が下がると不満を述べています。それ自体は正しいですしかし Linux ユーザーがなぜ移植性をそこまで気にすべきなのかは説明していません。OpenBSD と FreeBSD は健在ですが、ユーザー数が少なすぎるので、特に心配すべき対象ではないように思えます。公平性の観点から、そうした OS も考慮すべきだと言うことはできますが、その基準はどこで止めるべきなのでしょうか?
vxWorksのようなマイナーなものまで考慮すべきなのでしょうか? BusyBox や Alpine 方面はもっと興味深いですが、変更点が非常に大きいので、いずれにせよほぼ常に別途移植が必要です。GNU 以外のエコシステムを気にすべき、ほかの説得力ある理由はあるのでしょうか?if [ a = b ] || grep -q ^hello$ /usr/share/dict/words; then echo "test failed and grep succeeded"; fiのようなものは、単にごく普通の日常的なシェルの使い方ではないですか?make $(shell …) expansionのように表示されていますが、本来はmake $(shell ...) expansionであるべきです本文では正しく書かれているように、3つのピリオドであって単一の省略記号ではないので、
mldr自体も正しくありません。おそらく互いに無関係な2つのバグが同時に影響したのでしょう最後の要点をさらに一歩進めると、
ifブロック自体もなくせます。if [ a = b ]; then echo "Oops!"; else echo "Expected; phew!"; fiは[ a = b ] && echo "Oops!" || echo "Expected; phew!"になりますどれくらい頻繁にこうすべきかは分かりませんが、
[ "$debug" ] && echo "what's going on" >&2のように、条件付きでデバッグ出力を標準エラーに出すときには時々便利です。ifブロックが通常のコマンドを検査するという事実のおかげで、if grep -q 'debug' /var/log/nginx/access.log; then echo "Debug request found!"; fiのようなことも可能です。まだ調べていないのは、[ $(expr 1 + 1) -eq 2 ] && [ $(expr 2 + 2) -eq 3 ]と書くべきなのか、それともtestの組み込み論理積である[ $(expr 1 + 1) -eq 2 -a $(expr 2 + 2) -eq 4 ]を使うべきなのかです。性能が問題でなければ、どちらも似たような理由で妥当に見えます[ a = b ] && echo "Oops!" || echo "Expected; phew!"を一般的な規則として受け取ってはいけません。おそらく bash はこの行を([ a = b ] && echo "Oops!") || echo "Expected; phew!"のように解釈しますそのため、
&&の後のコマンド列が失敗すると、||の後のコードはいずれにせよ実行されます。たとえば>/dev/full echo "strings match"が書き込みエラーで失敗すると、文字列は一致していたのに"strings don't match"が出力されます。これはifブロックの意味とは異なりますset -eを使っているなら、if [ a = b ]; then echo "Oops!"; fiは期待どおりに動作しますが、[ a = b ] && echo "Oops!"は式aがbと等しくないときにエラーで終了します-a、-oの二項基本式と(、)演算子は廃止予定とされています。詳しくは https://pubs.opengroup.org/onlinepubs/9699919799/utilities/t... の "Application Usage" を参照してください-aを使うにせよ、2つのテストと&&を使うにせよ、bash の算術評価を使えるならexprを実行しに行く必要はありません:[ $((1+1)) -eq 2 ]数年前から
[を使わなくなりました。testは、これが構文ではなく他のコマンドと同じコマンドにすぎない、という点を強調してくれます。それにman bashを漁るよりman testのほうがずっと快適ですman testだけでなくman [のマニュアルページもありますBash には手早いチートシートとして
help testがあります。[コマンドは非常に古く、すでに1979年の Version 7 Unix に入っていました同意。
[と bash 専用の[[は、実際に何が起きているのかについて多くの混乱を生み、確信を持ちにくかったただし
[[が組み込みコマンドであることが保証される点には、シェルスクリプトの性能に意味があった時代には明確な目的があったし、それもそれほど昔の話ではないtestを使うと思う。bash スクリプトを頻繁には書かないので、if文、特に空白のルールにいつも引っかかる。理由を見てみるとあまりに当然で、testを使えば単に引数を渡しているだけだという点がよりよく分かる[とtestで最大の落とし穴は、引数が1つだけの場合の挙動。たとえば変数が空でないことを確認しようとして[ -n $FOO ]と書けるところが
FOOが設定されていないと、空文字列ではなく何もないものに展開され、[ -n ]と同じになる。POSIX は[の引数1つの形式では、その引数、ここでは"-n"が空でなければ成功しなければならないと要求している。そのため$FOOが空でないと誤って報告する。変数は必ず引用符で囲むことtest組み込みコマンドの仕様自体に落とし穴があるのではなく、落とし穴はシェルそのものにあるその挙動は筋が通っている。
[ "$FOO" ]は内容が何であれ、たとえ"-n"であっても、常に空でないかを検査する形式だから$FOOに空白が入っていると複数の引数に展開される。とにかく常に変数を引用符で囲め[ x"$FOO" != x"" ][ -n "${FOO?}" ]を使って、$FOOが null か未設定のときにスクリプトが即座に止まるようにすると思うchubot が
test/[/[[のさらに微妙な部分を掘り下げた興味深い文書を書いていた。そのブログの他の記事も、シェルのおかしな点をかなり興味深く説明している¹ https://www.oilshell.org/blog/2017/08/31.html
² https://www.oilshell.org/blog/2016/11/18.html
[がプログラムだとはまったく知らなかったし、最後の引数が閉じ角括弧かどうかを確認するという事実が少し笑えるそれでも、なぜ角括弧の両側に空白が必要なのかは説明がつく
[[は bash 専用。bash だけを使うと分かっているなら使えばよい。詳しくは記事がうまく扱っているhttps://zsh.sourceforge.io/Doc/Release/Conditional-Expressio...
[[を使えばよいzsh と ksh にもあり、実際には 1988 年かそれ以前の ksh で始まったものだとほぼ確信している
testと[は POSIX に明記されており、たいてい実際のバイナリとして存在する。ただしシェル組み込みコマンドがそれを隠すこともある一方で
[[は POSIX に明記されておらず、通常はシェル組み込みとしてしか存在しない最小公分母のシェルだけを対象にするのは、完全に古代的な心配のように感じる
最後の
if文がなぜ紛らわしいのか、よく理解できなかった。シェルスクリプトを初めて学ぶとき、[は単なる別プログラムではなく bash スクリプト言語の一部だと普通は仮定するから、ということなら理解できる。そうでないなら、なぜ驚くべきことなのか説明してほしい[がバイナリだと知らなくても、なぜ紛らわしいのかよく分からない。ごく普通の bash に見えるシェルについて強い好みがあるが、世の中の大多数とはあまり合わない
[は絶対に使うべきではなく、testだけを使うべきだと思っている。[はその仕組みが言語文法の一部であるかのように錯覚させるが、実際にはもう1つの「プログラム」にすぎない。ここでの「プログラム」には組み込みコマンドや関数も含む。if/||/&&は終了ステータスを見ており、プログラムは展開後にはただの文字列である魔法の変数$?を見る以外に、ほかのものの終了ステータスを見ることはできない。caseは文字列を見るが、終了ステータスに基づいて動作するわけではなく、case ... esacの動作の一部として終了ステータスを設定するわけでもない。「プログラム」が終了ステータスを設定する。また[/testはファイルシステム構造を評価するときだけ使うべきで、test -f /dev/nullのような場合がそれに当たる。文字列評価にはcaseを使うべきだと思う。当然、ほとんどのスクリプトを見るとかゆくなるし、自分の書いたスクリプトは他人には奇妙に見えるシェルを書くときは、
ifの次にプログラムを別行に置き、thenに進む形を好む。ifの後にあるものは「then の前の最後のコマンドの終了ステータスを見る」のだと強調するためだ。たとえばif; ls /tmp/goober; test -d /tmp/goober; echo "I'll always execute the then clause because echo will always return a 0 return code"; then ... fiのような構造では、ifは結局echoの終了ステータスだけを見るため、常に then 節が実行されるtestが入った 8 年分のスクリプトが、私が同意している証拠だ。孤独な道だ……Google シェルスタイルガイドのせいにしているshとbashの両方で動く必要があるスクリプトを書きながらtestを好む習慣がついたが、文字[をコマンドのように扱うより意味的に納得できるので使い続けている。]が別バイナリではなく[の引数だという点も奇妙だ。技術的な理由は理解しているが、ハックのように感じるtestだけを使う人が数十人はいるようだ。数十人も!正規表現マッチをしたいときだけ
[[を使う、というふうに学んだ。例:if [[ "${foo}" =~ ^bar$ ]]; then echo Yes; fiそれ以外では単に
"test"や"["を使う。bash を 85,000 行目まで書いているところだ。bash が素晴らしいと言うつもりはないが、まだ多くの用途で自分のニーズを満たしてくれるexprは基本正規表現をマッチでき、キャプチャグループも返せる1: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/e...
[ "$foo" = bar ] && echo Yesでよい部分文字列のマッチングには、
[と*グロブでたいてい十分。[ "$bar" = extra* ] && echo '$bar began with extra'のような形だ。Bashの正規表現方言は原始的なので、わざわざ使う価値はほとんどない。複雑なことはgrep、awk、perlのような別のツールを使うのが正しい。より高い再利用性、モジュール性、組み込み型が必要な複雑な作業まで、すべてbashでやろうとこだわると、得られるものは急激に少なくなる