@kyanny's blog

My thoughts, my life. Views/opinions are my own.

pcomplete (programmable completion) の書き方

Emacs には pcomplete.el というものが付属しており、これはいわゆる bash-completion のようなものだが、 bash-completion ほど充実していなくて git などは補完できなかったので、自分でやってみた。

まずいくつか読むべきファイルがある。俺の場合は /Applications/MacPorts/Emacs.app/Contents/Resources/lisp 以下にあった。違う環境では /usr/share/emacs/*VERSION*/lisp 以下などにあるはず。

  • pcomplete.el
    • pcomplete が提供する基本的な関数群が書いてある。
  • pcmpl-*.el
    • 具体的に、どのコマンドに対してどういう補完を行うかを定義した関数が書いてある。
  • eshell/em-cmpl.el
    • Eshell から pcomplete を使うための関数群が書いてある。

まず、 eshell/em-cmpl.el の先頭のコメントを読むと、例として pcomplete/make を読むといいよと書いてあるが、 pcomplete/make は簡素すぎてこれだけ読んでも意味がわからないので pcmpl-cvs.el を読む。これは cvs コマンドに対する補完を定義しているもので、 cvs は git と同じくサブコマンドを持っているのでいいお手本になる。

あんまり数が多くなくて悲しいけど、既存の pcmpl-*.el と pcomplete/* 関数を読むと、典型的な pcomplete/* 関数は以下のような形になることがわかる。

(defun pcomplete/hoge ()
  "Completion rules for the `hoge' command."
  (let ((pcomplete-help "(hoge)Invoking hoge"))
    (pcomplete-opt "hv") ; hoge コマンドは -h, -v というオプションを持つ
    (pcomplete-here '("foo"))
    (cond ((pcomplete-test "foo")
           (setq pcomplete-help "(hoge)Call foo")
           (while (pcomplete-here (pcomplete-entries))))
          (t
           (while (pcomplete-here (pcomplete-entries)))))))

関数の命名規則は pcomplete/補完したいコマンド名 という風にすると関数を定義して eval した時点でもう補完時にその関数が勝手に呼ばれるようになる。

pcomplete-help にはコンテキストに応じたヘルプメッセージを入れる約束になってるらしいがこれは明示的にどこかで pcomplete-help 関数を呼ばないと表示されないようだ。

pcomplete-opt は、このコマンドが(現在のコンテキストにおいて)とりうるオプションを全て連結した文字列を渡す。オプションはデフォルトで "-" ではじまるものと解釈される。オプション引数でいろいろ挙動を変えられる。 prefix を別のに変えたりとか。あと文字列の中に () で囲んだ部分があると、 read-from-string で S 式を読んで eval してくれるらしい(キモイ!)。ただ、ロングオプションを解釈させる方法はわからなかった。

pcomplete-here が補完のコアとなる関数で、実際にはこれはマクロになってて本体は pcomplete--here という関数。引数がいっぱいあり、引数に何が入ってるかによっていろいろよしなにやってくれる(要するに説明できるほど理解できてない)。 pcomplete-here* というのもあり、こちらは引数展開をやらない?とか、微妙に違いがあって使い分けられてるみたいだがさっぱり意味がわからなかった。英語と Emacs Lisp の両方の読解力を要する。

pcomplete-test も中でいろいろやってるが、 $ hoge foo[TAB] と押したら (pcomplete-test "foo") が真になるのでその下のコードブロックを実行する、という挙動になる。 cvs add と cvs rm で違う補完候補を示したいときの場合分けに使う。

pcomplete-entries ってのは、単にカレントディレクトリ以下のファイルとディレクトリを補完候補として返す関数のようで、補完のメカニズムそのものとは関係ない。

さて、それらとその他のいろんなコードを読んで(ドキュメントは探してもあんまりみつからないので、詳しく知りたければコードを読むしかない)、実際に書いてみたのがこれ。

pcmpl-lwp-request/pcmpl-lwp-request.el at master · kyanny/pcmpl-lwp-request · GitHub

lwp-request コマンドに対する補完を提供する、 pcomplete/lwp-request を書いてみた。 provide とか defcustom とか defgroup とかはぜんぶ pcmpl-cvs.el からのコピペ。ファイルを開いて M-x eval-buffer としてから M-x eshell として、 $ lwp-request -[TAB] とかすると、オプションが補完されるはず。

いくつかのオプションには、さらに補完候補を出すようにしている。実はこれを書いたのは、 -o オプションに指定できる format って何だっけ、というのをよく忘れる(覚えてない)ので、補完してくれると man 引かなくて済むから便利だな、というのが理由だった(というのはもちろん後付けの理由で、 pcomplete の練習のためにちょうど手頃な題材だったから書いただけ)。

あとは再帰的に、オプションとかサブコマンドに応じて S 式のツリーを深くしていきながら補完候補のリストを充実させていけばいいだけ・・・だったら万々歳だったのだが、こんな単純そうなコードでもバグがある。

lwp-request は -c と -C, -s と -S のように大文字小文字の区別があるオプションをもっている。 pcomplete/lwp-request が -c などを補完しようとすると、 pcomplete-insert-entry の中で落ちる。どうも引数の型が正しくないせいらしいのだけど、どこでおかしくなってるのかデバッガで追っても結局わからなかった(デバッガの使い方は別エントリにて)。

他にもなんか微妙なところがあったり、どうせ補完するなら history を読んで URL も補完させたいとか、まともなクオリティにするにはまだまだ時間がかかりそうだけど、とりあえず「作ってみた」までということで。

なお、俺は eshell を使っているので定義した関数がすぐに *eshell* バッファで使えるようになったが、 shell-mode を使う場合は (add-hook 'shell-mode-hook 'pcomplete-shell-setup) を .emacs に追記する必要があるらしい。しかし残念なことに、自分の環境で試してみたら、オプションの補完はできたけど pcomplete-test の補完がきかなかった。ので他の環境でもたぶん動かないでしょう。

本当は git 用の補完が欲しくて pcmpl-git/pcmpl-git.el at master · kyanny/pcmpl-git · GitHub を昨日書き始めたのだけど、もっと簡単そうなもので練習したほうが良さそうだし、何よりさっさとブログに書いておかないと調べたことを忘れて続きが書けなくなりそうだったので、ブログを書くためのお題として lwp-request のほうを今日三時間くらいかけて書いた。