コーディングエージェントを使うようになって以前よりターミナルを酷使するようになった。ターミナル上でふと四則演算や文字列加工がしたくなったときみんなどうするだろうか?自分は電卓やブラウザなど他のアプリを開きたくはない。ターミナルからできるだけ離れたくはない。shell の算術演算でもいいが冗長だし書き方を忘れがち。ruby や python のコマンドでワンライナーを eval するのもありだが、しかしそれはそれでコマンドを書くのとクォートを書くのが少し面倒。
まったく違う解決方法として、表現力の高い Shell を採用するという選択肢もある。 Nushll, Xonsh, Elvish, YSH などどれも短く便利に色々書ける。
- Nushell
- The Xonsh Shell — Python-powered shell. Python shell. Python in the shell. Shell in Python. Shell and Python. Python and shell.
- Elvish Shell
- GitHub - kevinshome/ysh: > the yikes shell (or ysh): a command line shell for linux
Lisp の世界に目を向けても Lisp を深く統合した Shell のプロジェクトというのがいくつかある。
rash は Racket を深く統合した Shell 環境。 |>, |>> といった特殊なパイプ記法を導入することで素の unix コマンドと Racket コードとの間を簡潔に橋渡しすることができるのが強力かつ面白い。
closh は Clojure を統合した Shell 環境。かなり完成度が高く作り込まれていて便利だが、現在はプロジェクトが hiatus (休止)ステータスだ。
どのプロジェクトを使ったとしても大きな制約が1つある。既存 Shell 環境とその資産を捨てて移行しないといけないということだ。自分は zsh を使っているがいくつかのプラグインや設定を移行するのはかなり骨が折れるだろう。
そこで、ZSHのラインエディタであるZLEから対話処理をフックし clojureのコードなら bb に処理を自動で移譲してくれる zsh プラグイン zsh-clj-shell を作った。
zsh-clj-shell
zsh-clj-shell をインストールするとこんなふうに書ける。
$ printf ' aaa \n bbb \nccc' | (map trim %) | (map upper-case %) | cat -n 1 AAA 2 BBB 3 CCC
入力行を簡易的にパースしてパイプの各ステージごとに Clojure コードかどうかを判定しその単位で変換している。
入力は、複数行文字列の場合には改行で区切った Sequence に変換された値が % に入る。
出力は同様に \n で JOIN された文字列となるので、パイプの次のステージが unix コマンドでも自然につながる。
上記はあくまで例であり、実際にはわざわざ一度 shell の世界に戻してパイプする必要はなくて Clojure 内でスレッディングマクロ ->> を使うほうがいい
$ printf ' aaa \n bbb \nccc' | (->> % (map trim) (map upper-case)) | cat -n 1 AAA 2 BBB 3 CCC
また、入力を Sequence で受け取らずに単一の文字列の塊として受け取りたい場合は %%に入っている。
$ printf ' aaa \n bbb \nccc' | (trim %%) aaa bbb ccc
ちょっとした計算や集計もサクッと書ける。
$ cat fee.csv Dinner,200 Lunch,150 Breakfast,100 Snacks,50 $ cat fee.csv | (map #(let [[i u] (split % #",")] (str i "," (format "%.0f円" (* 150 (Double/parseDouble u))))) %) Dinner,30000円 Lunch,22500円 Breakfast,15000円 Snacks,7500円
インストール
インストールは、 clone して手動でzshrc などに読み込むようにする、でもいいし install スクリプトも用意している。もちろん zsh のプラグインマネージャーも使える。[^2]
zinit
zinit light hatappo/zsh-clj-shell
zplug
zplug "hatappo/zsh-clj-shell"
antigen
antigen bundle hatappo/zsh-clj-shell
sheldon
Add to ~/.config/sheldon/plugins.toml:
[plugins.zsh-clj-shell] github = "hatappo/zsh-clj-shell"
制限
いくつか注意点がある。
1つ目は ( から始まる場合にはBabashkaを起動する。そのため ( を先頭に持ってきたサブシェルの書き方ができなくなってしまっている。1
# サブシェルではなく Clojure として認識されてしまいエラーとなる。 $ (x=123; echo $x;) ----- Error -------------------------------------------------------------------- Type: clojure.lang.ExceptionInfo Message: EOF while reading, expected ) to match ( at [1,83] Data: {:type :edamame/error, :line 1, :column 168, :edamame/expected-delimiter ")", :edamame/opened-delimiter "(", :edamame/opened-delimiter-loc {:row 1, :col 83}} Location: NO_SOURCE_PATH:1:168 Phase: parse ----- Context ------------------------------------------------------------------ 1: (do (require '[clojure.string :as str :refer :all]) (let [input "" % input result (x=123; echo $x;)] (if (string? result) (println result) (println (pr-str result))))) ^--- EOF while reading, expected ) to match ( at [1,83] ----- Stack trace -------------------------------------------------------------- edamame.impl.parser/throw-reader - <built-in> edamame.impl.parser/parse-to-delimiter - <built-in> edamame.impl.parser/parse-list - <built-in> edamame.impl.parser/dispatch - <built-in> edamame.impl.parser/parse-next - <built-in> ... (run with --debug to see elided elements) edamame.impl.parser/parse-to-delimiter - <built-in> edamame.impl.parser/parse-list - <built-in> edamame.impl.parser/dispatch - <built-in> edamame.impl.parser/parse-next - <built-in> edamame.core/parse-next - <built-in>
サブシェルは () の代わりに {} で書くなどの工夫が必要になる。
$ {x=123; echo $x;} 123
これは仕様上の問題なので実装を工夫して解決することはできないが、大きな問題ではないと思っている。
2つ目は、データは clojure 側に渡るときには文字列化していること。そのためパイプがデータをストリーム処理をしない。 unix のいいところが台無しだ。大規模なファイルを扱う上ではつらい。しかし、調べてみると xonsh も似たようなもののようだ。他の Shell はストリーム処理を頑張っているものもある。これについては頑張れば対応できるような気がするが、 InpurtStream などを単に使うようにして、あとはそれを扱いやすくしてあげれば問題ないのか、そのあたり unix のパイプについて今はまだ勉強不足なので見通しが立っていない。
まとめ
zsh-clj-shell プラグインを使うとインタラクティブ・シェル上でシームレスに Clojure (Babashka) が使えます。いくつかの制限によるトレードオフはありますが、既存のZSH資産を捨てずに Clojure を Shell に統合できるというのが最大に良さです。ぜひ試してみてフィードバックをもらえると嬉しいです。
- Babashka のエラーを見やすくするのも今後の課題の1つかな。↩