0
0

More than 3 years have passed since last update.

Gitっぽい自作コマンドラインツールのTab補完をするやつ

Last updated at Posted at 2020-09-06

はじめに

あー、ブラウザ立ち上げるのめんどくさい。またもやコマンドラインツールを作りたい熱がでてきた。なんかこんなやつ。

  • git みたいに階層的にサブコマンドがあるやつ
  • TAB で自動補完するやつ
  • シェルが立ち上がる REPL っぽい雰囲気のやつ1
  • コマンド入力時にサーバの REST API コールするやつ
  • 結果をいい感じに整形してだしてくれるやつ

今回は、コマンドを定義して TAB で保管できるようにするところ。いいサンプルがみつからないので、調べながら作ってみました2

デモ

今回はこんな感じのものを作るまでのところ

ac.gif

コマンドの階層が一つだけの場合

テンプレは基本的に 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 を使ってみます。

petstore.rb

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 で候補をマッチする候補を探す

みたいな感じです

細かい修正

マイナーなケースかもしれませんが、上記のやり方だと、例えばコマンド候補が petpetGroup のように pet まで補完してもまだ候補が複数マッチする時に、正しい候補が表示されません。これは、pet まで補完した時に、petpetGroup の候補をだしたいのですが、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

これで期待した動作になって、心地よくタイプできるようになりました

今後

ひとまず動きましたので、コマンドのパースや処理のところに行きます。補完についてもっといい実装方法があったらアドバイスを!

参考にしたサイト


  1. REPL (Read-Eval-Print Loop) とは、入力・評価・出力のループのこと。 インタプリタにおいて、ユーザーとインタプリタが対話的にコード片を実行できるもの。個人的に Cisco っぽいやつ、とも呼んでいる。 

  2. その昔、自作コマンドラインツールでオートコンプリートするのような記事を書きましたが、その ruby バージョンです。前回と微妙に違うところとして、今回は REPL1 風にしています 

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0