はじめに
あー、ブラウザ立ち上げるのめんどくさい。またもやコマンドラインツールを作りたい熱がでてきた。なんかこんなやつ。
-
git
みたいに階層的にサブコマンドがあるやつ -
TAB
で自動補完するやつ - シェルが立ち上がる
REPL
っぽい雰囲気のやつ1 - コマンド入力時にサーバの
REST API
コールするやつ - 結果をいい感じに整形してだしてくれるやつ
今回は、コマンドを定義して TAB
で保管できるようにするところ。いいサンプルがみつからないので、調べながら作ってみました2
デモ
今回はこんな感じのものを作るまでのところ
コマンドの階層が一つだけの場合
テンプレは基本的に Readline
使ってこんな感じです。TABで①が呼ばれて、リターンで②が回る
require "readline"
# 補完候補のコマンド
pet_store = [ "pet", "store", "user"]
# ①TAB押すと呼ばれて select で補完しようとする
Readline.completion_proc = proc do |input|
pet_store.select { |name| name.start_with?(input) }
end
# ②リターン押すと回る
while input = Readline.readline("$ ", false)
if input
break if input == "q"
p Readline.line_buffer.split if !input.empty?
end
end
一階層の場合はコマンド候補が固定なので簡単です
コマンドが多階層の場合
多階層のコマンドというのは、さっきの例の補完候補のコマンドを現在のコンテキスト(前に選ばれたコマンド)に応じて変化すればいいんだと思います。
要は、「今選ばれているコマンド」に応じて「次の候補」を動的に入れ替えればいいんでしょう。ということでコマンドのツリーのようなものを事前に定義してみます。ツリー構造なので、YAML
を使ってみます。
require "readline"
require "yaml"
command_tree = <<-YAML
pet:
buy:
dog:
cat:
sell:
bird:
fox:
list:
all:
filter_by:
store:
find:
by_name:
by_tag:
by_address:
list:
user:
login:
loout:
sign_up:
YAML
command_tree = YAML.load(command_tree)
# pp command_tree
# 「今選ばれているコマンド」に応じて「次の候補」を動的に入れ替え(ツリーを降りていく)
def current_option(command_tree, line_buffer)
current_option = command_tree.keys
line_buffer.split.each do |command|
command_tree = command_tree[command]
if command_tree.nil?
current_option
else
current_option = command_tree.keys
end
end
current_option
end
comp = proc do |input|
current_option(command_tree, Readline.line_buffer).select { |name| name.to_s.start_with?(input) }
end
Readline.completion_proc = comp
while input = Readline.readline("$ ", true)
break if input == "q"
p Readline.line_buffer.split if !input.empty?
end
やってることは:
- コマンドの階層を
YAML
で定義しておく -
Readline
でキーボード入力を受け取ったのがinput
へ -
TAB
をヒットしたときに、proc
の処理が動き、その時点のinput
を元に、現在のコマンド候補の配列を入れ替える(階層を降る) - その配列に対して
select
で候補をマッチする候補を探す
みたいな感じです
細かい修正
マイナーなケースかもしれませんが、上記のやり方だと、例えばコマンド候補が pet
と petGroup
のように pet
まで補完してもまだ候補が複数マッチする時に、正しい候補が表示されません。これは、pet
まで補完した時に、pet
か petGroup
の候補をだしたいのですが、Hashのキー pet
にマッチして、一階層降りてしまうので、pet
の下階層が候補となってしまうためです。これを避けるために、以下の1. のようにしてみました。
def get_current_option(command_tree, line_buffer)
current_option = command_tree.keys
commands = line_buffer.split
commands.each_with_index do |command, i|
# 1. Don't go down to the lower level(and return current_option) in case current command matches multiple candidates such as "pet" and "petGroup"
return current_option if i == commands.size-1 and !line_buffer.end_with?("\s")
# 2. Go down
if command_tree.has_key?(command)
if command_tree[command].nil? # invalid command or key at leaf
current_option = []
else
command_tree = command_tree[command]
current_option = command_tree.keys
end
end
end
current_option
end
これで期待した動作になって、心地よくタイプできるようになりました
今後
ひとまず動きましたので、コマンドのパースや処理のところに行きます。補完についてもっといい実装方法があったらアドバイスを!
参考にしたサイト
- https://stackoverflow.com/questions/13876024/how-to-write-a-ruby-command-line-app-that-supports-tab-completion
- https://thoughtbot.com/blog/tab-completion-in-gnu-readline-ruby-edition
- https://thoughtbot.com/blog/tab-completion-in-gnu-readline
- http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/151732
- http://bogojoker.com/readline/
-
REPL (Read-Eval-Print Loop) とは、入力・評価・出力のループのこと。 インタプリタにおいて、ユーザーとインタプリタが対話的にコード片を実行できるもの。個人的に Cisco っぽいやつ、とも呼んでいる。 ↩ ↩2
-
その昔、自作コマンドラインツールでオートコンプリートするのような記事を書きましたが、その
ruby
バージョンです。前回と微妙に違うところとして、今回はREPL
1 風にしています ↩