Le problème
Très rapidement : les outils en ligne de commande sont peu découvrables. On tape une commande puis quoi ?

On lit le manuel GNU de ses morts ? On tente peut-être un -h ou un --help
qui listera un sous-ensemble des options sans trop d’explications, ou au
contraire, imprimera la bible toute entière1 ? Si l’outil est pas trop
compliqué, mettons mv, c’est jouable. Et encore, il faut savoir que man mv
existe, désepérer du fait que ce soit un manuel de référence, éventuellement
savoir que info mv existe, probablement plutôt aller voir des tutos sur
internet et se rendre compte que, puisque l’on est en 2026, la plupart sont
générés par IA. Bref pas facile.
Une solution pourrait être de faire de l’auto-complétion. Grâce à la magie de ce qu’il se passe lorsqu’on appuie sur la touche tabulation en écrivant une commande dans un shell moderne2 le shell peut nous indiquer ce qu’il est possible d’écrire à cet endroit de la commande.
J’ai trouvé très peu de tutoriels satisfaisant sur la question donc j’en écris un :)
Les limites
Dans cet article je partage ce que j’ai appris en écrivant l’auto-complétion
zsh pour l’outil tricount. Il en
découle :
- qu’il ne couvre pas l’auto-complétion
bash(mais j’ai cru comprendre que c’était similaire) - que l’on ne parle pas de découvrabilité lorsque l’on ne sait même pas quelle commande taper (voir plutôt zenu pour ça)
- que ce n’est pas un tutoriel complet sur l’auto-complétion
zshmais uniquement ce dont j’ai eu besoin pourtricount.
Par ailleurs j’émets l’hypothèse que l’auto-complétion est significativement utile pour la découvrabilité d’une commande mais je ne l’ai pas réellement testé. Pour pouvoir réellement mesurer l’intérêt de l’auto-complétion pour la découvrabilité il faudrait que je le fasse tester à des copaines sans et avec l’auto-complétion mais on est pas ici pour faire de la recherche lol 🤓
Le résultat
L’objectif est de passer de ça :
à ça (vidéo de 500Ko, cliquez dessus pour lancer) :
On remarquera que l’outil fonctionne avec une syntaxe de commande, sous-commande et objet. Les sous-commandes et objets disponibles dépendent de la commande choisie. Par exemple la commande creer ne nécessite rien d’autre mais si l’on choisit la commande ajouter il faudra ensuite choisir un objet entre dépense et personne. Si l’objet est dépense il faudra choisir le nom d’une personne puis un montant etc.
Ce qui rend l’auto-complétion vraiment utile est qu’elle est entièrement
contextuelle. C’est ce que fait par exemple l’auto-complétion zsh de la
commande sed. L’auto-complétion sait si l’on est en position d’écrire une
nouvelle commande (ici s///) ou d’apporter une modification à une commande s
(g et i par exemple) :
Le code
La base, compadd
L’auto-complétion de zsh repose sur des fonctions d’auto-complétion3. Ces
fonctions sont des fonctions shell classiques appelées à chaque fois que
l’utilisateurice appuie sur la touche tabulation4. Elles commencent
habituellement par un _ et il est apparemment conventionnel de les nommer
du nom de la commande qu’elles complètent bien que ce ne soit pas obligatoire.
Imaginons donc une fonction de complétion tout à fait inutile affichant a,
destinée à compléter la commande a5 :
_a(){ printf "a"; }
Pour l’associer à la commande a (inexistante mais c’est accessoire pour le
moment) il existe une commande compdef qui prend en argument le nom d’une
fonction puis le nom de la commande qu’elle doit compléter :
compdef _a a
Pour vérifier le résultat il suffit d’initier une commande a, de mettre un
espace puis d’appuyer sur tabulation :
C’est très bien mais ça ne permet pas l’auto-complétion. Pour programmer ce que
l’on souhaite, c’est à dire la sélection des options et arguments parmi un
ensemble restreint il faut utiliser une seconde fonction zsh, compadd6.
Dans sa forme la plus simple compadd s’utilise en lui passant en argument
les “candidats” à l’auto-complétion. Si l’on ajoute 1234, 5678 et 91011
alors ces trois chaînes seront proposées à l’auto-complétion via un petit menu
s’affichant en dessous de la ligne de commande en cours et navigable avec les
flèches. On verra plus
tard
qu’il est possible de modifier l’affichage de ce menu pour par exemple donner
des informations à propos des candidats.
Si l’on commence à écrire un argument et que le début match avec l’un de ces candidats de manière non ambiguë, l’auto-complétion l’ajoutera automatiquement :
Puisque la fonction dans laquelle on l’écrit est une fonction shell
classique on peut entourer les compadd de toute la logique nécessaire pour
trouver les bons candidats et les afficher au bon moment7. Reste alors à
connaître les différentes options pratiques de compadd et les variables
disponibles pour connaitre l’état de la commande en cours d’écriture.
Ses options
Afficher des message d’information ou d’erreurs
Pour ajouter un texte explicatif avant les candidats on peut utiliser l’option
-X ou -x de compadd. -X s’affichera uniquement s’il existe des candidats,
-x s’affichera quoi qu’il arrive. -x est donc assez utile pour afficher des
messages “d’erreurs”. Ainsi dans la vidéo suivante on utilise -X pour
afficher le message “Choisir une personne à retirer” mais -x pour afficher
“Personne à retirer” :
Auto-compléter plusieurs candidats d’un coup avec -Q
Il peut être utile qu’un unique candidat soit lui même la concaténation de plusieurs arguments. Disons par exemple qu’à un état de l’auto-complétion on veuille faire exécuter la sous-commande “cmd1” suivi de la sous-sous-commande “cmd2” à l’utilisateurice. Si l’on ajoute naïvement la chaîne “cmd1 cmd2” en candidat le système le considère en un seul bloc et échappe l’espace entre les deux :
a cmd1\ cmd2
Pour y remédier il faut utiliser l’option -Q :
compadd -Q "cmd1 cmd2"
Qui auto-complètera :
a cmd1 cmd2
A l’éxécution la commande a comprendra bien cmd1 et cmd2 comme deux
arguments différents.
Cette astuce est utilisée au tout début de la vidéo du résultat pour
automatiquement ajouter nom-bdd creer en l’absence de bdd existante.
Ne pas trier automatiquement les candidats avec -J arg -o nosort
Par défaut le système d’auto-complétion trie les candidats par ordre
alphabétique. Si ce n’est pas souhaitable et qu’on veut les afficher dans
l’ordre fourni à compadd il faut associer l’option -o nosort avec l’option
-J qui prend un argument. -o ne fonctionne pas seul, il faut créer un
“groupe” de candidat avec l’option -J en écrivant, par exemple, -J a. Je
n’ai pas eu besoin d’utiliser les groupes de candidats donc je n’en sais pas
plus.
Avec la commande compadd -J a -o nosort b c a le système nous proposera les
candidats dans l’ordre b c a plutôt que a b c.
Décorréler l’affichage des candidats de l’argument auto-complété avec -ld
Par défaut ce qui s’affichage dans le menu d’auto-complétion est identique à la
valeur des candidats. Si l’on a pour candidats a b c le mnu proposera a b c
et notre choix complètera a, b ou c.
Il est parfois utile de décorréler ce qui est affiché dans le menu de choix et
ce qui est réellement complété. Pour cela il faut associer les options
-l et -d. -l demande à ce qu’un seul candidat soit affiché par ligne et
-d permet de renseigner un tableau de valeurs séparées par des espaces. Le
système va associer une à une les valeurs de ce tableau et les candidats qu’il a
récupéré en argument de compadd. Le menu de choix affichera visuellement le
contenu du tableau -d mais ce sera les candidats associés qui seront ajoutés
dans la ligne de commande. Ainsi avec le tableau et l’appel à compadd suivants :
desc="(
creer\ \ \ \ --\ Créer\ une\ nouvelle\ base\ de\ donnée
ajouter\ \ --\ Ajouter\ une\ dépense\ ou\ une\ personne
retirer\ \ --\ Retirer\ une\ dépense\ ou\ une\ personne
lister\ \ \ --\ Afficher\ le\ contenu\ de\ la\ bdd
calculer\ --\ Calculer\ qui\ doit\ combien\ à\ qui
)"
compadd -J a -o nosort -ld $desc creer ajouter retirer lister calculer
Le système proposera visuellement l’option creer -- Créer une nouvelle base
de donnée pour le candidat creer et ainsi de suite. Puisque les éléments du
tableau du menu de choix sont séparés par des esapces il faut bien en échapper
les espaces.
Cette technique est utilisée à deux reprises dans la vidéo du résultat, d’abord pour les commandes vues dans l’exemple ici, ensuite pour sélectionner la dépense à retirer (l’utilisateurice navigue dans la bdd ligne par ligne mais uniquement l’identifiant de la dépense est auto-complété).
Ne pas supprimer les candidats doublons avec -2
Par défaut le système supprime les candidats doublons. Si l’on fait compadd 1 1
2 seul deux candidats seront proposés, 1 et 2. Il peut être utile de ne pas
les dédoublonner, en particulier en combinaison avec l’option précédente -d.
On peut par exemple vouloir proposer le retrait d’un élément qui apparaît à
plusieurs endroits dans une base de donnée sous plusieurs formes différentes. Il
faut donc que le candidat puisse apparaître plusieurs fois pour être associé
correctement au tableau du menu de choix. Pour ne pas dédoublonner il faut
utiliser l’option -2 :
desc="(
dépense1\ blabla\ truc
dépense1\ bidule\ machin
dépense2\ chouette\ aaaa
)"
compadd -J a -2 -ld $desc 1 1 2
A noter que, comme l’option -o, cette option nécessite l’option -J. Cette
option est utilisée lors du retrait d’une dépense, vers la seconde 52 de la
vidéo de résultat. On y voit trois options dans le menu dont les deux premières
sont associée à deux candidats de valeurs 1. Avec dédoublonnage cela n’aurait
pas été possible.
Les variables d’environnements
Savoir à quel numéro d’argument on en est avec $CURRENT
Dans la variable $CURRENT se trouve le numéro du mot que l’on est en train
d’écrire/auto-compléter. Le décompte commence à 1 et le premier mot est toujours
la commande en cours. Le premier paramètre (argument ou option, peu importe) est
donc à 2 et ainsi de suite. En utilisant compadd -x pour visualiser la valeur
de la variable en dessous de la commande :
On peut utiliser le contenu de cette variable pour faire varier l’auto-complétion, voir l’exemple ci-dessous.
Connaître la valeur d’un mot déjà rempli avec $words
Il peut être utile de savoir quelle est la valeur d’un paramètre déjà écrit.
Pour cela on peut lire dans le tableau $words à l’indice correspondant. Le
premier éléments ($words[1]) est toujours le nom de la commande en cours, le
second (words[2]) le premier paramètre etc. En utilisant la même astuce que
précédemment :
Tout mettre ensemble, l’exemple de tricount
Voyons comment utiliser tout ça pour reconstruire l’auto-complétion de
tricount. Je commente le code ligne par ligne en passant vite sur les aspects
purement shell.
D’abord, déclarer la fonction au nom _commande et inscrire quelle commande
elle auto-complète :
#compdef tricount
_tricount() {
Ensuite écrire éventuellement en dur l’endroit où se trouve les bases de données et récupérer la valeur de la base courante, de l’action à mener dessus et l’objet :
local BDDFOLDER="/srv/tricount"
local curbdd="$words[2]"
local action="$words[3]"
local objet="$words[4]"
Puisque cette fonction est appelée à chaque fois que la touche tabulation est
lancée les variables curbdd, action et objet peuvent très bien être vides
parce que la liste $words n’est pas encore remplie.
Si on est au deuxième argument c’est que l’on cherche à choisir une base de donnée. Il faut récupérer la liste en regardant dans le dossier correspondant. S’il y en a au moins une on les ajoute en tant que candidats avec un message explicatif. S’il n’y en a pas on peut pré-remplir la commande pour en créer une avec un nom bidon modifiable :
if [ "$CURRENT" = 2 ];then
bdds=$(find $BDDFOLDER -type f | cut -d'/' -f4 | sort)
if [ "$bdds" ];then
compadd -X "Choisir une base de donnée" $bdds
else
compadd -Qx "Aucune bdd de dispo, go en créer une" "nom-bdd creer"
fi
fi
Si on est au troisième argument c’est que l’on cherche à effectuer une commande
sur une bdd existante ou pas. Si la bdd n’existe pas on ajoute techniquement
l’action creer en tant que candidat. Sinon on prépare les variables contenant
les candidats et leurs descriptions en faisant usage de -l et -d.
if [ "$CURRENT" = 3 ];then
if ! [ -f $BDDFOLDER/$curbdd ];then
compadd creer
else
actions=(ajouter retirer lister calculer)
desc="(
ajouter\ \ --\ Ajouter\ une\ dépense\ ou\ une\ personne
retirer\ \ --\ Retirer\ une\ dépense\ ou\ une\ personne
lister\ \ \ --\ Afficher\ le\ contenu\ de\ la\ bdd
calculer\ --\ Calculer\ qui\ doit\ combien\ à\ qui
)"
compadd -X "Choisir une action" -J a -o nosort -ld $desc $actions
fi
fi
Si on est au quatrième argument et que l’action (le troisième) est ajouter ou
retirer alors on propose un objet à ajouter ou retirer :
if [ "$CURRENT" = 4 ] && { [ "$action" == "ajouter" ] || [ "$action" == "retirer" ]; };then
compadd -X "Choisir un objet à $action" depense personne
fi
Si l’action est retirer et l’objet depense alors on récupère les
identifiants des dépenses et on construit ensuite un tableau pour l’affichage du
menu en réorganisant les lignes de dépenses de la bdd et en échappant les
espaces8. Finalement on ajoute les identifiants en tant que candidats et les
lignes de dépenses en tant qu’option visuelles. Aussi on retire le tri par
défaut qui mélangerait toutes les dépenses et on demande à ne pas supprimer les
candidats doublons. Ainsi les éventuelles multiples lignes pour une dépense
donnée auront bien l’identifiant de la dépense en candidat en face.
if [ "$action" = "retirer" ] && [ "$objet" = "depense" ];then
ids=($(< "$BDDFOLDER/$curbdd" cut -f5 | sed 1d | paste -s -d' '))
# Ex: ids="1 1 2"
desc="(
$(< "$BDDFOLDER/$curbdd" awk 'BEGIN{OFS="\t"};NR>1{print $5,$1,$2,$3,$4}' | column -ts' ' | sed 's# #\\ #g')
)"
# Ex: desc="(
# dépense1\ blabla\ truc
# dépense1\ bidule\ machin
# dépense2\ chouette\ aaaa
# )"
compadd -X "Choisir une dépense à retirer" -J a -2 -o nosort -ld $desc $ids
fi
Si l’action est retirer et l’objet de retrait une personne alors on récupère
la liste des personnes dans la base de donnée. Si la liste est vide on affiche
un message d’erreur comme quoi il n’y a personne à retirer, sinon on ajoute la
liste en candidats :
if [ "$action" = "retirer" ] && [ "$objet" = "personne" ];then
personnes=($(< "$BDDFOLDER/$curbdd" head -n1 | tr ' ' '\n' | sed 1d | sort))
if ! [ "$personnes" ];then
compadd -x "Il n'y a aucune personne à retirer"
else
compadd -X "Choisir une personne à retirer" -o nosort $personnes
fi
fi
Si l’action est ajouter et l’objet une depense alors on récupère la liste
des personnes de la base de donnée :
if [ "$action" = "retirer" ] && [ "$objet" = "personne" ];then
personnes=($(< "$BDDFOLDER/$curbdd" head -n1 | tr ' ' '\n' | sed 1d | sort))
Puis on vérifie à quelle étape de la construction de la dépense on est. Si l’on est au paramètre 5 c’est le début et on cherche à ajouter la personne qui paye. S’il n’y a pas de candidats disponible on peut afficher un message d’erreur :
if [ "$CURRENT" = 5 ];then
if ! [ "$personnes" ];then
compadd -x "Pas de personnes disponibles :("
compadd -x "Vous pouvez tout de même entrer un nom, ça fonctionnera"
else
compadd -X "Choisir la personne qui paye" $personnes
fi
fi
Si l’on est à l’argument 6 on cherche à ajouter un montant :
[ "$CURRENT" = 6 ] && compadd -X "Choisir un montant" -J a -o nosort $(seq 1 50)
Si l’on est à l’argument 7 on cherche à ajouter une raison pour la dépense. Il est possible de faire un peu de pré-traitement sur les candidats pour qu’ils évoluent avec la base de donnée. Ici les raisons apparaîtront dans l’ordre de la plus utilisée à la moins utilisée avec quelques raisons d’exemple en bonus à la fin :
if [ "$CURRENT" = 7 ];then
raisons=($(< "$BDDFOLDER/$curbdd" cut -f4 | sed 1d | sort | uniq -c | awk '{print $2}' | paste -s -d' '))
compadd -X "Mettre une raison" -J a -o nosort $raisons repas transport courses ...
fi
Puis finalement si l’on est à l’argument 8 on chercher à ajouter les
bénéficiaires de la dépense. On peut afficher plusieurs lignes de messages en
multipliant les appels à compadd -x. On peut ajouter “à la main” un candidat
en plus de ceux récoltés dans une liste comme le candidat toustes ici :
if [ "$CURRENT" -ge 8 ];then
if ! [ "$personnes" ];then
compadd -x "Pas de personnes disponibles :("
compadd -x "Vous pouvez tout de même entrer un nom, ça fonctionnera"
else
compadd -X "Choisir une ou plusieurs personnes bénéficiaires" $personnes toustes
fi
fi
Cet exemple n’est pas parfait. On pourrait factoriser un certain nombre de choses et sortir de la fonction rapidement après avoir ajouté des candidats plutôt que de faire tous les tests alors même que l’on sait qu’ils seront faux. Il est ici à titre d’exemple.
Installation
Pour activer l’auto-complétion zsh dans toutes ces sessions il faut ajouter
les lignes suivantes dans son fichier ~/.zshrc9 :
autoload -Uz compinit
compinit
# Pour pouvoir parcourir le menu des suggestions
# avec les flèches comme dans les vidéos
zstyle ':completion:*' menu yes select
Il faut ensuite écrire la fonction d’auto-complétion avec ceci pour toute première ligne :
#compdef cmd
_cmd() {
[...]
}
Cette première ligne est un “faux” commentaire permettant à zsh de savoir que
la fonction qui suit doit être utilisée pour compléter la commande cmd comme
si l’on avait lancé compdef _cmd cmd à la main.
Finalement il faut installer le fichier, préférablement nommé _cmd dans le
dossier /usr/share/zsh/functions/Completion/Unix/_cmd10. Au lancement zsh
lit tous les fichiers présents dans ces dossiers, regarde la première ligne et
fait l’association entre la fonction de complétion et la commande.
De l’auto-complétion personnalisée ?
L’un des mécanismes principaux pour faciliter la découvrabilité et l’usage du shell, notamment des commandes les plus complexes, est de restreindre l’espace d’exploration. C’est ce qui est par exemple fait dans cet article11 via un système capable de parser un ensemble restreint de commandes shell, sélectionné par un·e experte, et rendant une GUI exposant les options et arguments utilisé.es par ce sous-ensemble plutôt que la totalité des fonctionnalités de la commande.
C’est également en partie l’idée derrière
zenu, à savoir mettre des commandes
parfois complexes derrière des menus pour faciliter la navigation, l’usage et le
partage de ces commandes tout en diminuant la charge cognitive.
Malheureusement les fonctions d’auto-complétion fournies par défaut avec zsh
laissent à désirer de côté là. Par exemple lorsque l’on auto-complète la
commande convert d’image-magick après avoir écrit un - dans zsh on obtient
:

