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

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

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

（※以前[プログラマーの君！ 騙されるな！ シェルスクリプトはそう書いちゃ駄目だ！！ という話](https://qiita.com/piroor/items/77233173707a0baa6360)という記事を書いたのですが、この記事は、その増補リファイン版として執筆させて頂いた[SoftwareDesign 2018年1月号掲載の記事](http://gihyo.jp/magazine/SD/archive/2018/201801)で書き足した内容*の一部に相当する*記事です。）

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

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

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

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

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

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

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

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

[^parameter]: [man bash](https://linuxjm.osdn.jp/html/GNU_bash/man1/bash.1.html#lbAT)では変数の代入の構文は`name=[value]`としか書かれていなくて分かりにくいのですが、明示的に「`=`の前後に空白文字を含められる」と書いていない以上は、空白は含められないものと読み取るのがここでは正解です。

![（空白を開けると「i」コマンドに「=」と「0」という2つの引数を指定するコマンド列になる様子）](https://qiita-image-store.s3.amazonaws.com/0/61387/7495b52d-8499-34e2-975c-51ba9dc96325.png)

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


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

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

```bash:
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-command]。`test`は、`test $i != 0`という風に引数で渡された条件を評価して、結果が真なら「成功」、結果が偽なら「失敗」するというコマンドです。

[^test-command]: `man test` と `man [` でそれぞれのコマンドの説明を見てみると、同じ説明になっている事が分かります。

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

![（ifの後に\[コマンドが書かれていて、\[コマンドには4つの引数を指定している。と解釈されている様子）](https://qiita-image-store.s3.amazonaws.com/0/61387/fea3cd00-129a-5b01-4733-8e764e89f77f.png)

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

![（if\[$i!=0\]、というコマンドとして扱われる様子）](https://qiita-image-store.s3.amazonaws.com/0/61387/e36cab68-423a-322c-6529-9059884556fd.png)

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


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

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

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

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

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

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

普段は[カーゴカルトプログラミング](https://ja.wikipedia.org/wiki/%E3%82%AB%E3%83%BC%E3%82%B4%E3%83%BB%E3%82%AB%E3%83%AB%E3%83%88%E3%83%BB%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0)のような事は慎むべきと思っている人でも、シェルのコマンド列の事になると急にそのレベルになってしまうというのは、（気持ちは分かりますが）あまりいい状態とは言えないです。

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


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


追記。この記事で腑に落ちないと感じた方は、より厳密な歴史的経緯・背景事情を解説した [シェルスクリプトは変数代入で = の前後にスペースを置けない！･･･の本当の理由を知ると優れた文法が見えてくる](https://qiita.com/ko1nksm/items/9650ed1fc21d668f2732) という記事を参照すると、スッキリできるかと思います。