シェルスクリプトを書くのをやめる - blog.8-p.info
これを見て:
夢の可能性が高くなってきたんですが、Perlのプラグマかなにかで、シェルスクリプトと混在できる……というか、存在しないサブルーチン呼び出しを外部コマンド呼び出しにするやつありませんでしたっけ
— aereal / 青木華絵 (@aereal) 2021年9月16日
まじだ... https://t.co/IF6SyBR4o8
— Kazuyoshi Kato (@kzys) 2021年9月16日
Shell - run shell commands transparently within perl - metacpan.org
use Shell qw(cat ps cp); $passwd = cat('</etc/passwd'); @pslines = ps('-ww'), cp("/etc/passwd", "/tmp/passwd");
Synopsisより。
もうちょっとそれっぽく書くとこう:
cp '/etc/passwd', '/tmp/passwd';
仕組み
コメントを除いて150行に満たない小さいコードです。答えを言うとメタプログラミングで実現されています。
オリジナルはLarry WallによってPerlの強力さをデモンストレートするために書かれたそうです。興味深いですね。
use
Perlを知らない人向けに解説すると、Perl の use Shell qw(cp mv);
は BEGIN { require Shell; Shell->import('cp', 'mv'); }
のsyntax sugarです。
qw(cp mv)
はリストリテラルの一種で ('cp', 'mv')
と等価です。
requireはパス上からShellというパッケージを探して読み込む処理で、それを行った後にimportというサブルーチンの呼び出しを行います。
呼び出されるShellパッケージでこのimportサブルーチン *1 を定義しておくと、読み込まれた際に任意の処理を実行できます。
シンボルのインポート・エクスポートとシンボルテーブル
ところでこういうPerlのコードを見たことがあるかもしれません:
use Carp qw(croak); croak('oops');
Carpというモジュールをuseするとcroakというサブルーチンが使えるようになりました。不思議ですね。
これは専用の言語機能があるわけではなくて上述のimportで何かをしています。
Perlにはシンボルテーブルというものがあり、他の言語でいうスコープとか環境 (environment) と呼ばれるものとだいたい一緒です。
変数やサブルーチンはこのシンボルテーブルに属しており、パッケージごとにシンボルテーブルが別れています。言い換えるとシンボルテーブルの境界をなすのがパッケージです。
Perlのおもしろいところはこのシンボルテーブルをランタイムにわりと自由に触れるということです。触れるというのは読み出すだけではなく、書き換えることもできます。
MyPackage::my_symbol
など パッケージ名::シンボル名
という風にアクセスできます。
Shellのコードでいうと *{"${callpack}::$sym"} = \&{"Shell::$sym"};
がシンボルテーブルを操作するコードになります。
$callpack
は呼び出し元のパッケージ、つまり use
を書いたパッケージにあたるので、呼び出し元のシンボルテーブルに対して $sym
の値を \&{"Shell::$sym"}
に書き換えています。
$sym
は import
の引数リストの要素なので、 'cp'
や 'mv'
になります。つまり Shell::cp
などのサブルーチンを指すのですが、これらの定義はどこにあるんでしょうか。
AUTOLOAD: Perl版method_missing
答えはAUTOLOADという関数です。これはRubyでいうmethod_missingで、AUTOLOADが定義されたパッケージに存在しないシンボルを参照しようとした時に呼ばれます。
ShellのAUTOLOADは呼び出そうとしたシンボル名を取り出し、 _make_cmd
に渡しています。この _make_cmd
がコマンドを実行するサブルーチンを作るので、それをシンボルテーブルに書き加えることで任意のコマンドを実行できるサブルーチンが無から湧き上がったようにみえるのでした。
補足
シンボルテーブル操作とか、スコープによるガードレールを破壊するような危険な行為じゃん! と思ったあなた。そうですね。
Perlはpragmaという仕組みで、一部の言語機能の使用を制限したり警告することができます。strict refs でこのシンボルテーブル操作を制限できます。
use strict;
とだけ書くとこのstrict refsも有効になります。
ちなみにこのpragmaのon/offはレキシカルに行えるので、これらをどうしても使いたい時には no strict 'refs';
と書くと一時的に無効にできたりします。
Perlは後方互換性を保つだけではなく書き手をある程度信頼する作りであるとも感じます。 これをもって大規模チームでの開発に向かないと評価する向きは理解できますが、2021年だしこういう「大いなる力には責任が伴う」を地で行くようなファンキーな言語がひとつくらいあっても良いのではないでしょうか。
おわり
私はちょっとした処理もGoで書くようになりました。
シェルスクリプトの一番の問題点はテスタビリティに欠ける言語設計だと思っているので、GoじゃなくてPerlでもRubyでもなんでも良いがテスタビリティが担保される言語ならなんでも良いと思います。
テストのないプロダクションコードはおぞましいと言う人が多くコモンセンスと言って差し支えないと思いますが、それが開発用の便利スクリプトとかになると途端に緩むのはどうしてなのでしょうか。 認証情報などが環境変数に露出していて、間違えると本番データを消してしまったり数百万の請求がやってくるかもしれない環境で、テストもない・dry-run機構が壊れているかもしれないスクリプトを実行することが恐しくないのでしょうか。
私は他人の書いた古びたシェルスクリプトほど怖いものはないです。
以上です。
Rubyのshellについて (2021-09-17追記)
それにインスパイアされた shell.rb ってあったような。https://t.co/fXmfm7EBeM
— Tatsuhiko Miyagawa (@miyagawa) 2021年9月17日
この辺やばみを感じますw pic.twitter.com/bvumKK0Flg
— Tatsuhiko Miyagawa (@miyagawa) 2021年9月17日
miyagawaさんから情報提供いただきました。
Rubyは演算子オーバーロードができるのでよりそれっぽい見た目になって最高ですね。
*1:関数のこと