[RUBY] Un outil de ligne de commande de type Git qui complète Tab

introduction

Ah, c'est ennuyeux de lancer le navigateur. Encore une fois, j'étais enthousiaste à l'idée de créer des outils de ligne de commande. Quelque chose comme ça.

Cette fois, je vais définir une commande pour qu'elle puisse être enregistrée avec TAB. Je n'ai pas pu trouver un bon échantillon, alors je l'ai créé lors de mon enquête [^ 2]

[^ 1]: REPL (Read-Eval-Print Loop) est une boucle d'entrée / d'évaluation / de sortie. Un interpréteur qui permet à l'utilisateur et à l'interpréteur d'exécuter de manière interactive des morceaux de code. Personnellement, je l'appelle un gars Cisco-ish.

[^ 2]: Il y a longtemps, j'ai écrit un article comme Autocomplete with my own command line tool, mais c'est la version ruby. Comme une légère différence par rapport à la dernière fois, cette fois, c'est comme REPL [^ 1]

démo

Cette fois jusqu'à ce que je fasse quelque chose comme ça

ac.gif

Lorsqu'il n'y a qu'une seule hiérarchie de commandes

Le modèle est fondamentalement comme celui-ci en utilisant Readline. ① est appelé par TAB, et ② tourne par retour

Commande à un niveau


require "readline"

#Commandes candidates d'achèvement
pet_store = [ "pet", "store", "user"]

#① Appelé pour appuyer sur TAB et essayer de terminer avec select
Readline.completion_proc = proc do |input|
  pet_store.select { |name| name.start_with?(input) }
end

#② Appuyez sur retour pour tourner
while input = Readline.readline("$ ", false)
  if input
    break if input == "q"
    p Readline.line_buffer.split if !input.empty?
  end
end

Dans le cas d'une couche, c'est facile car les candidats de commande sont fixes

Lorsque la commande est multicouche

Pour une commande multicouche, je pense que la commande du candidat à l'achèvement dans l'exemple précédent doit être modifiée en fonction du contexte actuel (commande précédemment sélectionnée).

Le fait est que le "prochain candidat" doit être remplacé dynamiquement selon la "commande actuellement sélectionnée". Prédéfinissons donc quelque chose comme un arbre de commandes. Puisqu'il s'agit d'une structure arborescente, essayez d'utiliser 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

#Basculer dynamiquement "prochain candidat" en fonction de la "commande actuellement sélectionnée" (descendre dans l'arborescence)
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

Qu'est-ce que je fais:

--Définissez la hiérarchie des commandes avec YAML --L'entrée du clavier a été reçue par Readline à ʻinput`. --Lorsque «TAB» est atteint, le traitement de «proc» fonctionne, et la séquence actuelle des candidats de commande est remplacée (dans la hiérarchie) en fonction de «l'entrée» à ce moment-là.

C'est comme

Correction mineure

Cela peut être un cas mineur, mais avec la méthode ci-dessus, le bon candidat est affiché lorsque plusieurs candidats correspondent encore même si les candidats de commande se terminent jusqu'à "pet" tels que "pet" et "petGroup". ne pas. Cela veut trouver un candidat pour pet ou petGroup en complétant jusqu'à pet, mais il correspond à la clé Hash pet et descend d'un niveau, donc sous pet C'est parce que la hiérarchie devient un candidat. Pour éviter cela, j'ai essayé ce qui suit 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

Vous avez maintenant le comportement attendu et vous pouvez taper confortablement.

à partir de maintenant

Maintenant que cela fonctionne, passons à l'analyse et au traitement des commandes. S'il existe une meilleure méthode de mise en œuvre pour la réalisation, donnez-moi quelques conseils!

Site référencé

Recommended Posts

Un outil de ligne de commande de type Git qui complète Tab
[Docker] Utiliser comme outil de ligne de commande en spécifiant des arguments (script wrapper)