Re: シェルスクリプトを書くのをやめる

この記事はシェルスクリプトを書くのをやめる - blog.8-p.infoの個人的感想である。

なお元の記事を書いた人と私は全くの赤の他人であり、何の接点もない。つまりこの記事は単なる外野のたわ言である旨をご承知いただきたく。

Re: シェルは悪いプログラミング言語である

シェルスクリプトの文法やら何やらが、最近のモダンなプログラミング言語と比較すれば色々とアレなのは確かだ。

しかし冷静に考えてほしいのだが、現在使われている/bin/shのもとになったBourne Shellは1977年生まれで、44年前の代物だ。それぐらい大昔の、まだ言語設計もシステム開発技法も未成熟だった時代に、プログラミング言語ではなく「プログラマブルな対話型コマンドラインインタプリタ」の一種として開発された代物だということは押さえておくべきだろう。

コンパイラ本になるが『コンパイラ―原理・技法・ツール〈1〉 (Information & Computing)』の原書が出版されたのは1986年だ。プログラミング技法に関しては、『ソフトウェア作法』でさえも原書が出版されたのは1976年だ)

言語設計が未成熟だった故に言語文法とか色々とアレだし、未成熟だったプログラミング技法のあおりで微妙な振る舞いをするようになってしまった部分がPOSIXやら何やらで「仕様」と化してしまった点もあるかもしれない。知らんけど。

そもそもUnixシェルはプログラミング言語の処理系ではないプログラミング言語ではないのだから、REPLとは言えない。プログラミング言語と「対話型コマンドラインインタプリタ」の間には「野球とクリケット」ぐらいの違いがあって、「プログラマブルな対話型コマンドラインインタプリタ」であるUnixシェルは野球に近づいているものの、依然として「野球とソフトボール」ぐらいの違いがある。だから、プログラミング言語の視点では微妙だと感じられる妥協点がある文法となっている。

プログラミング言語ではないものをプログラミング言語として認識してしまう我々プログラマの業が、思わぬ認識の齟齬に繋がっている気がしないでもない。

その上で、良いか悪いか判断に迷うところだが、40年前にBourne Shell向けに書かれたシェルスクリプトは(使用しているコマンドの有無や、コマンドの仕様の変化などで躓かなければ)今でも動作する。例えば原書が1984年に出版された『UNIXプログラミング環境 (海外ブックス)』の第3章~第5章が、2021年になってもシェルプログラミングの入門文書としてほぼ修正なしで通用するのである。

見方を変えれば、シェルスクリプトのコアな部分は40年以上前から何も変わっていない、ということだ。

適切な例えではないかもしれないが、ある意味でシェルプログラミングには「21世紀のプログラマから見た『FORTRAN 77でプログラミング』や『COBOL-80でプログラミング』の世界」みたいな世代差による断絶があるのではないかと思う。知らんけど。*1

Re: シェルはあまりパワフルではないので、結果として複数の言語を混ぜることになる

……まあ、シェルスクリプトはグルー言語の王様だからなあ。

CLIの対話型コマンドラインインタプリタでは、どうしてもコマンドが主役となる。Unixシェルがそれ以前のコマンドラインインタプリタよりも優れていた点の1つは、パイプのような「コマンドとコマンドを容易に組み合わせられる機能」が組み込まれたことだ。

シェルは、例えるなら現場監督にすぎない。職人たち(各種コマンド群)を連れてくる(インストールする)か、もしくは職人を養成(コマンドを自作)したうえで、彼らに作業を割り振って工事を進める(コマンドを組み合わせて処理をする)のが、もっとも効率のよいシェルの使い方である。

言い換えれば、シェル自身に職人の役割が求められるようになった時点で非効率なのだ。他の、もうちょっと本格的なプログラミング言語に切り替えた方がよい。

Unixシェル環境に慣れている」という前提の下での話となるが、個人的な体感としては、単機能のプログラムなら小さなシェルスクリプトで実装してしまうのが手っ取り早い。シェル上でのワンライナーも含めて、小さなシェルスクリプトで済む範囲であるならば、シェルは現場監督として大活躍する。しかし少し複雑なプログラムをシェルスクリプトで実装しようとすると、シェルに配列のような「現場監督以外の役割」が欲しくなってくる――その時点で他のプログラミング言語を検討するべきだろう。

Re: シェルが存在しない環境のことを考えると、書いたものも、そこまでポータブルではない

そもそもシェルが存在する環境であっても、シェルスクリプトは移植性の問題を抱えやすい。この点はEric S. Raymondも『The Art of UNIX Programming』で書いている。以下、同書のP.365より:

