Bash
シェルスクリプト

プログラマーの君! 勘違いするな! シェルスクリプトでは読みやすさのためにスペースを置くな!! という話

普通のプログラミング言語での開発に慣れた人ほどシェルスクリプト、特にBashで戸惑う部分の一つに、i = 0のように空白を開ければエラーになるし、かといってif[$i!=0]のように詰めてもやっぱりエラーになる、という点が挙げられます。書きたい物を思うように書けなくて「なんだよこのクソ言語は!!!」とブチギレる人は少なくないのではないでしょうか。この記事では、そのイライラを解消するポイントをお伝えしようと思います。

以下、特に断り無く「シェルスクリプト」と書いている場合はすべて「Bashのスクリプト」という意味になります。zsh等他のシェルではまた事情が異なりますので、ご注意ください。

(※以前プログラマーの君! 騙されるな! シェルスクリプトはそう書いちゃ駄目だ!! という話という記事を書いたのですが、この記事は、その増補リファイン版として執筆させて頂いたSoftwareDesign 2018年1月号掲載の記事で書き足した内容の一部に相当する記事です。)

あっちとこっちでルールが違う理由

シェルスクリプトの中で「空白を開けてはいけない場面」と「空白を開けなければいけない場面」の代表は、それぞれ以下のような場面でしょう。

  • 空白を開けてはいけない:i=0のような変数の代入文
  • 空白を開けなければいけない:if [ $i != 0 ]のような条件分岐や制御構造

普通のプログラミング言語とは異なり、「空白を開けても開けなくてもどちらでもいい」という場面はシェルスクリプトにはあまりありません1。「全部空白を開けよう」「全部空白を詰めよう」という訳には残念ながらいかないのです。何故そんなに融通が利かないのでしょうか。

シェルスクリプトで戸惑う人は、一つ重大なことを忘れがちなのだと思います。今一度思い出して頂きたいのですが、シェルスクリプトはコマンド列を書き連ねたものです。すべての行は、シェル上で実行するコマンド列と同様に解釈されます。これが、この「融通の利かなさ」の正体なのです。

空白を開けてはいけないのは、普通のコマンド列として解釈されては困る場面

変数の代入におけるi=0のような書き方は、何故スペースを空けずにひと続きに書かなくてはいけないのか。それは、「スペースを空けて書いた物をシェルのコマンド列として解釈したらどうなるか」を考えれば分かります。ここでi = 0のように空白を開けると、「iコマンドに=0という2つの引数を指定するコマンド列」になってしまうのです2

(空白を開けると「i」コマンドに「=」と「0」という2つの引数を指定するコマンド列になる様子)

シェルスクリプトの内容はあくまでコマンド列です。空白文字で区切られたトークン群は「最初のトークンはコマンド名、以後のトークンは引数」と解釈される、という原則を忘れてはいけません。

空白を開けなければいけないのは、普通のコマンド列として解釈されて欲しい場面

勘違いしている人が結構いそうなのですが、シェルスクリプトの「if文」はif [ 式 ]ではなくif コマンド列という構文です。括弧は構文の一部ではないのです。なので、以下のような書き方が普通にあり得ます。

if cat path/to/file | grep "foo" 1>/dev/null 2>&1
then
  echo "fileの中にはfooが含まれています"
else
  echo "fileの中にはfooが含まれていません"
fi

この例の場合、cat path/to/file | grep "foo" 1>/dev/null 2>&1というコマンド列を実行すると、grepの結果が1行でもヒットすればコマンド列全体では「成功」、結果が1行も見つからなければコマンド列全体は「失敗」となります。ifは、「その後に続くコマンド列が成功したらthen節を(そうでなければelse節を)実行する」という構文なのです。

