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
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
Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。
また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!
こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?
コメント