複雑なシェルスクリプトは、移植性問題を抱えていることが多い。それはシェル自身に問題があるというよりも、コンポーネントとして他のプログラムが存在することが前提となっていることに起因するものだ。Bourne ShellKorn Shellのクローンは、Unix以外のオペレーティングシステムにも散発的に移植されているが、シェルプログラムは(現実的にいって)Unix以外のオペレーティングシステムには移植性がない。

以上をまとめると、シェルの長所は小さなスクリプトを書くためには非常に自然で手っ取り早いことだ。欠点は、特に大きなシェルスクリプトでは、すべてのターゲットマシンで同じように動作するとは限らず、ない場合もあるようなコマンドに大きく依存してしまうことだ。大規模なシェルスクリプトでは、依存関係を分析するのも容易ではない。

個人的には、シェルスクリプトの移植性は「C言語の移植性」と同程度に疑わしいと思っている。教科書の類には「C言語は移植性が高い」と書かれているけど、現実として、移植性の高いC言語ソースコードを書くためにプログラマは色々と苦労している訳で――Unix環境間の移植性であっても、それなりの苦労をしないとシェルスクリプトの移植性は得られないと考えるべきだろう。

先生誰にも言わないから、Ubuntu上で使っていたシェルスクリプトmacOSでうまく動かなかった子は手を上げなさい――先生は動かなかったことがあります。

Re: じゃあ代わりに何を使うの?

これな、これなあ、物凄く悩ましいの。

正直なところ、周囲の開発者の同質性が高めなら、そこまで悩まなくてもよいと思うの。だってみんなが慣れている言語から検討を始めればよいし、仮にちょうど良い言語が無かったとしても、モダンでメジャーでそれっぽい言語を採用して周囲に広めるのにあまり苦労しないはずだから。

でも皆が割と違うことをやっていて、作業環境もバラバラだと、使用する言語を統一しようという試みは概ね失敗する。どの言語を選んでも角が立つものだ。

個人的には、最近は色々と割り切って「プラットフォームごとに違う言語を使う」というようにしている。Windows向けの小ツールはPowerShell 5.1で実装する*2macOSなら頑張って「標準のコマンドを使用する前提のシェルスクリプト」を書くか、思い切ってSwiftでスクリプトを書いてしまうこともある*3

――Linux向けのツール? macOS以外のUnixユーザランド環境をまともに触っているの、自分だけだから……好き勝手にやらせてもらってる。小さなツールならシェルスクリプトで書いてしまうことが多いし、ちょっと複雑なツールは「スクリプト処理系が存在するプログラミング言語」の中からその時々の気分で選んで実装する。でもバイナリデータの処理を書くときはC言語C++を併用しがちだ――生のバイナリデータを直接操作することに慣れてしまった弊害だよなぁ。

なお状況次第ではコンパイル型言語を採用することもある。こういう時は、大抵は「各々のマシンにスクリプト処理系をインストールしたくない」という要求があるので、その影響で必要なライブラリ類を全て静的リンクしたFatな実行ファイルを配布することが多い――要するに「各々のマシンに追加の共有ライブラリ類をインストールすることも避けたい」ということだ。

Re: ShellCheck 使えばいいのでは?

よくある「シェルスクリプトの代替」はPerl/Python/Rubyあたりだと思うのだが、動的型付けのスクリプト言語を本格的に使うときは「lintツールによる静的解析 + 単体テストツール」は必須だと思っている。コンパイル型言語におけるコンパイル・チェックという名の静的テストに相当するテストを、lintによる静的テストと単体テストによる動的テストで補完するのが目的だ。

シェルスクリプトを「動的型付けのスクリプト言語」に含めるのは何か違うと思うのだが、しかし、コンパイル・チェックがなくて実行時でないと洗い出せないエラーが多々あるという点では、シェルスクリプトPerl/Python/Rubyと同じだ。だからシェルスクリプトへの「lintによる静的テスト」は効果的だと考えている。

――なんだけど、シェルスクリプトを書くときにShellCheckを使うのは、何というか大袈裟すぎる気がするというか、むしろ「ShellCheckが必要な規模のシェルスクリプト」を書くこと自体がアカンのではないか、というもにょもにょしたものを感じてしまう。

あれだな、「シェルスクリプトはシンプルで単純なもの」という思い込みがあるのだ。

Re: Python とか Rubyシェルスクリプトみたいなことするの面倒くさくないですか?

ひんしゅくを買う発言だと思うけど、そもそもPythonRubyで「シェルスクリプトみたいなこと」をする必要があるのだろうか……?

実現したい内容によるのだろうけど、私ならまず最初に「PythonRubyでコマンドを書いて、シェルスクリプトでラッピングしてコマンド同士を組み合わせる」というアプローチを検討すると思う。

