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

# はじめに

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

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

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

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

[^2]: その昔、[自作コマンドラインツールでオートコンプリートする](https://qiita.com/narutaro/items/5b148b9c475d4200cb4f)のような記事を書きましたが、その `ruby` バージョンです。前回と微妙に違うところとして、今回は `REPL`[^1] 風にしています

# デモ

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

![ac.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32540/ccfe01f1-1813-efae-7351-3d717aea5a2e.gif)

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

テンプレは基本的に `Readline` 使ってこんな感じです。TABで①が呼ばれて、リターンで②が回る

```ruby:一階層のコマンド
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` を使ってみます。


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

みたいな感じです

# 細かい修正

マイナーなケースかもしれませんが、上記のやり方だと、例えばコマンド候補が `pet` と `petGroup` のように `pet` まで補完してもまだ候補が複数マッチする時に、正しい候補が表示されません。これは、`pet` まで補完した時に、`pet` か `petGroup` の候補をだしたいのですが、Hashのキー `pet` にマッチして、一階層降りてしまうので、`pet` の下階層が候補となってしまうためです。これを避けるために、以下の1. のようにしてみました。

```ruby

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/