シェルスクリプトの実行時に構文チェックを自動で行う

## はじめに

シェルスクリプトはコードを読み込みながら実行します。そのためシェルスクリプトの後半にシンタックスエラーがあったとしても、途中まで実行されてしまいます。ここで紹介するのは構文チェックを実行前に行うテクニックです。

```sh:test.sh（シンタックスエラーがあるシェルスクリプト）
#!/bin/sh
echo "start"
if true;        # ← then を書き忘れた
  echo "true"
fi
echo "end"
```

```console
$ ./test.sh
start          ← echo "start" が実行されている
./test.sh: 5: Syntax error: "fi" unexpected (expecting "then")
```

## シェルは構文チェック機能を内蔵している

すべての POSIX シェルには構文チェック機能が内蔵されています。この機能は POSIX で標準化されているため、POSIX に準拠した POSIX シェルであれば必ず実装されているはずです。構文チェックを手動で実行するには `-n` オプションを指定して実行します。

```console
$ sh -n test.sh
test.sh: 5: Syntax error: "fi" unexpected (expecting "then")
```

`-v` オプションを併用すると、どこまでシェルスクリプトを読み込んでエラーになったかがわかるので便利です。

```console
$ sh -nv test.sh
#!/bin/sh
echo "start"
if true;
  echo "true"
fi
test.sh: 5: Syntax error: "fi" unexpected (expecting "then")
```

## 構文チェックを自動で行う

さてシェルに構文チェック機能が内蔵されているのはわかりましたが、これを実行するたびに毎回手動でやるのは面倒です。構文チェックにかかる時間などたいして問題にならないわけですから、毎回自動でチェックしてもらいましょう。まずすぐに思いつくのはこのようなコードです。

```sh
#!/bin/sh

# 構文チェック
sh -n "$0" || exit

echo "start"
if true;        # ← then を書き忘れた
  echo "true"
fi
echo "end"
```

おそらく、このような書き方でもほとんどの人は問題ないでしょう。しかし私のように `sh` だけではなくさまざまなシェルでシェルスクリプトを実行してテストをしている人には困ります。なぜなら、このシェルスクリプトは必ず `sh` を使って構文チェックを行い、`bash` でシェルスクリプトを実行したとしても `bash` でテストしてくれないからです。

## 現在実行中のシェルで構文チェックを行う

シェルのオプションは、`set` コマンドのオプションとだいたい同じです。なので、`set` コマンドにも構文チェックを行う機能はついています。しかしシェルスクリプトの中で `set -n` を実行すると、それ以降は構文チェックモードになってしまいコードを実行しないので `set ＋n` では戻せません。ではどうするのかというと、このように書きます。

```sh
#!/bin/sh

# 構文チェック
(eval "set -n; $(cat -- "$0")") || exit

echo "start"
if true;        # ← then を書き忘れた
  echo "true"
fi
echo "end"

```

つまり、**`set -n` をサブシェルの中で実行する**ということです。サブシェルは、現在のシェルとは独立した実行環境であるため、呼び出し元の環境には影響を与えません。あとはうまいこと構文チェックを行えるようなコードを書くだけです。ただし1つだけ注意点があります。POSIX によると「サブシェルの中の `set -n` は無視されることがある」と書いてあることです（[参照](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_13): `The set -n option may be ignored.`）。ただし無視されるシェルの存在を私は確認していませんし、無視されるシェルがあったとしても、おそらく無視されないシェルで実行するでしょうから大した問題ではありません。POSIX にはもう一つ次のような文章が書かれています（太字は私によるもの）。構文チェックは普通は `sh -n` でやるものらしいので、この記事はその常識を覆したテクニックです。

> Use of set -n causes the shell to parse the rest of the script without executing any commands, meaning that set +n cannot be used to undo the effect. **Syntax checking is more commonly done via sh -n script_name**.

このコードでは `.` コマンド、または `source` コマンドで読み込むシェルスクリプトの構文チェックは行いません。それを行うには次のように書きます。

```sh
(eval "set -n $(echo; cat -- ./lib.sh)") || exit
. ./lib.sh
```

少し書き方が違いますが、実は最初のコードは一行目を実行してしまいます。シェルスクリプトの一行目は普通はシバンを書くので問題ありませんが、`.` コマンド、または `source` コマンドで読み込むシェルスクリプトの場合は一行目から実行コードが記述されている可能性があります。そのために `echo` を使い強制的に二行目にしています。副作用として構文チェックを行ったときの行番号出力がズレます。`.` コマンド、または `source` コマンドで読み込むシェルスクリプトでも一行目にコメントを書くという決まりにしていれば、最初の書き方でも問題ありません。

## ShellCheckへの応用

シェル内蔵の構文チェック機能は必ず内蔵されているので常に使えます。ただしチェック内容は ShellCheck より劣ります。シェルスクリプトの実行時に ShellCheck を実行したい場合は次のように書くとよいでしょう。

```sh
#!/bin/sh

# shellcheckによるチェック
type shellcheck >/dev/null 2>&1 && { shellcheck "$0" || exit; }

echo "start"
if true;
  echo "true"
fi
echo "end"
```

最初の `type` は `shellcheck` コマンドがインストールされているときだけチェックを行うためのものです。残念ながらシェルの構文チェックを行うためのシェルはシバンに書かれたシェルとなり、実行しているシェルの構文チェックを行うことはできませんが、ほとんどの人にとってはこれで十分なはずです。

## おわりに

以上、ちょっとしたテクニックでした。

ちなみに私は「途中まで実行されても別に困らなくね？」と思ってるので、このテクニックを使っていません。どの言語もコンパイルエラーやシンタックスエラーで見つけられるのは単なる書き間違いです。書き間違いをバグに含めるという考え方は何十年も前に卒業しました。どちらにしろ実行時エラーやバグは（どの言語でも）実行するまでわかりませんし、テストを行うなら実行するのは当然で、シンタックスエラーは実行すればすぐに分かります。単なる書き間違いの修正に時間はかかりません。これは「シェルスクリプトはシンタックスエラーがあっても実行してしまう」と文句を言っている人のためのものです。

ShellCheck は使いますが目的が違います。シンタックスエラーは動きませんが、ShellCheck は動くけれども良くないコードを探すものです。動かないものは必ず修正しますが、動くものは気が付かなければ見逃してしまいます。私はシェルスクリプトの書き方には十分慣れてしまったので、ShellCheck はコードのコミットのタイミングなどで行えば十分です。ShellCheck は普段シェルスクリプトを書かない人のためのもので、本格的ではない小さなシェルスクリプトを書くときに使うものです。