シェルスクリプトのはなし
シェル(shell)はUnix系OSにおけるコマンドラインインターフェースです。一連の手続きをファイルに書いておいて自動実行するものを「シェルスクリプト(shell script)」と呼び、Linuxの世界では手順の自動化だけでなく初期化シーケンスの制御やイベント対応処理などいろんなところで使われます。今回はそのシェルスクリプトのおはなしです。
シェルスクリプトの基本
まず、もっとも簡単なシェルスクリプトの例を示します。
#!/bin/sh
echo Hello, world
これをtest.shとして保存し、chmod +x test.shで実行属性を付け、./test.shとして起動すると確かにHello, worldと表示されます。
文字列の部分はダブルクオート""で括っても、シングルクオート''で括っても同じ動きをします。
#!/bin/sh
echo "Hello, world"
#!/bin/sh
echo 'Hello, world'
ふーん、と思いますが、これは後で変数の参照について重要な手がかりになります。
シェルスクリプトには複数行のコマンドを書くこともできます。echoの-nは「文字列末端に改行を付けない」指示です。
#!/bin/sh
echo -n "Hello,"
echo -n " world"
echo ""
コマンドは;を付けて1行に書くこともできます。
#!/bin/sh
echo -n "Hello,"; echo -n " world"; echo ""
これも「ふーん」ですが、後でifの構文を説明するときに重要な手がかりになります。
シェル変数
シェルは変数(環境変数)を持つことができます。幾つかの変数は何もしなくても設定されており、たとえば実行コマンドを探す順序を示す"$PATH"もシェルのデフォルト変数です。シェル変数は
変数名=値
で設定することができ、
$変数名
で参照されます。設定時、=の前後にスペースを入れてはいけないことに注意してください。例えば
VAR=1
という行は「$VARという変数に1という値を設定する」と解釈されますが、
VAR = 1
としてしまうと
VAR: command not found
というエラーになります。スペースが入ると、シェルはそれを「VARというコマンドに引数=と1を付けて実行する」と解釈するからです。
通常の変数は子プロセスには引き継がれません。子プロセスに引き継ぐ変数はexportで設定します。
export 変数名(=値)
export 変数名で(たとえ未定義でも)その変数が引き継ぎ対象として設定されます。「=値」をくっつけて書くことで、値設定と引き継ぎ宣言を同時に行うこともできます。
変数を取り消すのはunsetコマンドを用います
unset 変数名
シェル変数は型を持たず、全ての値は文字列です。そしてダブル・シングルクオートも同じように扱われます。すなわち
VAR=1
VAR='1'
VAR="1"
は全部同じ結果になります。
ここで問題になるのが、値にスペースが入る場合です。例えば"1 2 3"という値を設定しようとして
VAR=1 2 3
とやれば
2: command not found
というエラーになります。シェルは変数代入文をスペースでいちど区切ると、その後に続く文字列をコマンドとして実行しようとします。つまり
VAR=1 echo ABC
と
VAR=1;echo ABC
は全く同じ動きをします。スペースの入った文字列を設定するためには、文字列全体をダブルまたはシングルクオートで括る必要があります。
VAR="1 2 3"
VAR='1 2 3'
変数の参照
代入式には右辺に変数を入れた表記を書くことが可能です。
VAR=1
VAR=$VAR
とすればVARには"1"が入りますし、
VAR=1
VAR=ABC$VAR
とすればVARには"ABC1"が入ります。
しかし、変数の後ろに文字列をくっつけるためにはどうすれば良いのでしょうか?
VAR=1
VAR=$VARABC
とやると、$VARには""が入ります。"$VARABC"は「変数$VAR + ABC」ではなく「変数$VARABC」と解釈され、そしてシェルは(デフォルトの挙動では)存在しない変数をエラーにはせずに""を返すからです。"set -u"によって未定義変数をエラーにさせることもできます。
例によって、ダブルまたはシングルクオートで括ることで「変数名」と「それにくっつける文字列」を区別することができます。
VAR=$VAR"ABC"
VAR=$VAR'ABC'
とやれば、$VARには"1ABC"が入ります。もうひとつ、変数を${}で括る表記法でも同じことができます。
VAR=${VAR}ABC
さらに、変数名の方を括ることもできます。ここではじめて、ダブルクオートとシングルクオートの違いが出ます。ダブルクオート内部の$変数名(または${変数名})は展開されますが、シングルクオートでは展開されません。
VAR=1
VAR="${VAR}ABC"
echo $VAR
1ABC
VAR=1
VAR='${VAR}ABC'
echo $VAR
${VAR}ABC
もうこの時点で十分混乱してきたかも知れません。
設定時の文法
変数名=値
単純形。値にスペースは入れられない。変数は展開される。
変数名='値'
値にスペースは入れられるが、変数は展開されない。
変数名="値"
値にスペースを入れられ、変数は展開される。
参照時の文法
$変数名
単純形。変数名に続けて値を書くことはできない。
${変数名}
変数名の明示形。変数名に続けて値を書くことができる。{}の中に拡張文法を含めることもできるが、ここでは割愛する。
'$変数名'
表記通りの文字列(リテラル)として解釈される。変数参照にはならない。
"$変数名"
変数参照つき文字列表記の応用。変数名に続けて値を書くことができる。
"${変数名}"
上と同じ。特に機能上の意味はない。これが変数であることを強調する目的で使われることがある。
同じことをやるのにたくさん違う書き方があるのは、Unixシェルの古い持病です。この性質はPerlやJavaScriptにも引き継がれましたが、Pythonはこれを敢然と否定し、「1つのことは1つの書き方でしか書けない」ように設計されています。
定義済み変数
いくつかの変数は特殊用途として定義済みになっています。
$?
直前に実行したコマンドの結果を格納します。数値0が正常終了、それ以外がエラーを示す場合が多いです。
$!
直前に実行したバックグラウンドプロセスのプロセスIDを格納します。
$$
自分自身のプロセスIDを格納します。
$#
スクリプトに渡された引数の数を格納します。C言語のargcと異なり「スクリプト名自身」は引数に含まないので、引数なしの場合は0になります。
$@,$*
スクリプトに渡された引数全体を格納します。$@と$*では微妙に動作が異なりますが、詳細は割愛します。
$1,$2,$3...
スクリプトに渡された引数を個別に格納します。歴史的理由で1桁目しか参照しないので、10番目以後の引数参照には${10}のように{}を付ける必要があります。
他にも幾つかありますが、主用されるのはこれくらいです。
シェルの数値演算
シェルは数値演算機能を持ちません。
VAR1=13
VAR2=7
VAR3=$VAR1+$VAR2
と書いても、VAR3には"13+7"という文字列が入るだけです。シェルで数値演算を行うためには、外部コマンドexprを呼び出す必要があります。
VAR3=`expr $VAR1 + $VAR2`
バッククオート``は「その内部に書かれた文字列をコマンドとして実行し、左辺に返す」働きを持ちます。なので
VAR2=`echo $VAR1 | grep "test" | sed s/^ *//"`
みたいな表記による文字列操作は、シェルスクリプトで多用されます。バッククオートは非常に古いシェルの文法なので、最近のシェルでは上位互換の$()が用いられることが多いです。
VAR3=$(expr $VAR1 + $VAR2)
exprコマンドは渡された引数を「数値または演算子」と解釈して順番に処理します。なので数値と演算子の間にはスペースを挟まなければなりません。
expr 13 + 7
は20が返りますが、
expr 13+7
は"13+7"という文字列がそのまま返されます。
さて、最近のシェルには数値演算機能が拡張されており、$(())を用いて呼び出すことができます。
VAR3=$(($VAR1+$VAR2))
exprと違って数値と演算子の間にスペースを挟む必要はありません。しかし挟んでもエラーになるわけでもないので、「好みの問題」になります。
シェルの条件判断(if文)
シェルのif文は、一般的には次のような構文で書かれます。
if [ 条件 ]; then
処理
処理...
fi
ifを閉じるのが"endif"や"done"ではなく"fi"というのが奇妙なところですが、特に合理的理由はなく、「最初に作った人がこうするのが良いと思ったから」くらいの理由でこうなっています。case文も閉じるのはesacですが、forやwhileはdoneで閉じるとか妙な非対称性があります。
「条件」のところ入るものはtestコマンドの引数に相当します。[とtestの機能・仕様は殆ど同じですが、現在のLinuxではtestと[を別々の実行バイナリにしており、/usr/bin/[と/usr/bin/testが別に入っています。
if [ "$変数名" ]; then 変数が NULL でないとき
if [ -n "$変数名" ]; then 変数が "" でないとき
if [ -z "$変数名" ]; then 変数が NULL または "" であるとき
if [ "文字列A" = "文字列B" ]; then 文字列Aと文字列Bが一致するとき
if [ "文字列A" != "文字列B" ]; then 文字列Aと文字列Bが一致しないとき
if [ "数値A" -eq "数値B" ]; then 数値Aと数値Bが一致するとき
if [ "数値A" -ne "数値B" ]; then 数値Aと数値Bが一致しないとき
if [ "数値A" -ge "数値B" ]; then 数値Aが数値Bと同じか大きいとき
if [ "数値A" -gt "数値B" ]; then 数値Aが数値Bより大きいとき
if [ "数値A" -le "数値B" ]; then 数値Aが数値Bと同じか小さいとき
if [ "数値A" -lt "数値B" ]; then 数値Aが数値Bより小さいとき
if [ -e "ファイル名" ]; then ファイルが存在するとき
if [ -x "ファイル名" ]; then ファイルが実行可能なとき
if [ -w "ファイル名" ]; then ファイルが書き込み可能なとき
if [ -r "ファイル名" ]; then ファイルが読み出し可能なとき
if [ -s "ファイル名" ]; then ファイルが0バイト以上のとき
if [ -d "ファイル名" ]; then ファイルがディレクトリのとき
if [ -f "ファイル名" ]; then ファイルが通常ファイルのとき
if [ -b "ファイル名" ]; then ファイルがブロックデバイスのとき
if [ -c "ファイル名" ]; then ファイルがキャラクタデバイスのとき
上で「シェル変数は型を持たず、全ての値は文字列です」と書いたのにここでいきなり「数値」が出てくるのは、「条件」を処理するのがシェルではなく外部のコマンドだからです。変数代入の=にスペースを入れてはいけませんでしたが、[の内側に並ぶのは[コマンドに渡される引数である以上、スペースを入れて区切らなければいけません。つまり
if [$VAR="0"]; then
はダメだし
if [ $VAR="0" ]; then
でもダメで、
if [ $VAR = "0" ]; then
と書かなければなりません。
比較コマンドの=や!=は文字列、-eqや-neは数値として解釈します。
if [ $VAR = 0 ]; then
と
if [ $VAR -eq 0 ]; then
はVAR="0"のときは同じ動作になりますが、VAR="00"のとき=は不一致判断となり、-eqは一致判断となります。「数値比較のつもりが文字列比較していた」というのはシェルのよくある落とし穴です。
また、ここで「シェルの変数は全て文字列」という前提が頭をもたげてきます。$VARが未定義あるいはゼロ長の場合、
if [ $VAR = "0" ]; then
は
if [ = "0" ]; then
と展開されるので、testコマンドは左辺が無いと怒って
sh: [: =: unexpected operator
みたいなエラーを出します。これを防ぐためには変数名を""で囲って
if [ "$VAR" = "0" ]; then
とすれば、$VARの中身がカラでも外側の""は残るのでエラーにはならずに済みます。ただしこれが通用するのは文字列比較の場合だけで、数値比較の場合は
unset VAR
if [ "$VAR" -eq 0 ]; then
と書いても
sh: [: Illegal number:
のようにエラーとなります。なんだかアホみたいな話ですが、シェルはもともとが「言語もどき」なので、こういう妙な規則は沢山あります。
「スペースを入れなければならない規則」とか「]とthenの間だけ;を入れなければならない規則」とかの妙な規則も、シェルのifがもともと別々のコマンドの寄せ集めとして実装されていたことに由来します。例えば
if [ $VAR -eq 0 ]; then
echo ZERO
fi
というスクリプト文法は、コマンドラインから1行づつ
if
test $VAR -eq 0
then
echo ZERO
fi
を打ち込むのと全く同じ動きをします。本来は別々のコマンドが順番に実行されていたものを、1行に複数のコマンドを並べて書くことで言語のように見せかけているわけです。閉じ括弧の後ろに;が付くのは「本当はここで行を切っているんだよ」という意味ですが、ifとthenはその規則に従いません。なのでifやthenに;を付けて
if; test $VAR -eq 0; then; echo ZERO; fi
と書くとエラーになります。
if test $VAR -eq 0; then echo ZERO; fi
ならエラーになりません。ここでtestの代わりに[を使うと
if [ $VAR -eq 0 ]; then echo ZERO; fi
になるわけです。ここで唐突に登場する]は、[コマンドに渡す引数の終端を意味します。testと[は殆ど同じ処理をしますがコマンドとしては別バイナリになっており、[は引数に]が渡されなければエラー扱いしますし、逆にtestに]を渡してもエラーになります。
否定論理は ! で表現されます。これも独立引数なので、スペースが必要なことに注意してください。
if [ ! $VAR -eq 0 ]; then
と
if [ $VAR -ne 0 ]; then
は同じ意味になります。
複数の評価式をORまたはAND論理でつなぐこともできます。これもtestコマンドへの引数として渡され、OR論理は-o、AND論理は-aになります。
if [ $VAR1 = yes -a $VAR2 = yes ]; then echo YES; fi
if [ $VAR1 = no -o $VAR2 = no ]; then echo NO; fi
ORとANDの混ざった条件式を書くときは注意が必要です。例えば
if [ $VAR1 -ne 0 -a $VAR2 -ne 0 -o $VAR3 -ne 0 ]; then
という条件判断はC言語で言うところの
if (var1 && var2 || var3)
あるいは
if ((var1 && var2) || var3)
と等価になります。すなわちVAR1・VAR2が0でもVAR3が1ならTRUE評価になります。ただしC言語の場合は演算子に優先順位があるので、順序を変えて
if (var3 || var1 && var2)
と書いても結果は同じになりますが(||より&&が先に評価される)シェルの場合は順序に影響されます。つまり
if [ $VAR2 -ne 0 -o $VAR3 -ne 0 -a $VAR1 -ne 0 ]; then
はVAR2とVAR3の条件がOR評価されたあとにVAR1がAND評価される動作になり、C言語で書けば
if ((var2 || var3) && var1)
と等価になり、VAR2・VAR3の値が何であれVAR1が0である限りFALSE評価になります。紛らわしいですね。
[]には()を渡して優先順位を指定することも可能ですが、()じたいがシェルのコマンド(指定された文字列をコマンドとして起動)を意味するので、「これはコマンドではないよ」という意味のエスケープ文字(バックスラッシュ)を付ける必要があります。
if [ $VAR1 -ne 0 -a \( $VAR2 -ne 0 -o $VAR3 -ne 0 \) ]; then
うーん、かっこ悪い。こんな書き方をするくらいなら
if [ $VAR2 -ne 0 -o $VAR3 -ne 0 ]; then
if [ $VAR1 -ne 0 ]; then
と、ifを2段に重ねた書いたほうが見通しが良くなると思います。
なお、ifには[(あるいはtest)以外のコマンドを渡すこともできます。
VAR=abc
if echo $VAR | grep "abc" > /dev/null; then echo ABC; fi
のような書き方をすれば、「if~thenまでの間に書かれたコマンドを実行し、その結果が0であればthen以後を実行する」という動作になります。シェルは単一の()を「コマンドの実行司令」として解釈するので、
if (echo $VAR | grep "abc" > /dev/null); then echo ABC; fi
のように括弧で括ることもよく行われます。もっとも単純なカタチとしては
if (true); then echo TRUE; fi ←表示される
if (false); then echo FALSE; fi ←表示されない
みたいな書き方もできます。true, falseは値ではなくコマンドで/bin/true, /bin/falseとして実装されており、実行結果としてそれぞれ常に0・1を返します。シェルのifにおける論理値はC言語の論理値と異なり、0がTRUE・!=0がFALSEであることに注意してください。たまに忘れて「おっかしーなー、論理は合っているはずなのに何でここのifを通らないんだ?」と悩んだりします。
また、ここでうっかり()ではなく[]を使って
if [ false ]; then echo FALSE; fi
と書くと条件判定はTRUE扱いになります。これはif (test false)と書いているのと同じであり、testコマンドは"false"を数値でもコマンドでもなく条件式として受け入れ、しかし-eqや==の付いていない文字列に対してはエラーにもせず無条件に0を返すので、ifの判定としては「常にTRUE」となるからです。
何でそんな紛らわしいことになってんだよ、理解できない条件式ならせめてエラー判定しろよと思いますが、事実としてこうなっているので仕方ありません。
拡張if文法その1
今のLinuxシステムの殆どは無印シェル(shあるいはbsh)ではなく拡張シェルを使っています。拡張シェルの代表はbashで、そしてbashには拡張ifの文法があります。拡張ifは[を2つ重ねた表記になります。
if [[ 条件 ]]; then
処理
処理...
fi
無印シェルの[]は外部コマンドでしたが、[[]]の中身はシェル内部で解釈され判断されます。=や!=が文字列比較で-eqや-gtが数値比較だとか、[[]]と条件の間・演算子の前後にスペースを入れなければならないルールは[]と共通ですが、幾つか違っているところがあります。
まず、[[]]の中で演算が出来るようになりました。たとえば
if [[ $VAR1+1 -eq 100 ]]; then
だとか
if [[ $VAR1 -eq 77+33 ]]; then
という表記が可能になります。ただしこの場合、演算子の前後にスペースを入れてはいけません。つまり
if [[ $VAR1 + 1 -eq 77 + 33 ]]; then
と書いてはいけません。相変わらず変な規則が付いて回りますね。
次に、AND/ORの条件表記が-a, -oではなくC言語互換の&&, ||になりました。
if [[ $VAR1 = "yes" && $VAR2 = "yes" ]]; then echo YES; fi
と書くわけです。[[]]では-a, -oは使えません。C言語と似た表記になりましたが、やっぱり演算子優先順位評価はありません。なので&&や||を並べて書いた場合、判定結果は順番に依存します。なので演算優先順位は()で明示しなければなりませんが、[]と異なり\なしで書けるようになりました。つまり
if [[ $VAR1 -ne 0 && ( $VAR2 -ne 0 || $VAR3 -ne 0 ) ]]; then
と書けるわけです(というか、[[]]の中で\(と書いてはいけません!)。[[]]の規則はまた変わっていて、[[]]と条件の前後、=や-eqの前後にはスペースを入れる必要がある一方、(や&&の前後にはスペースを入れなくても良いという妙なことになっています。なので
if [[ $VAR1 -ne 0&&($VAR2 -ne 0||$VAR3 -ne 0)]]; then
とくっつけて書いてもエラーにはなりません。ただし[[$VAR1とくっつけてしまうとエラーになります。閉じ側がエラーにならないのは、")"を挟んであるからです。あえてスペースを入れるか、くっつけられるところはくっつけて書くかは「好みの問題」になります。
さて、ここまで"$VAR1"ではなく$VAR1のように""括りなしで書いてきました。実は[]と異なり、[[]]では変数の中身がカラでもエラーになりません。つまり
unset VAR
if [ $VAR = "" ]; then echo EMPTY; fi
はエラーになりますが
unset VAR
if [[ $VAR = "" ]]; then echo EMPTY; fi
だとエラーになりません。文字列比較だけでなく、数値比較でもエラーになりません。ゼロ長文字列は数値0として評価されます。
unset VAR
if [[ $VAR -eq 0 ]]; then echo ZERO; fi
しかし""で括ってもエラーになる訳でもないし挙動も変わらないので、[[]]を使ったとき$VARを""で括るかどうかは「好みの問題」になります。「同じことをやるのにたくさん違う書き方があるのは、Unixシェルの古い持病です」と書きましたが、この持病はこの後もどんどん出てきます。
もうひとつ細かいことですが、[]の数値比較では
VAR="100"
if [ "$VAR" -eq 100 ]; then echo HUNDRED; fi
と
VAR="100"
if [ "$VAR" -eq 0100 ]; then echo HUNDRED; fi
は同じ実行結果(一致判定)になりますが、[[]]では
VAR="100"
if [[ $VAR -eq 100 ]]; then echo HUNDRED; fi
と
VAR="100"
if [[ $VAR -eq 0100 ]]; then echo HUNDRED; fi
は同じにはなりません。[[の中ではゼロから始まる数字は8進数として評価されるので、100 -eq 0100は不一致になります("0100"は8進数の100=64になります)。「何でそんなとこの仕様を変えるんだよ!」と思いますが、事実としてそうなっているので仕方ありません。シェルには「同じことをやるのに違う書き方がたくさんある」一方で、「同じ書き方をしても文脈によって解釈が変わる」という持病もあるのです。
拡張if文法その2
[]と[[]]の混在だけでもう十分ややこしいのに、bashには更にもう1つの拡張if文法(())があります。
if ((条件)); then
処理
処理...
fi
[や[[と異なり、((と条件の間にはスペースを入れなくても構いません(入れても構いません。これまた「好みの問題」になります)。
((は数値演算用となっており、文字列判断には使えません。演算子はC言語と同じ==,>,>=,<,<=,!=になっています。つまり
if [ $VAR -eq 0 ]; then echo ZERO; fi
if [[ $VAR -eq 0 ]]; then echo ZERO; fi
if (($VAR==0)); then
は全部同じ判定になります。ただしVARが未定義あるいはゼロ長だったとき、[[]]だけはエラーになりませんが、[]と(())はエラーになります。
(())も条件式に演算式を入れることができます。[[]]と違って演算子前後にスペースを入れても構いません。すなわち
VAR=9
if [[ $VAR+1 -eq 5+5 ]]; then echo TEN; fi
と
VAR=9
if (($VAR +1== 5 + 5)); then echo TEN; fi
は同じになります。何でこんな所の規則がいちいち違うんだよ!と思いますが、事実としてそうなっているので仕方ありません。
(())には比較演算子を使わずに数値を直接評価させることもできますが、その場合は[]や[[]]とは逆に0がFALSE, !=0がTRUEという判定になります。
VAR=1
if (($VAR)); then echo TRUE; fi ←表示される
VAR=0
if (($VAR)); then echo FALSE; fi ←表示されない
何でこういうことになるかというと、シェルのifはあくまで「if~thenの間に書かれたコマンドを実行し、その終了値が0であればthen以後を実行する」という機能だからです。(())はシェルの内部コマンドではありますがコマンドには違いなく、そして(())は演算結果が0以外であれば0を、0であれば1を返す仕様になっています。==演算子もCとは逆に「一致判定のとき0を返す」仕様になっています。
((0))
echo $?
1
((1))
echo $?
0
((1==1))
echo $?
0
[]や[[]]の-eqや==も同じように、渡された評価式の結果がTRUEであれば0・FALSEであれば1を返すコマンドなのですが、(())は数値を直接扱えるだけに、偶にこの落とし穴に嵌まることがります。
もうだいぶぐちゃぐちゃになってきたと思いますが、シェルのifについて少しまとめてみます。
- ifの書き方には (), [], [[]], (())の4種類がある。
- ()は括弧内に書かれたコマンドを実行した結果をもって判定する。()を付けなくても同じ動きになる。
- []は外部の条件評価コマンドの呼び出しであり、その文法はtestコマンドとほぼ同じである。
- [[]]はシェルの内部処理による条件評価で、文法は[]と似ているが少し異なる。
- (())もシェルの内部処理による数値演算評価で、文法は[]や[[]]とだいぶ異なる。
- 変数名や演算子の間にスペースを入れなければならない規則・入れてはならない規則は[],[[]],(())の3つで全部異なる。
- ゼロ長/未定義変数対策として、[]では変数名を""で囲むことが推奨される。[[]]では必要ない。(())では回避できないのでゼロ長を渡さない注意が必要になる。
ifのようでifでない文法
既に書いたように、シェルは複数のコマンドを;でつなげて1行に書くことができます。そして、これにもバリエーションがあります
コマンド1;コマンド2;コマンド3
通常の列挙文法で、コマンド1が終了すればコマンド2,コマンド2が終了すればコマンド3...という風に順番に実行されてゆきます。
コマンド1&&コマンド2&&コマンド3
;と似ていますが、次のコマンドを実行するか否かは直前のコマンドの実行結果に依存します。直前のコマンドの実行結果が0だったときのみ次のコマンドが実行されます。
コマンド1||コマンド2||コマンド3
&&とは逆に、直前のコマンドの実行結果が0以外だったときのみ次のコマンドが実行されます。
コマンド1&コマンド2&コマンド3
コマンド1,コマンド2,コマンド3が並行して実行されます。
このなかで&&と||は条件判断を伴うので、これをifの代わりに使うこともできます。例えば
ls | grep "test" > /dev/null && echo "TEST"
という文法は「lsの結果にtestという文字列が入っていればecho TESTを実行する」意味になり、つまり
if (ls | grep "test" > /dev/null); then echo "TEST"; fi
と同じ動作をします。この論理実行式をifと組み合わせて
if [ $VAR1 -ne 0 ] || [ $VAR2 -ne 0 ]; then echo "NOT ZERO"; fi
だとか
if [ $VAR1 -eq 0 ] && [ $VAR2 -eq 0 ]; then echo "ZERO"; fi
という書き方もできます。-oや-aより見た目がわかりやすいので、こちらを多用する人もいます。
結果的には-oや-aと同じですが、-o/-aの場合は一回の[コマンドに複数の条件式が渡されてまとめて評価された結果が返されるのに対し、&&や||は[コマンドが複数回呼び出された結果をシェルの側で論理判定する点が異なり、理屈でいえば後者のほうが実行オーバーヘッドが大きくなります。とはいえ、実行効率が問題になるような処理ならそもそもシェルスクリプトで書くべきではなく、誤差みたいな実行効率よりも見た目のわかりやすさを優先することも間違いではありません。
なお、&&や||の前後は「別のコマンド」なので、
if [ $VAR1 -eq 0 ] && (($VAR2==0)); then echo "ZERO"; fi
などと複数の異なる文法を混ぜて書くことも可能ですが、絶対にやめましょう!シェルスクリプトはただでさえわけわかんない文法規則でぐっちゃぐちゃになのに、わざわざチャンポンにして余計わかりにくくする意味なんてありません。
まとめ
elseやelif, case-esac, while, forなど他の制御構文、サブルーチン(と見せかけて実はset定義されたスクリプト)などの話題に移る前に紙面が尽きました。改めて書いてみると、変数定義・参照とifだけで変な規則が多すぎますね。()と(())、[]と[[]]、$()と$(())が全部違う機能になり、括弧の中に入れる文字列のスペース区切りの有無や""の意味が全部違うという、「よくこんなもん使ってられるな」という有様になっています。
それなりにLinuxに馴染んだつもりでも、シェルスクリプトはだいたいコピペで済ませて「同じことをやるのにたくさん違う書き方がある」うちの一部しか知らなくて、他人の書いたスクリプトを読んだとき「何やってんだこれ?」と首を傾げることも結構あります。そのスクリプトに加筆するとき、原作者の流儀には倣わずに「自分が慣れ親しんだ書き方」を突っ込むことも多く、これが数人の手を経ると複数の文法がチャンポンになった「継ぎ足し続けた秘伝のソース」になることも、これまた不幸にしてよくあることです。
「同じ変数なのに何でこっちでは""こっちでは${}で囲い、どうしてこのifの中では囲っていないんだ?」
「ifの書き方に[]と[[]]が混在しているのは何だ?何か意図や意味があるのか?」
「ここで突然&&のオンパレードが出てくるのは何だ?ifを使いたくない理由でもあったのか?」
みたいなスクリプトはけっこうゴロゴロしていて、そして大抵そこに意味や意図なんて無くて「書いた人がその文法しか知らなかったから」「他所からコピペで持ってきたから」くらいの事情です。しかし「なんか気に入らんから書き直そう」と思って下手に手を付けると、上で書いてきたような数々の落とし穴に嵌って突然挙動不審になったりします。文字列比較と数値比較の混用とか、「本当ならば動かないはずの評価式が偶然動いていた」というのもこれまたよくある話です。
今回は余談みたいなコラムでしたが、「あれはそういうことだったのか!」という再発見になって頂けたなら幸いです。