Reline を使うと複数行編集ができるようなので、自分が使いそうな基本的な部分について調べてみました。
このリッチなのが標準で使えるの嬉しいですよね。ありがたや……。
RUBY_VERSION #=> 3.0.0 Reline::VERSION #=> 0.2.5
最初の雛形
ブロックは必須。
require "reline" PROMPT = "> " loop do text = Reline.readmultiline(PROMPT) do |_| # このブロックについては後述 true end # 編集が完了した複数行文字列を使った処理 puts "text (#{text})" end
これだけだとまだ複数行編集できないんですが、デバッグ用のログと履歴まわりを準備しておくと捗るので先にそっちを片付けます。
挙動確認のためのデバッグログ出力
デバッグ用の表示を同じターミナルに出力すると混ざって分かりにくいので、
ファイルに出力して別ターミナルで tail -F
することに。
$log = File.open("debug.log", "a") def debug(*args) $log.puts *args $log.flush end
履歴の保存・復元
同じ入力を繰り返すのは面倒なので履歴まわりを用意しておきます。
Reline.readmultiline
の第2引数 add_hist
を true にすると
Reline が履歴を覚えてくれて、カーソルキー上下や ctrl-n
ctrl-p
で履歴を辿れるようになります。
text = Reline.readmultiline(PROMPT, true) do |input| ...
一度終了して次回実行したときに前回までの履歴が復元されてほしいので、シリアライズしてファイルに保存します。とりあえず JSON で適当に。
require "json" HISTORY_FILE = "history" def add_history(text) File.open(HISTORY_FILE, "a") { |f| f.puts JSON.generate(text) } end def load_history return unless File.exist?(HISTORY_FILE) File.read(HISTORY_FILE).each_line do |json| Reline::HISTORY << JSON.parse(json) end end
Reline.readmultiline のブロック
text = Reline.readmultiline(PROMPT, true) do |input| debug "" debug "-->> readmultiline block" debug input.inspect true end
こんな感じでデバッグ出力して動作を見てみると、
Enter
キーが押されたタイミングでこのブロックが呼び出されているようだぞ、ということが確認できます。
11
Enter 22
Enter と入力したときのデバッグ出力:
-->> readmultiline block "11\n" -->> readmultiline block "22\n"
このブロックは LineEditor#confirm_multiline_termination_proc
にセットされ、
ed_newline => confirm_multiline_termination => @confirm_multiline_termination_proc
という流れで呼び出されるようです。
ed_newline
という名前のメソッドから呼ばれているので、改行の入力がトリガーになっていると考えて良さそうな雰囲気です。
ブロックの呼び出しはこのようになっていて、
@confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n")
各行の末尾に LF の付いた文字列がブロックの引数に渡ってくることが分かります。
ブロックの評価値の扱いも見てみます。
class Reline::LineEditor # ... private def ed_newline(key) # ... if confirm_multiline_termination finish else key_newline(key) end
真だったら編集完了、偽だったら編集継続となるようです(見た感じでは)。
というわけで、
- 編集中の複数行文字列が引数としてブロックに渡ってくる
- 編集が完了しているかを判断し、完了している場合はブロックの評価値を真にする。途中だったら偽にする。
というあたりを踏まえて、ブロックの中身を修正します。
例として、末尾が ;
になっていたら編集完了というルールにします。
text = Reline.readmultiline(PROMPT, true) do |input| debug "" debug "--> readmultiline block" debug input.inspect finished = input.strip.end_with?(";") debug "finished (#{finished})" finished end
11
Enter ;22
Enter ;
Enter と入力したときのデバッグ出力:
-->> readmultiline block "11\n" finished (false) ... まだ編集の途中 -->> readmultiline block "11\n;22\n" finished (false) ... まだ編集の途中 -->> readmultiline block "11\n;22\n;\n" finished (true) ... 末尾が ; なので編集が完了したと判断
なるほど。基本的なことがやりたいだけであればこのくらい分かっていれば良さそうですね。
プロンプトをカスタマイズする
たとえば、最初の行とそれ以外で異なるプロンプトを表示したいといった場合、
Reline.prompt_proc
に Proc オブジェクトをセットすることでカスタマイズできるようです。
PROMPT = "> " Reline.prompt_proc = Proc.new do |lines| lines.each_with_index.map do |line, i| i == 0 ? PROMPT : "| " end end
$ ruby sample.rb > 11 | 22 | ; text (11 22 ;) >
この場合、Reline.prompt_proc
で生成したプロンプト文字列が優先して使われ、Reline.readmultiline
の第一引数で渡したプロンプト文字列は使われなくなるようです(表面的な挙動を見た感じでは)。
ただし、Reline.readmultiline
の第一引数で渡したプロンプト文字列と長さが異なっていると履歴を移動した際にカーソル位置がずれるので、とりあえず同じ長さにしておくと良いようです。
まとめたもの
require "reline" require "json" HISTORY_FILE = "history" PROMPT = "> " $log = File.open("debug.log", "a") def debug(*args) $log.puts *args $log.flush end def add_history(text) File.open(HISTORY_FILE, "a") { |f| f.puts JSON.generate(text) } end def load_history return unless File.exist?(HISTORY_FILE) File.read(HISTORY_FILE).each_line do |json| Reline::HISTORY << JSON.parse(json) end end def finished?(input) stripped = input.strip return true if stripped == "exit" return true if stripped.end_with?(";") false end Reline.prompt_proc = Proc.new do |lines| lines.each_with_index.map do |line, i| i == 0 ? PROMPT : "| " end end load_history loop do text = Reline.readmultiline(PROMPT, true) do |input| debug "" debug "-->> readmultiline block" debug input.inspect finished = finished?(input) debug "finished (#{finished})" finished end add_history text # 編集が完了した複数行文字列を使った処理 puts "text (#{text})" break if text == "exit" end
メモ
- 使用例については irb のソースも見てみると良さそう
- たとえば v3.0.1 だとここで使われています
https://github.com/ruby/ruby/blob/v3_0_1/lib/irb/input-method.rb#L319
- たとえば v3.0.1 だとここで使われています