できない.dev

bash の if で「unary operator expected」「too many arguments」になる

[ ] の中の変数を引用符で囲んでいないと、空文字や空白を含む値が単語分割で消えたり増えたりして構文が壊れる。
変数を "$var" で囲むか、bash なら [[ ]] を使えば解消する。

公開:

要約

条件分岐を書いただけなのに、実行するとエラーが出る。

name=""
if [ $name = "alice" ]; then echo hit; fi
bash: [: =: unary operator expected

原因は [ がシェルの構文ではなくコマンドで、引数の個数がそのまま意味を決めることである。$name が空文字だと単語分割で消えてしまい、[=alice の 2 引数だけを受け取る。
2 引数の場合は「単項演算子+値」と解釈されるため、= が単項演算子として扱われて破綻する。

値に空白が入ると逆に引数が増える。

name="alice smith"
if [ $name = "alice" ]; then echo hit; fi
bash: [: too many arguments

どちらも変数を引用符で囲むだけで直る。

よくある原因

  1. 変数が空で引数が減る: 未設定の変数や空文字は、Word Splitting(新しいタブで開く) の結果として引数そのものが消える。[ $x = y ][ = y ] になり、unary operator expected が出る。
  2. 値に空白があって引数が増える: alice smith のような値は 2 語に割れる。[ alice smith = alice ] は 4 引数となり、too many arguments になる。
  3. 値がワイルドカードを含む: x="*" のときパス名展開が働き、カレントディレクトリのファイル名すべてに置き換わる。
    ファイルが 1 つなら偶然動き、2 つ以上あると突然壊れるので原因を掴みにくい。
  4. [ を構文だと思っている: [test の別名にあたるコマンドで、閉じ括弧の ] すら最後の引数でしかない。
    したがって空白の入れ方や引用符の有無が、そのまま引数の切れ目になる。
  5. 単項演算子に渡す値が空: [ -n $x ] も同じ罠を踏む。$x が消えると [ -n ] になり、これは「文字列 -n が空でないか」を判定する 1 引数の形になるため、常に真を返す。
    エラーにならず黙って誤判定するぶん、こちらのほうが厄介である。

解決策

1. 変数を引用符で囲む

もっとも基本的で、sh でも通る対処である。

name=""
if [ "$name" = "alice" ]; then echo hit; fi

引用符の中では単語分割もパス名展開も起こらないため、$name が空でも空白入りでも常に 1 引数として渡る。[ の中に出てくる変数参照は、例外なく囲むと決めてしまうのがよい。

2. bash なら [[ ]] を使う

bash や zsh 専用のスクリプトなら [[ ]] を使う。
これはコマンドではなくシェルの構文なので、Conditional Constructs(新しいタブで開く) が説明しているとおり、内部で単語分割とパス名展開が行われない。

name="alice smith"
if [[ $name = "alice" ]]; then echo hit; fi

=~ による正規表現マッチや && || の直接記述も使える。
ただし #!/bin/sh を宣言したスクリプトでは使えないので、移植性が要るなら方法 1 を選ぶ。

3. 既定値を与えて空文字を避ける

set -u を有効にしていると未設定変数の参照自体がエラーになる。
既定値展開を併用すると両方まとめて防げる。

set -u
if [ "${name:-}" = "alice" ]; then echo hit; fi

${name:-default} の形にすれば、未設定時の値も指定できる。

4. bash -x で展開結果を見る

エラーの原因は「展開後に引数が何個になったか」なので、それを直接見るのが早い。

bash -x script.sh
+ name=
+ '[' = alice ']'

トレース行に '[' = alice ']' と出ていれば、変数が消えて 2 引数になっていることが確定する。
修正後は + '[' '' = alice ']' のように空文字が 1 引数として残る形になる。

この記事は役立ちましたか?