Introduction
Aujourd'hui nous sommes quelques-uns à nous être réunis Porte de la Villette à Paris pour des sprints Python.
Le premier sprint de ce matin a porté sur l'ajout de l'autocompletion Bash des arguments de la commande circusctl.
Pour ce faire, nous nous sommes inspirés de l'autocompletion des commandes django-admin qui est bien réalisée, et voici comment vous allez pouvoir, vous aussi, ajouter l'autocompletion bash à vos commandes.
Bash Autocomplete
La fonctionnalité de completion automatique de bash fonctionne de la manière suivante :
Une fonction bash est définie qui s'occupe de modifier la valeur de la variable bash COMPREPLY. Cette valeur contient la liste des completions possibles séparées par des espaces.
_my_script_completion() {
COMPREPLY=("hello world")
}
Il faut ensuite utiliser complete pour associer une fonction à un nom de programme :
complete -F _my_script_completion -o default my_script.py
Une fois ceci fait, on peut taper :
$ ./my_script.py <tab><tab>hello world
Pour compléter le mot courant, c'est compgen qu'il faut utiliser :
$ cur="hel"
$ opts=("hello world")
$ compgen -W "$opts" -- $cur
hello
Il est important d'utiliser -- avant $cur pour éviter les injections d'options à compgen dans le contenu de $cur.
Et voici un exemple de script complet :
_my_script_completion() {
local args cur opts
# COMPREPLY désigne la réponse à renvoyer pour la complétion
# actuelle
COMPREPLY=()
# argc : index du mot courant (sous le curseur)
argc=${COMP_CWORD};
# cur : mot courant (sous le curseur)
cur="${COMP_WORDS[argc]}"
# les options possibles pour notre auto-complétion
opts="hello world"
# on auto-complete la ligne de commande en recherchant cur
# dans la liste opts.
COMPREPLY=( $(compgen -W "$opts" -- $cur ) )
# A noter que le -- est important ici pour éviter les
# "injections d'options" depuis $cur.
}
complete -F _my_script_completion -o default my_script.py
Pour le tester dans un terminal :
$ source ~/path/to/my_bash_script_completion
$ ./my_script.py <tab><tab>
hello world
$ ./my_script.py hel<tab>lo
Nous avons donc la complétion pour notre script inexistant. Super !
Un fichier bash générique pour nos programmes
En fait ce sont nos programmes qui connaissent la liste des options/arguments valides, ce sont donc à eux de nous retourner la liste des complétions possibles.
Nous pouvons donc passer les arguments $COMP_WORDS et $COMP_CWORD à notre programme et lui demander de retourner une liste de complétion possible.
On va également ajouter une variable $AUTO_COMPLETE pour signaler à notre programme qu'on est en mode autocomplete et éviter tout comportement anormal de notre commande par la suite.
Voici le contenu générique de notre fichier d'autocompletion :
# #########################################################################
# This bash script adds tab-completion feature to my_script.py
#
# Testing it out without installing
# =================================
#
# To test out the completion without "installing" this, just run this file
# directly, like so:
#
# source ~/path/to/my_script_bash_completion
#
# After you do that, tab completion will immediately be made available in
# your current Bash shell. But it won't be available next time you log in.
#
# Installing
# ==========
#
# To install this, source this file from your .bash_profile, like so:
#
# source ~/path/to/my_script_bash_completion
#
# Do the same in your .bashrc if .bashrc doesn't invoke .bash_profile.
#
# Settings will take effect the next time you log in.
#
# Uninstalling
# ============
#
# To uninstall, just remove the line from your .bash_profile and .bashrc.
_my_script_completion() {
COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \
COMP_CWORD=$COMP_CWORD \
AUTO_COMPLETE=1 $1 ) )
}
complete -F _my_script_completion -o default my_script.py
Gérer la complétion du côté du programme
Du côté du programme, voici un exemple d'implémentation en python (my_script.py) :
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
class ControllerApp(object):
"""Controller that manages the command dispatch."""
def __init__(self):
self.options = ['hello', 'world']
def autocomplete(self):
"""Output completion suggestions for BASH.
The output of this function is passed to BASH's `COMREPLY` variable
and treated as completion suggestions. `COMREPLY` expects a space
separated string as the result.
The `COMP_WORDS` and `COMP_CWORD` BASH environment variables are
used to get information about the input. Please refer to the
BASH man-page for more information about these variables.
Note: If debugging this function, it is recommended to write the
debug output in a separate file. Otherwise the debug output will be
treated and formatted as potential completion suggestions.
"""
# Don't complete if user hasn't sourced the bash_completion file.
if 'AUTO_COMPLETE' not in os.environ:
return
# list of individual words on the command line
words = os.environ['COMP_WORDS'].split()[1:]
# index (in the words list) of the word under the cursor
cword = int(os.environ['COMP_CWORD'])
try:
# curr is the current word, with cword being a 1-based index
curr = words[cword - 1]
except IndexError:
curr = ''
print(' '.join(sorted(filter(lambda x: x.startswith(curr),
self.options))))
sys.exit(1)
def main():
controller = ControllerApp()
controller.autocomplete()
if __name__ == '__main__':
main()
Encore une fois, pour le tester :
$ chmod +x my_script.py
$ ./my_script.py<tab><tab>
hello world
$ ./my_script.py he<tab>llo w<tab>orld
Conclusion
En conclusion, ce sprint sur circus m'a permis de trouver un bon moyen de gérer simplement et efficacement la complétion des arguments d'une commande.
Pour la suite du sprint, il faudra lancer une CLI lorsque circusctl est lancé sans arguments.