シェルスクリプトを進化させるaltSHの技術

altSH とは altJS をパクって作った私の造語です。といっても誰でも思いつくような言葉なので既に使ってる人もいるかと思います。大層なタイトルですが、記事の内容は私のアイデア（＋α）程度を書いてる程度なのであまり期待しないでください。

みなさん、シェルスクリプトは好きですか？嫌いという人も多いでしょう。本物のプログラミング言語じゃないとか100行超えたら他の言語を使うべきだとか散々言われています。言いたい事はありますがまあプログラミングが難しいという点は強く否定できません。そもそも今主流の POSIX シェルは ksh88 を源流としており、その名の通り 1988 年、つまり 30 年以上前、そしてそれ以前の Bourne Shell を加えれば 40 年以上前に設計されたものでそこから大きく変わっていません。最新の言語に比べたら使いづらいのも当然でしょう。しかしその反面大きく変わらないということは高い互換性があり、ポータビリティがもっとも高いスクリプト言語とも言えます。

POSIX で規定されていることもあり `/bin/sh` は POSIX シェルであり、普段皆さんが使ってるであろうシェルは bash や zsh が多数を占めていると思います。互換性の必要性があるため POSIX シェルの仕様は簡単には変えられないので、この状況が近い未来にすぐ変わるとも思えません。しかしながら POSIX シェルは今後、より使いやすく進化して欲しいという要求が（私には）あります。bash のような特定シェル専用のオレオレ拡張ではなく POSIX シェルの仕様がという意味です。

さて、これはどこかで聞いた話ですね？ そう altJS です。ブラウザに搭載されている JavaScript は簡単に変えることはできないが言語は進化させたい。そこで登場したのが CoffeeScript です。より優れた CoffeeScript という言語を（当時は貧弱だった）JavaScript に変換することでより快適にブラウザで動くスクリプトを書くことができるようになりました。その後 Babel などのトランスレータによって ECMAScript6 以降で書いたコードを対応していない古いブラウザでも動かせるようになり、JavaScript と ブラウザは互換性を保ちながら止まることなく進化し続けており、今もまた TypeScript というブラウザでは直接動かないより改善されたスクリプト言語も多く使われています。

これと同じ流れがシェルスクリプトの世界でも発生すればいいなと私は考えています。シェルスクリプトの世界にはシェルスクリプト用のライブラリが不足していますが、昔のJavaScriptもそうでした。そこに Prototype や jQuery などのライブラリが登場しました。RequireJS などのモジュール管理の仕組みが登場しました。ビルドによって複数のファイルは結合され1ファイルのスクリプトファイルに変換されるようになりました。その後モジュール化の仕組みは標準化されました。シェルスクリプトを進化させるには JavaScript の世界で必要だったものがシェルスクリプトの世界でも必要なのではないかと思います。シェルスクリプトが使いづらいなら使いやすく改善すればいいのです。

さて altJS ですが大きく二つの種類に分けられると思っています。一つは CoffeeScript のように全く異なる言語から JavaScript に変換するタイプと、TypeScript のように JavaScript と互換性を保ちながら言語を拡張しているタイプです。前者は大胆な言語拡張が行えますがそれだけ移行は大変です。後者はある程度の制限が発生しますが今までの知識を応用することができます。一長一短でどちらが優れているとは言い切れません。

