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]
This time until I make something like this
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.
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 --When
TAB 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
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.
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!