ruby-talk:308431に回答する

内容の理解

Subject: Readline and conditional tab results based on input
From: Marc Heiler <shevegen linuxmail.org>
Date: Fri, 18 Jul 2008 01:44:26 +0900

最初に「cd」を入力してから TAB を押すとファイルやディレクトリのパスを補完するが、不正な文字 (例えば「blabalblabla」) だと補完しないようにしたいとのこと。
このときの方法を知りたいようだ。
回答のひとつに、方法としては Readline.completer_word_break_characters にスペースを含めないようにすれば cd を含め、全ての入力が complete_proc のブロックパラメータとして渡される。というものがある。[ruby-talk:308526]
現在の Readline ではこのような力技を使うしかない。GNU Readline の API のようなメソッドを用意してあげたい。
補完の前の文字列を取ることをする patch が RubyForge にあった気がする。確認する。

以下がそうだ。
http://rubyforge.org/tracker/index.php?func=detail&aid=3212&group_id=426&atid=1698

パッチは見つけた。あとはこれをどうやって適用するか。
まずは、Readline の他のメソッドのテストを書こう。

とりあえずの回答のメールを送る

あと、質問の回答のメールを書いておこう。

私は Marc Heiler の要求を満たせるようなメソッドを追加したいと考えている。
I want to add some methods that satisfy the Marc Heiler's demands.

RubyForge に投稿されたパッチを取り込む予定です。
The patch contributed to the RubyForge is taken. 
[#3212] Readline does not provide enough context to the completion_proc
http://rubyforge.org/tracker/index.php?func=detail&aid=3212&group_id=426&atid=1698

まずは、 ruby 1.9 に取り込みます。その後、 1.8 系にバックポートする予定です。
First of all, I will take it into ruby 1.9. Afterwards, backport to 1.8.

「パッチの適用」準備

メールも書いたし、「パッチを適用する」準備をする。
まずは、既存のメソッドのテストを記述する。
テストは RubySpec を参考にした方が良いだろうね。と思って、RubySpec を眺めてみたけど、自動生成されたままのものが多くて参考にならない。自分で考えることにする。
テストを書いてコミットした。

  • r18489
  • r18491

パッチの適用

ようやくパッチを適用します。
前田さんと相談し、補完の実装を助けるようにするという方針はいいのだが、API の名前をよく考えてください、と言われています。
パッチでは以下のメソッドを追加している。

line_buffer
入力している 1 行
match_start
補完の対象の単語の開始インデックス
match_end
補完の対象の単語の終了インデックス。現在のカーソルの位置と同じ。

line_buffer は GNU Readline に rl_line_buffer が存在する。採用しても問題ないと考えている。
match_start は GNU Readline では存在しません。採用は難しいでしょう。
match_end は GNU Readline では rl_end です。 completion_proc のブロックパラメータ text の長さと等しいので不要だと思います。

以上の考察から、次の Readline のクラスメソッドを追加します。

line_buffer
入力している 1 行
point
現在のカーソル位置

この情報を元に次のようなことを実現できます。

require "readline"
Readline.completion_proc = proc { |text|
  cmds = Readline.line_buffer.strip.split(/\s+/)
  if cmds.empty? || (cmds.length == 1 && text.length > 0)
    ["cd"]
  else
    case cmds.first
    when "cd"
      ["/usr", "/var", "/home"].grep(/\A#{Regexp.escape(text)}/)
    else
      []
    end
  end
}
while buf = Readline.readline("> ", true)
  p([:buf, buf])
  p([:line_buffer, Readline.line_buffer])
end

ここで注意すべきなのは point を使っていないことです。point は本当に必要なんでしょうかね。
それと、match_start は text と point を使って取得できます。このサンプルを RDoc に記述しておけば match_start はなくても良いと考えています。

require "readline"
Readline.completion_proc = proc { |text|
  start = Readline.point - text.length
  ...
}

ということで、私の考える修正案を ruby-talkRubyForge などに投稿して、みなさんの意見を聞くことにします。

とりあえず、今回の問題の対応は(私の中では)これで一段落しました。
しばらく様子を見て、反応がないようであればコミットします。
Ruby 1.8へのバックポートも検討します。