「じゃあif [ ~ ]ってどういう意味なんだ?」と思うかもしれませんが、これもやはりif コマンド列という書き方です。というのも、[という名前のコマンドが実際にあるのです。

詳しく言うと、[testというコマンドの別名です。testは、test $i != 0という風に引数で渡された条件を評価して、結果が真なら「成功」、結果が偽なら「失敗」するというコマンドです。

よって、if [ $i != 0 ]と書くと、これは「ifの後に、[コマンドを実行するコマンド列を書いている。[コマンドには$i!=0]という引数を指定している」という意味に解釈されます。

(ifの後に\[コマンドが書かれていて、\[コマンドには4つの引数を指定している。と解釈されている様子)

空白を入れないと、全く別の意味になります。if[$i!=0]と詰めて書くと、ifが個別のトークンとしてトークナイズされないため、そういう名前のコマンドを実行しようとしている物として扱われてしまいます。

(if\[$i!=0\]、というコマンドとして扱われる様子)

ちなみに、これと同じ事がwhile文にも言えます。こちらも構文としてはwhile [ 式 ]ではなくwhile コマンド列なので、while grep~のような書き方ができますし、空白を詰めて書けばエラーになります。

シェルスクリプトは「コマンド列を書き連ねたもの」だという原則を忘れないで!

一般的なプログラミング言語では空白は大きな意味を持ちませんが、シェルスクリプトでは空白に意味があります。シェルスクリプトはあくまでシェルのコマンド列が書かれている物であって3、コマンド列としてトークナイズされて欲しい場面では空白を開けて、そうでない部分では空白を詰める、という意識的な書き分けが必要です。というか、基本的には全部コマンド列なので空白を開ける必要があって、一部に例外的に空白を詰めて書かなくてはいけない「普通のコマンド列とは違ってひとまとまりで解釈されてくれないと困る部分」があるという感じです。

「読みやすいコードを書くこと」を心がけてそのために空白を使っているという人は戸惑うでしょうが、「読みやすさ」の前に「一般的なコマンド列としてどうトークナイズされるか」という観点で空白を置くように、まず気をつけるようにしてみて下さい。それだけで、シェルスクリプトはグッと書きやすく・読みやすくなるはずです。

なお、シェルスクリプトには他にも、関数の引数と戻り値の活用・配列の活用という普通のプログラミング言語でのgood partsが、シェルスクリプトではbad partsになるという性質があります。このあたりの事については、冒頭にも記した通りSoftwareDesign 2018年1月号掲載の特集記事で図付きで詳しく説明していますので、興味を持たれた方はバックナンバーを取り寄せるなり図書館へ行くなりして見てみて頂ければ幸いです。

また、「シェルスクリプトはシェルのコマンド列を書き連ねたものである」ということは、シェルのコマンドの扱いに長じれば長じるほどシェルスクリプトも書きやすくなるという事が言えます。そうなると、「やりたい事をキーワードにしてググってでてきたコマンド列を闇雲に実行するだけ」の状態から脱して、「自分の意志で思った通りのコマンド列を組み立てられるだけの基礎知識」が必要になってきます。その点に不安があるという方は、筆者が日経Linux誌上で連載している「シス管系女子」も参照してみて下さい(バックナンバーを追うのは大変なので、今から読むなら連載をまとめた本の「まんがでわかるLinux シス管系女子」シリーズがオススメです)。こちらでは、主にSSH越しのコマンド操作をするという場面を想定して、コマンドを自由自在に使いこなせている人が頭の中でどういう事を思い描いているかの「正解」を、ケーススタディの漫画形式で詳しく解説しています

苦手意識があるからという理由だけでコマンド操作やシェルスクリプトを避けてしまうと、他の技術で代わりのことをしようとして、却って大変な思いをする事にもなりかねません4状況に合わせた適切なツールを選べることは、有能なITエンジニアの資質の一つだと自分は思っています。シェルスクリプトやシェルのコマンドについても理解を深めて、状況に合わせた適切な選択肢の一つとして選べるようになって頂ければ幸いです。


  1. パイプラインの|の前後や、バックグラウンド実行の&、リダイレクトの>など、前後に空白を開けても開けなくてもどちらでもいいという場面もいくつかあります。 

  2. man bashでは変数の代入の構文はname=[value]としか書かれていなくて分かりにくいのですが、明示的に「=の前後に空白文字を含められる」と書いていない以上は、空白は含められないものと読み取るのがここでは正解です。 

  3. なので、「シェルのコマンド列でプログラムっぽいことをできるようにした物」であるシェルスクリプトは、プログラミング言語としてきちんと設計され文法が整備された言語に比べると、本質的にはチグハグで場当たり的な仕様になっているのは事実です。シェルスクリプトだけで本格的に開発するという使い方よりは、複数の既存のCLIツールを組み合わせるためのグルー言語として簡単な使い方のみに留めるのが、シェルスクリプトの適切な使い方だと自分は思っています。 

  4. シェルのコマンド数行で済む=数行のMakefileで済むような物について「嫌でござる! 死んでもシェルのコマンドは書きたくないでござる!!」と忌避した結果、Nodeのバージョンやgulpの依存パッケージ群といった本質的には必要でないはずの依存性が山ほど加わってしまっている、みたいな例はよくあるのではないでしょうか……