……あれ? 結局シェルスクリプト自体は書いているような……しかも追加の依存関係が発生したぞ?

ま、まあ、アレだ、シェルが得意とする処理はシェルスクリプトで行えばよいし、シェルが不得意な処理は他のプログラミング言語で行えばよい、という考え方だ。そのアプローチの1つとして、私は「他のプログラミング言語でコマンドを実装して、シェルスクリプトで連結する」というシェルスクリプト・ファーストな方法を採用することが多い。

その結果として、結局はシェルスクリプトを書くことになるのだが、シェルスクリプト自体は小さくシンプルなコードで済む。まあ、単なる分割統治ですな、複数の言語を使っているだけの。

Re: Python とか Ruby で書いたときに、依存パッケージとかどうしてますか?

標準ライブラリ縛りは割と鉄板ですな。

もう少し真面目に書くと、例えばチーム開発をしていて、みんなPythonRubyを使っていて、自分と周囲の開発者の開発環境の同質性が高い――とかならpipやgemを使う前提でも問題ないと思うの。類似案件として「macOSでHomebrew」みたいなケースもそう。

でも世の中そんな環境ばかりじゃない訳で、開発環境の同質性が低い環境にて「他人も使うツール」を書くとなると、スクリプト言語なら標準ライブラリ縛りにせざるを得ないことが多い。

例えばscipy必須のPythonスクリプトを社内に配布すると、絶対に何人かはscipyの導入やらpipenvの問題やら「そもそも吾輩は中身を理解しないまま手順書通りにvirtualenvを使っているでござる」やらでトラブルを報告してくる。実装に要するコスト次第だが、配布後のユーザ・サポートのコストを抑える目的で、あえてサードパーティのライブラリを使わないケースは少なくない。

別のアプローチとして、コンパイル型言語を使って、必要なライブラリ類を全て静的リンクした実行ファイルを配布することもある。

人月の神話【新装版】』の第1章「タールの沼」に曰く、単なるプログラムをプログラミング製品に磨き上げるには最低3倍のコストがかかる。自分が使うだけのツールと「他人も使うツール」とでも、やはり相応のコスト差があると考えた方がよいだろう。

おわりに:2021年にシェルスクリプトについて学ぶべきか否か?

そもそもUnixシェルが本質的に「Unixマシンを操作するための対話型コマンドラインインタプリタ」である以上、まず肝となるのは「Unixマシン上でシェルというCLIを多用するか否か」という点だろう。

というのも「Unixシェルでの操作に慣れて、パイプなどでコマンドを組み合わせることを覚えたユーザが、『コマンドの組み合わせ』を再利用するためにスクリプト化する」というのが、伝統的な「シェルスクリプトに手を出す契機」の1つだと思うからだ。

もしくは「Unixシェルでの操作に慣れて、既存のコマンドに不満を感じたユーザが、そのコマンドを拡張するためにスクリプトでラッピングする」というケースもあるだろう。というか『UNIXプログラミング環境』の第5章の導入部分がもろにそれである。

つまりUnixシェルの住人ならば、住民歴が長くなるにつれてシェルスクリプトに手を出すようになるのは自然であるし、その恩恵を享受しやすいのだ。でもUnixシェルの住人になる気が無いのならば、シェルスクリプトに手を出しても得られる恩恵は少ない。

だから、コンテナではなくUnixマシンそのもの(VirtualBox上の仮想マシンを含む)をCLIで管理したいとか、Unixシェル上の柔軟で効率的な「テキストファイル処理環境」で日常の作業を行いたいとか、そういう動機があるならば、2021年の今でもシェルスクリプトについて学ぶ意味はあるだろう(ただしUnixシェルにある程度親しんだ後に、だが)。

でも主眼が「ツールを自作する」というプログラミングにあるのならば、プログラミング環境としてUnixシェルを選択する意義は今でもあるかもしれないが*4、言語処理系としてUnixシェルを選択する意義は薄いだろう――もちろん作ろうとしているツールに課せられた制約次第だが。

*1:FORTRAN 77やCOBOL-80でのプログラミング経験が無いので、本当のところは分からない。

*2:PowerShell 5.1なら大抵のWindows 10マシンで動作する。

*3:swift(1)の引数にSwiftで書いたソースファイルを指定することで、ビルドしないで実行することができる。shebangにだって対応している。周囲のmacOSユーザはiOS/macOSアプリの開発者だけなので、大抵はXcodeがインストール済みであり、swift(1)を使用できる環境が整っている。

*4:とはいえGUIの便利なエディタを使ってしまうものである。