[RUBY] A Git-like self-made command line tool that complements Tab

Introduction

Ah, it's annoying to launch the browser. Once again, I was enthusiastic about making command line tools. Something like this.

--A guy with hierarchical subcommands like git --The one that auto-completes with TAB --The one with a REPL-like atmosphere where the shell stands up [^ 1] --The one that calls the server's REST API when entering a command ――The guy who shapes the result nicely

This time, I'm going to define a command so that it can be saved with TAB. I couldn't find a good sample, so I made it while investigating [^ 2]

[^ 1]: REPL (Read-Eval-Print Loop) is an input / evaluation / output loop. An interpreter that allows the user and the interpreter to interactively execute pieces of code. I personally call it a Cisco-ish guy.

[^ 2]: A long time ago, I wrote an article like Autocomplete with my own command line tool, but it's the ruby version. As a slight difference from the last time, this time it is like REPL [^ 1]

demo

This time until I make something like this

ac.gif

When there is only one command hierarchy

The template is basically like this using Readline. ① is called by TAB, and ② turns by return

One-level command


require "readline"

#Completion candidate commands
pet_store = [ "pet", "store", "user"]

#① Called to press TAB and try to complete with select
Readline.completion_proc = proc do |input|
  pet_store.select { |name| name.start_with?(input) }
end

#② Press return to turn
while input = Readline.readline("$ ", false)
  if input
    break if input == "q"
    p Readline.line_buffer.split if !input.empty?
  end
end

In the case of one layer, command candidates are fixed, so it is easy.

When the command is multi-layered

For multi-level commands, I think that the command of the completion candidate in the previous example should be changed according to the current context (previously selected command).

The point is that the "next candidate" should be dynamically replaced according to the "currently selected command". So let's predefine something like a tree of commands. Since it is a tree structure, I will try using 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

#Dynamically replace "next candidate" according to "currently selected command" (go down the 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

What I'm doing:

--Define the command hierarchy with YAML --Received keyboard input with Readline to ʻinput --WhenTAB is hit, the processing of proc moves, and the current command candidate array is replaced (down the hierarchy) based on ʻinput at that time. --Search for a candidate that matches the candidate with select for that array

It's like

Minor correction

It may be a minor case, but with the above method, the correct candidate will be displayed when multiple candidates still match even if the command candidates complete up to pet such as pet and petGroup. not. This wants to come up with a candidate for pet or petGroup when complementing to pet, but it matches the Hash key pet and goes down one level, so under pet This is because the hierarchy becomes a candidate. To avoid this, I tried the following 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

Now you have the expected behavior and you can type comfortably.

from now on

Now that it works, let's go to the command parsing and processing. If there is a better implementation method for complementation, please give me some advice!

Referenced site

Recommended Posts

A Git-like self-made command line tool that complements Tab
[Docker] Use as a command line tool by specifying arguments (wrapper script)