C’est sympa d’avoir des petites descriptions mais la taille de la liste est
intimidante et rend la complétion assez peu utile. Ce n’est pas un diss contre
les contributeurices des fonctions de complétion de zsh, cela s’explique par
le fait que :
zshest utilisé par des (dizaines de ?) millions de personne dans le monde. Il n’est donc pas possible de créer une fonction de complétion qui convienne à tout le monde. À défaut la philosophie retenue semble être de faire une fonction assez peu directive mais exhaustive.- Image-magick comporte un nombre incalculable de fonctionnalités (peut-être trop).
J’émets l’hypothèse qu’en adoptant une approche située, en sachant pour qui et quels usages on créé les fonctions de complétion, il serait possible d’en faire des outils pédagogiques intéressants. Il n’y aurait pas une fonction d’auto-complétion exhaustive mais ne satisfaisant réellement personnes mais une multitude de fonctions que l’on se partagerait selon nos pratiques ou notre niveau de familiarité avec l’outil.
Bien sûr cela nécessite d’écrire du code mais c’est pour ça que je fais ce tuto :)
Références
La doc zsh de référence à propos de compadd est ici :
https://zsh.sourceforge.io/Doc/Release/Completion-Widgets.html#Completion-Builtin-Commands
-
yt-dlp -h | wc -l= 892. Et en plus faut attendre deux secondes pour que ça s’affiche. ↩ -
Par moderne j’entends sous la forme que l’on utilise aujourd’hui depuis au moins la version de
zshdatant de 1999 et depuis bien plus longtemps de manière plus primitive dans d’autres shells. ↩ -
J’utilise “auto-complétion” et “complétion” de manière interchangeable ↩
-
aussi connue sous le nom de la touche “flèche flèche” ↩
-
en
zshpas besoin du;de fin quand on écrit des fonctions sur une seule ligne mais en, team posix ici ↩ -
la documentation
zshprécise quecompaddest assez “bas niveau” et qu’il est souvent préférable d’utiliser les très nombreuses autres fonctions proposées parzshwrappantcompadd. Le souci est qu’elles sont très nombreuses, compliquées et qu’elles sont surtout utiles pour faire de l’auto-complétion de commandes respectant des conventions bien établies (combo d’option courtes et longues---, certaines des flags, d’autres suivies d’une chaÎne de caractère, parfois des fichiers, parfois des IP etc). Puisque l’on fait un peu n’imp avectricountet que la documentation est assez opaque j’ai trouvé qu’il était plus facile d’apprendre uniquementcompadden partant du principe que tout était possible avec. ↩ -
Ou carrément exécuter des commandes pour lancer un logiciel ou je ne sais trop quoi d’autre. C’est étrange parce que ce n’est clairement pas ce qui est attendu par les utilisateurices mais peut-être que ça peut convenir à votre manière de travailler. ↩
-
Un peu de quoting hell ici puisque je connais très mal zsh et ses structures de données. Je pense qu’on peut faire mieux ↩
-
Ou tout autre endroit qui est sourcé à la création de chaque shell interactif, si vous sachez vous sachez ↩
-
du moins pour Debian, probablement pour tous les linux. ↩
-
A propos duquel je pense écrire un article de blog bientôt d’ailleurs ↩