(->> read write unlearn)

書いても書かなくても忘れる

clojureのコードをインラインで実行できるzshプラグイン

コーディングエージェントを使うようになって以前よりターミナルを酷使するようになった。ターミナル上でふと四則演算や文字列加工がしたくなったときみんなどうするだろうか?自分は電卓やブラウザなど他のアプリを開きたくはない。ターミナルからできるだけ離れたくはない。shell の算術演算でもいいが冗長だし書き方を忘れがち。ruby や python のコマンドでワンライナーを eval するのもありだが、しかしそれはそれでコマンドを書くのとクォートを書くのが少し面倒。

まったく違う解決方法として、表現力の高い Shell を採用するという選択肢もある。 Nushll, Xonsh, Elvish, YSH などどれも短く便利に色々書ける。

Lisp の世界に目を向けても Lisp を深く統合した Shell のプロジェクトというのがいくつかある。

docs.racket-lang.org

rash は Racket を深く統合した Shell 環境。 |>, |>> といった特殊なパイプ記法を導入することで素の unix コマンドと Racket コードとの間を簡潔に橋渡しすることができるのが強力かつ面白い。

github.com

closh は Clojure を統合した Shell 環境。かなり完成度が高く作り込まれていて便利だが、現在はプロジェクトが hiatus (休止)ステータスだ。

どのプロジェクトを使ったとしても大きな制約が1つある。既存 Shell 環境とその資産を捨てて移行しないといけないということだ。自分は zsh を使っているがいくつかのプラグインや設定を移行するのはかなり骨が折れるだろう。

そこで、ZSHのラインエディタであるZLEから対話処理をフックし clojureのコードなら bb に処理を自動で移譲してくれる zsh プラグイン zsh-clj-shell を作った。

zsh-clj-shell

github.com

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 に統合できるというのが最大に良さです。ぜひ試してみてフィードバックをもらえると嬉しいです。

github.com


  1. Babashka のエラーを見やすくするのも今後の課題の1つかな。

claude code で max_tokens のエラーが出続ける

Claude Code でプロンプトを送信すると何を書いても、どんなコマンドを実行してもレスポンスがこうなる。

❯ /init 
  ⎿ API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"max_tokens: 200001 > 64000,
    which is the maximum allowed number of output tokens for
    claude-opus-4-5-20251101"},"request_id":"req_XXXXXXXXXXXXXXXX"}

例外は /status コマンドなどトークンを消費しなそうなコマンドのみ。

Claude Code のバージョンを 2.1.2 までダウングレードすると事象が解消する。

原因はこの設定が環境変数に入っていたこと。この設定値が上限オーバーのようで、LLM をたたくときにエラーになる。

"MAX_THINKING_TOKENS": "200000"

自分は settings.json にいつからか入れたままあまり精査されず残っていた。

おそらく v2.1.7 で入ったこちらの修正の影響と思われる。

Fixed context window blocking limit being calculated using the full context window instead of the effective context window (which reserves space for max output tokens)

コンテキストウィンドウのブロック制限が、有効なコンテキストウィンドウ(最大出力トークンのスペースを予約する)ではなく、完全なコンテキストウィンドウを使用して計算されていた問題を修正しました。

https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md#217

Claude Code のバージョンが古いと Opus4.6 が使えないようなのでちゃんと調べたらすぐ分かった。

MySQL 脳の自分のための PostgreSQL クエリ・メモ

PostgreSQL を使いだしてもう何年か経つのにいまだに MySQL 脳なのか、スキーマとデータベースはどっちがどっちか忘れがち。 → db > schema > table の階層関係。

[ PostgreSQL Instance ] (サーバー / クラスター)
       │
       ├── [ Database A ] (独立したデータ空間)
       │      │
       │      ├── < Schema: public > (デフォルト)
       │      │      ├── Table: users
       │      │      └── Table: orders
       │      │
       │      └── < Schema: inventory > (論理的なグループ)
       │             ├── Table: products
       │             └── Table: stocks
       │
       └── [ Database B ]
              └── < Schema: public >
                     └── Table: logs
graph TD
    Instance[PostgreSQL Instance]
    
    subgraph DB1 [Database: my_app]
        subgraph S1 [Schema: public]
            T1[Table: users]
            T2[Table: posts]
        end
        subgraph S2 [Schema: sales]
            T3[Table: invoices]
        end
    end

    subgraph DB2 [Database: analytics]
        subgraph S3 [Schema: public]
            T4[Table: raw_data]
        end
    end

    Instance --> DB1
    Instance --> DB2

MySQL 脳だと SHOW DATABASES; したくなる。そういうのはない。毎回ググるので一通りメモしておく。

1.データベース一覧

/*List Databases*/
\l+
/*List Databases*/
SELECT 
    datname AS database_name,
    pg_get_userbyid(datdba) AS owner,
    pg_size_pretty(pg_database_size(datname)) AS size
FROM pg_database
WHERE datistemplate = false;

2.スキーマ一覧

/*List Schemas*/
\dn+
/*List Schemas*/
SELECT schema_name 
FROM information_schema.schemata
WHERE schema_name NOT IN ('information_schema', 'pg_catalog') 
  AND schema_name NOT LIKE 'pg_toast%'
  AND schema_name NOT LIKE 'pg_temp%';

3.テーブル一覧

/*List Tables*/
\d
\ds     -- sequence のみ
\dt+    -- table のみ
/*List Tables*/
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_type = 'BASE TABLE'
  AND table_schema NOT IN ('information_schema', 'pg_catalog')
ORDER BY table_schema, table_name;
/*Change default search schema*/
SHOW search_path;
SET search_path=public;

4.カラム一覧

/*List Columns*/
\d TABLE_NAME
/*List Columns*/
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = 'TABLE_NAME'/*変更*/
ORDER BY ordinal_position;

それでは Happy database life!