まず CoffeeScript タイプの altSH ですが、実はこの間たまたま見つけたので紹介したいと思います。その名も [powscript](https://github.com/coderofsalvation/powscript) です。「coffee for the shell」と書かれているように Ruby や Python を参考にした構文から bash に変換するトランスパイラです。[こちら](http://powscript.isvery.ninja/) でオンラインでためすことができます。実験的な機能として bash ではなく POSIX シェル用のコードを出力する機能（**スタンドアロンで使える bash から POSIX シェルへのコンバーター**）もあるようです。シェルスクリプトのわかりづらい構文がより親しみやすい構文に改善されています。

もう一つの TypeScript タイプですが残念ながら私はそういう言語を知りません。しかし近いものならあります。[ShellSpec](https://shellspec.info/) の スペックファイル（テストファイル）です。はい、すみません。長々とひっぱってきましたが、実は ShellSpec の技術の話です（笑）。ShellSpec では以下のようなスペックファイルを書いてシェルスクリプトのユニットテストをおこないます。独自のDSLを使用しますが、シェルスクリプトと互換性がある文法になっており内部にシェルスクリプトを含めることもできます。

```sh
Describe "bc command"
  bc() { echo "$@" | command bc; }

  It "performs addition"
    When call bc "2+3"
    The output should eq 5
  End
End
```

もちろん、これは普通のシェルスクリプトではなく `Describe` や `It` などの DSL が含まれているのでそのままではシェル上で動きません。一般的には `shellspec [スペックファイル]` コマンド経由で実行します。`shellspec` コマンドは内部でこのファイルを普通のシェルスクリプトコードに一行ずつトランスレートしながらシェルに渡して実行させています。ShellSpec も内部のトランスレータもシェルスクリプトで実装されているので POSIX に準拠したシェルさえあれば動きます。

そして ShellSpec はこのスペックファイルを実行可能なスクリプトにする手段を用意しています。その方法は上に通常のシバンに加え、特殊な行を一行追加して実行権限をつけるだけです。

```sh
#!/bin/sh
eval "$(shellspec - -c) exit 1"

Describe "bc command"
  bc() { echo "$@" | command bc; }

  It "performs addition"
    When call bc "2+3"
    The output should eq 5
  End
End
```

シバンが `#!/bin/sh` なことに注意してください。通常、特殊なファイルを実行可能にするには `#!/usr/bin/env shellspec` のようにするのが常套手段ですが、それではシェルスクリプトではありません。ただ実行可能なだけです。ShellSpecは「スペックファイルは（拡張された）シェルスクリプトである」という主張であるため拡張子は（デフォルトで） `.sh` でありシバンにも `/bin/sh` を使います。これは bash と互換性がない文法でシバンに `#!/usr/bin/env bats`を使い .bats 拡張子を使う [bats-core](https://github.com/bats-core/bats-core) よりも優れた点です。

このシバンはただ見た目を誤魔化しているだけではありません。この例では `/bin/sh` を使用していますが、`/bin/bash` を使えばちゃんと bash でスペックファイルを実行します。またシェルスクリプトとしての正しいシバンなのでこれを参照して動作する ShellCheck などのツールがそのまま使えます。そして最も重要なのは（Linuxでは、GNU Core Utilities 8.30 以上を使わない限り）シバンで引数を複数渡すことができないという問題を解決しているという点です。例えば `#!/usr/bin/env shellspec` と書いた場合 `shellspec` が `env` の引数であるためこれ以上引数を渡せません。（スペースで区切って指定しても全て結合されて一つとみなされます。） ShellSpec ではこの問題を解決しています。

さてこの `eval "$(shellspec - -c) exit 1"` の一行がなにをやっているかですが、`eval` なしで `shellspec - -c` を実行してみるとわかると思います。これは次のようなコードを出力します。

```sh
IFS= read -r shebang < "$0"
case $shebang in \#\!*) shell=${shebang#??};; *) shell="";; esac
exec $shell '/Users/koichi/bin/shellspec' '-c' "$0" --pattern "*" "$@"
exit 1
```

`exec` を実行している所がキモです。スペックファイルを実行してると思いきや、スペックファイル自身のシバン（`#!/bin/sh`）を読み取り、そのシェルを使って `shellspec` コマンドにスペックファイルのパスを与えて `exec` でプロセスをすげ替えています。つまりスペックファイルの実行は `exec` を実行した時点で一旦終了します。そして`shellspec` コマンドからスペックファイルを実行している場合は、この行が再帰的に実行されないように何もしない `shellspec` "関数"を定義して無効化しています。

`shellspec` 関数は何もしないと書きましたが、実際には `#` という文字を出力しています。これにより `eval "$(shellspec - -c) exit 1"` の最後の `exit 1`はコメントアウトされ実行されません。説明が逆になってしまいましたが、この `exit 1` は `shellspec` コマンドがインストールされていない環境を考慮したものです。`shellspec` コマンドがない環境では `$(shellspec - -c)` の部分はエラーメッセージを出力し空文字になるので `exit 1` が実行されスペックファイルの実行が中断されます。

上記のコードを生成するために `shellspec` コマンドの上部には以下のようなコードが書かれています。

```sh

if [ "${1:-}" = "-" ]; then
  if [ $# -gt 1 ]; then shift; set -- "$0" "$@"; else set -- "$0"; fi
  for i; do i="$i'" j=''
    while [ "$i" ]; do j="$j${i%%\'*}'\''" && i=${i#*\'}; done
    set -- "$@" "'${j%????}'" && shift
  done
  echo 'IFS= read -r shebang < "$0"'
  echo 'case $shebang in \#\!*) shell=${shebang#??};; *) shell="";; esac'
  echo 'exec $shell' "$@" '"$0" --pattern "*" "$@"'
  echo 'exit 1'
  exit 0
fi
```

やってる処理は大したことなく、引数の最初が `-` であった場合は残りの引数を（ちゃんとシングルクォートをエスケープして）実行コマンドを組み立てて終了しているだけです。

これは ShellSpec が使用している技術の一つですが ShellSpec に依存しているわけではありません。altSH を実現する仕組みとして汎用的に使用することができます。このアイデアを利用すればシバンに `#/bin/sh` を使用するシェルスクリプトと互換性がある拡張された文法の言語を作ることも可能でしょう。そして将来 POSIX シェルがその文法にネイティブに対応し POSIX シェルの標準仕様に取り込まれればシェルスクリプトの進化は完了です。

私は今はまだ ShellSpec の開発に専念していますが、どこかで誰かが始めないかなーと思ってこの記事を書きました。なお、この記事のコードは特に権利は主張しないので無断で使用して構いません。