Écrire un fichier de complétion pour zsh
Table of Contents
zsh est très connu pour son système de complétion très poussé. Malheureusement, zsh n'est pas équipé pour gérer les conventions d'appel de tous les outils qu'on peut trouver sur nos babasses. On peut donc être amené à écrire soi-même une méthode de complétion. Ces notes ont été prises à l'occasion de l'écriture d'un fichier de complétion pour ezjail, et les exemples viennent de là.
1 Parfois, il n'y a presque rien à faire…
Il y a quelques cas où on n'a presque rien à faire pour bénéficier de
la complétion sur un nouvel outil. Par exemple, si un outil utilise
les conventions GNU, zsh peut parser la sortie de
--help
pour
obtenir la liste des options existantes. Il suffit pour cela d'ajouter
quelque chose comme :
compdef _gnu_generic foo
dans un fichier lu par zsh (
.zshrc
conviendra), et de relancer son
shell pour pouvoir compléter les options de
foo
. On a même le droit
à la documentation des options.
Un autre cas où l'on n'a presque rien à faire est lorsqu'on peut réutiliser une complétion existante. Si un outil attend comme argument uniquement des noms d'hôtes, on utilisera :
compdef _hosts foo
Maintenant, en tapant
foo <TAB>
, zsh proposera des complétions
basées sur les noms d'hôtes, trouvés par
`getent hosts`
ou qui sont
dans l'une des bases
ssh_known_hosts
entre autres.
Dans l'exemple au-dessus,
_hosts
est le nom d'une fonction zsh, dont
la définition est (sur mon système) dans
/usr/local/share/zsh/4.3.11/functions/Completion/Unix
.
La documentation de zsh donne un autre exemple assez courant : le cas d'une commande qui accepte tous les fichiers qui ont une certaine extension.
compdef '_files -g "*.h"' foo
Ici,
_files
est encore une fonction, définie au même endroit que
_hosts
(et bon courage à celui qui voudra se plonger dans le code de
cette fonction…). La documentation de cette fonction est visible
dans le manuel de zsh, au nœud «
Completion Functions >
Utility
Functions ».
2 …parfois c'est plus compliqué
Lorsqu'on a des besoins plus poussés, au hasard pour un outil tel
qu'ezjail, on va devoir coder sa propre complétion. Une complétion
n'est en soi qu'une fonction zsh qui est appelée par le shell lorsque
l'utilisateur appuie sur
TAB
(ou
C-d
pour afficher les
complétions).
Les fonctions de complétions sont installées par défaut dans
/usr/local/share/zsh/site-functions/
(l'emplacement exact peut
dépendre de votre OS). Par convention, le fichier dans lequel vous
allez écrire votre fonction de complétion a le même nom que la
commande concerné, avec un
_
ajouté au début.
Lors du développement d'un fichier de complétion, il sera peut-être
plus pratique de pouvoir éditer le fichier sans être
root
. Dans ce
cas, il suffit de choisir un dossier local, par exemple
~/zsh
. On
placera le fichier de complétion dedans, et on l'ajoute au tableau
$fpath
(bien sûr, à placer avant l'appel à
compinit
) :
fpath=(~/zsh $fpath)
Notons que zsh refusera de charger des fonctions (donc des définitions de complétion) si le fichier appartient à un utilisateur autre que root ou l'utilisateur courant, ou s'il est modifiable par un autre utilisateur.
Le fichier de complétion commence avec un tag spécial sur la première ligne :
#compdef ezjail-admin
C'est un commentaire, qui indique à zsh que le fichier contient une complétion, et bien sûr quelle commande est concernée.
Ensuite, on définit une fonction de complétion, et on l'appelle :
_ezjail () { # code de complétion } _ezjail "$@"
2.1 Informations fournies par le shell aux fonctions de complétions
Notre code, ici la fonction
_ezjail
, sera appelée avec quelques
informations sur la ligne de commande qui a déjà été saisie par
l'utilisateur.
Les informations qui sont accessibles depuis le code de complétion seront les suivantes :
Variable | Description |
---|---|
$words
|
Les mots déjà entrés par l'utilisateur |
$CURRENT
|
L'index, dans
$words , du mot que l'utilisateur essaie de compléter (c'est
$#words sauf si l'utilisateur est revenu en arrière)
|
$curcontext
|
Une chaîne de caractères, décrivant le contexte courant |
Le contexte courant est utilisé en interne à zsh pour savoir comment
interpréter les demandes de complétions. Depuis le code de complétion,
on passera aussi
$curcontext
à
zstyle
pour récupérer les styles de
complétion voulus par l'utilisateur. Comme je n'utilise pas
zstyle
,
je ne me suis pas beaucoup avancé dans cette direction.
Si vous avez sous les yeux le
fichier de complétion d'ezjail, vous
verrez que la fonction principale,
_ezjail
, fait un petit tour de
passe-passe sur ces variables pour sauter sur la fonction
correspondant à la sous-commande déjà entrée. La plupart des outils
pourront certainement se passer d'une telle manipulation.
2.2 Renvoyer les candidats à la complétion
Pour renvoyer à zsh les complétions elles-mêmes, on va utiliser l'une
des
fonctions de complétion. La fonction
_arguments
sera notre outil
principal, en particulier pour les outils qui ont des options
standards.
2.2.1 Exemple typique
Si l'on prend comme exemple la complétion (un peu réorganisée) pour
ezjail-admin create
:
_ezjail_cmd_create () { _arguments -s : \\ "-i[file-based jail]" \\ "-x[jail exists, only update the config]" \\ "-a[restore from archive]:archive:_files" \\ "-c[image type]:imagetype:(bde eli zfs)" \\ "-C[image parameters]:imageparams:" \\ "-s[size of the jail]:jailsize:" \\ ":jail name:" \\ ":comma-separated IP addresses:" }
Le
-s
donné à
_arguments
permet de dire que pour cette commande,
les options courtes peuvent être combinées, i.e.
-ix
est équivalent
à
-i -x
. Le texte entre crochets est affiché à côté de la
complétion, sauf si l'utilisateur a désactivé cela.
Dans l'exemple au-dessus,
-i
et
-x
sont des options qui
n'attendent pas d'arguments. Les autres options attendent des
arguments : on les distingue parce qu'une description de l'argument et
une méthode pour compléter cet argument sont données, avec des
:
pour séparer les champs. Dans le cas de
-a
, l'argument sera complété
par la fonction
_files
, qui propose des noms de fichiers comme
choix. Dans le cas de
-c
, la completion se fait sur un petit nombre
de choix possibles, qui sont donnés entre parenthèses. Pour
-C
, on
indique qu'on attend un mot donné par l'utilisateur, mais le shell
n'aidera pas à le compléter.
Enfin, la commande prend encore deux arguments, mais on ne sait pas
offrir d'aide pour les compléter (il n'y a rien avant le premier
:
).
2.2.2 Options mutuellement exclusives
Il peut y avoir des cas où des options sont mutuellement exclusives.
C'est le cas pour
ezjail-admin archive
: ou bien l'on archive toutes
les jails avec
-A
, ou alors on donne une liste de jails.
_ezjail_cmd_archive () { _arguments -s : \\ "-a[archive name]:archive name:" \\ "-d[destination directory]:destination dir:_files -/" \\ "-f[archive the jail even if it is running]" \\ - archiveall \\ "-A[archive all jails]" \\ - somejails \\ "*:jail:_ezjail_stopped_jails" }
On sépare les groupes d'options qui s'excluent avec un tag (je ne sais pas si le nom est réutilisé pour être montré à l'utilisateur dans certaines circonstances). Les options qui peuvent toujours être utilisées sont données en premier. Remarquez au passage l'astérisque pour la dernière ligne : cela permet d'indiquer qu'on peut avoir des répétitions.
2.2.3
_values
: plus simple
Lorsqu'on a juste une liste de mots à proposer, surtout si cette liste
est générée dynamiquement, les arguments pour
_arguments
ne sont pas
faciles à manipuler. Dans ce cas,
_values
, qui permet de simplement
donner la liste des mots candidats à la complétion, est plus facile à
manipuler. En prenant comme exemple
_ezjail
, dans le cas où on
propose les sous-commandes, on a :
_ezjail () { local cmd if (( CURRENT > 2)); then # déjà un mot complet sur la ligne; sauter à la fonction # spécifique à cette sous-commande. else # on complète la sous-commande _values : \\ "archive[create a backup of one or several jails]" \\ "config[manage specific jails]" \\ "console[attach your console to a running jail]" \\ "create[installs a new jail inside ezjail\\'s scope]" \\ "cryptostart[start the encrypted jails]" \\ "delete[removes a jail from ezjail\\'s config]" \\ "install[create the basejail from binary packages]" \\ "list[list all jails]" \\ "restart[restart a running jail]" \\ "restore[create new ezjails from archived versions]" \\ "start[start a jail]" \\ "stop[stop a running jail]" \\ "update[create or update the basejail from source]" fi }
2.2.4
compadd
: pour construire les complétions au fur et à mesure
compadd
est le
builtin utilisé par
_arguments
,
_values
et les
autres fonctions pour ajouter les candidats à la complétion. Il y a
certains cas où il est plus facile d'ajouter un à un les candidats.
C'est le cas pour lister les jails : une première version utilisait
quelque chose de la forme :
_ezjail_running_jails () { _values : `_ezjail_list_jails running` } _ezjail_list_jails () { local jailcfgs= "/usr/local/etc/ezjail" local state=$ 1 local j ( cd $ jailcfgs && echo * ) | while read j; do case $ state in running) [[ -f /var/run/jail_${ j}.id ]] && echo $ j ;; stopped) [[ -f /var/run/jail_${ j}.id ]] || echo $ j ;; *) echo $ j ;; esac done }
Le problème est que lorsque la sortie de
_ezjail_list_jails
est
vide,
_values
remonte un message d'erreur parce qu'on ne lui a donné
aucun argument. Pour corriger cela, une première solution pourrait
être de capturer la sortie de
_ezjail_list_jails
dans une variable,
tester si cette variable est vide ou non, et agir en conséquence. La
deuxième solution, qui est celle que j'ai retenu, est de faire ajouter
à
_ezjail_list_jails
les complétions elles-mêmes. Il faut encore
gérer à part le cas où il n'y a pas de candidats à la complétion, en
revoyant 1 dans ce cas ; mais c'est plus facile à gérer. Au passage,
la première version ne renvoyait pas 1 en l'absence de candidates…
L'implémentation ressemble à ceci :
_ezjail_running_jails () { _ezjail_list_jails running } _ezjail_list_jails () { local jailcfgs= "/usr/local/etc/ezjail" local state=$ 1 local ret=1 local j for j in $ jailcfgs/*(:t) ; do case $ state in running) [[ -f /var/run/jail_${ j}.id ]] && compadd $ j && ret=0 ;; stopped) [[ -f /var/run/jail_${ j}.id ]] || compadd $ j && ret=0 ;; *) compadd $ j && ret=0 ;; esac done return $ ret }
compadd
, en tant que brique de base, permet bien plus que de
simplement ajouter un candidat. Encore une fois, allez voir la
documentation pour tous les détails.
2.3 Style
Le code de complétion est lancé à chaque fois que l'utilisateur
tapotte sa touche
TAB
ou
C-d
. Le code doit donc être
raisonnablement rapide.
Par ailleurs, le code de complétion s'exécute dans le shell courant de l'utilisateur. En particulier :
- le code n'a pas le droit de faire un
exit
; - il ne peut pas afficher de chose sur
std{out,err}
sans faire une horrible mixture entre le prompt, les propositions de complétion et ce qui est affiché ; - il faut penser à retourner 1 si on n'a pas de candidats à la complétion ;
- toutes les variables utilisées doivent être déclarées avec
local
, pour éviter d'écraser ou de polluer les variables du shell courant de l'utilisateur ; - on écrit du code pour zsh, on a toute latitude pour écrire du code qui n'est pas POSIX.
Il y a également
completion-style-guide
, qui est chez moi dans
/usr/local/share/doc/zsh
(
copie en ligne), qui donne des conseils.
J'ai découvert ce fichier un peu tard, et la complétion pour ezjail
ne suit pas toujours à la lettre les conseils donnés.
Bien sûr, lorsqu'on a quelque chose qui marche, on le propose au
projet en question, ou alors directement sur la liste
zsh-workers@
,
pour en faire profiter les petits copains.
3 À voir
A User's Guide to the Z-Shell, Peter Stephenson, Chapter 6: Completion, old and new. Il y a en particulier un tutorial bien fait.
The Z Shell Manual,
20. Completion System, et en particulier
20.6
Utility Functions. Vous en avez certainement une version accessible en
local avec
info zsh
(ou
C-h i m zsh
depuis
emacs
). Le manuel est
assez aride, je ne l'ai utilisé que comme référence.
zsh completion support for ezjail , de votre serviteur.