Comment créer très simplement des commandes CLI, avec commandes, drapeaux, et sous-commands multiples ?
C’est facile avec google/subcommands.
Voyons comment.
Les données du problème
Tout d’abord, de quoi parle-t-on ?
N’importe quel programme Go est - au moins conceptuellement - lancé depuis une
ligne de commande, que ce soit la réalité depuis un shell, ou depuis un outil
d’automatisation utilisant l’appel système execve(2)
.
D’un point de vue terminologie, prenons l’exemple d’une commande typique:
docker -D container inspect -s some_container
docker
est le programme, ou application-D
est un drapeau (flag) globalcontainer
est une commande racine, ou de premier niveauinspect
est une sous-commande de la commandecontainer
; c’est plus généralement une commande elle aussi, mais de niveau 2-s
est un drapeau local (local flag) de la sous-commandeinspect
some_container
est un argument de la sous-commandeinspect
Pour interpréter une telle structure, le code Go doit se fonder sur une seule
variable globale, os.Args
, dont le premier élément est le chemin absolu du
programme lancé, et le reste l’ensemble des arguments qui lui sont passés.
Pour éviter d’avoir à réaliser dans chaque programme ce travail extrêmement encadré par les usages, diverses bibliothèques plus ou moins complexes existent, dont les plus populaires sont:
Module | ☆ | ⑂ | Dép. dir. |
---|---|---|---|
spf13/cobra | 25400 | 2200 | 5 |
urfave/cli | 17400 | 1500 | 3 |
alecthomas/kingpin | 3200 | 240 | 5 |
google/subcommands | 594 | 51 | 0 |
Les trois premières sont de loin les plus connues et les plus riches fonctionnellement, mais elles alourdissent les projets d’un graphe de dépendances indirectes parfois important (3136 pour Cobra 1.3). Justifiable dans des projets de grande complexité comme Docker ou Kubernetes, cette complexité n’est pas souhaitable dans des projets typiques comme de petits utilitaires CLI ou des microservices.
C’est là qu’intervient google/subcommands, qui fournit l’essentiel des services de création de commandes sans ajouter aucune dépendance, en un seul module de 500 lignes environ, d’intégration très simple et de poids négligeable, même pour les projets les plus simples et légers. C’est celui que nous allons examiner dans ce didacticiel.
Tout au long des niveaux successifs de raffinement de cette démo, nous allons
enrichir le même exemple, disponible sur https://github.com/fgm/subcommands_demo,
dont chaque branche correspond à la section du même nom dans ce didacticiel,
et où vous trouverez l’ensemble du code source pour le niveau en cours,
et un Makefile
configuré pour démontrer le sujet traité par le niveau concerné.
Ce didacticiel couvre 100% de la fonctionnalité disponible dans google/subcommands, d’où sa longueur ; mais vous aurez de quoi créer une application complète typique dès le niveau 1.2 : les niveaux suivants ajoutent des fonctionnalité destinées à des projets progressivement plus étendus et complexes.
Niveau 1: Commandes simples
Au Niveau 1, nous allons créer des commandes de premier niveau,
en organisant notre code dans deux répertoires, la racine et cmd/
,
dont les principaux fichiers sont les suivants :
Chemin | Contenu |
---|---|
go.mod | le fichier de description du module |
main.go | le point d’entrée du programme |
Makefile | le fichier de commandes |
cmd/ | le répertoire des commandes |
cmd/root.go | le code de lancement des commandes |
Recommandation: Dans chaque branche, la commande make
exécute une séquence par défaut différente,
qui illustre les changements apportés par la branche en cours:
il est recommandé de l’utiliser sur votre machine à chaque changement de branche
pour observer l’impact du nouveau code.
1.1 Créer des commandes en quelques lignes
Le code de ce niveau est disponible sur level1.1-builtin_commands.
À ce niveau, le code se compose de deux fichiers: le point d’entrée main.go
,
ci-dessous:
|
|
…et le code de lancement des commandes dans cmd/root.go
:
|
|
Ce code enregistre les trois commandes facultatives fournies par subcommands
lui-même,
qui permettent de lister les commandes et drapeaux disponibles et d’activer l’aide en ligne, puis il analyse les drapeaux globaux et passe le contrôle à la commande
désignée par les arguments de ligne de commande, dont il renvoie le résultat.
À ce stade, notre squelette est prêt et reconnaît les commandes commands
, flags
, et help
.
$ go run . help
Usage: subcommands_demo <flags> <subcommand> <subcommand args>
Subcommands:
commands list all command names
flags describe all known top-level flags
help describe subcommands and their syntax
$
C’est maintenant à nous de créer notre première commande.
1.2 Créer ses propres commandes
Le code de ce niveau est disponible sur level1.2-custom_commands.
La meilleure pratique pour organiser ses commandes consiste à créer un fichier par commande, pour faciliter la localisation et la comparaison du code des commandes entre elles. Nous allons donc ajouter deux fichiers pour les commandes racines que nous allons créer:
top1
qui affiche un message fixetop2
qui affiche un message lié aux arguments qui lui sont passés
Chemin | Contenu |
---|---|
cmd/top1.go | la commande top1 |
cmd/top2.go | la commande top1 |
Pour google/subcommands, une commande est une variable, d’un type implémentant
l’interface subcommands.Command.
Nos deux fichiers seront donc très similaires, sur la structure du fichier cmd/top2.go
suivant :
|
|
Ici, top2
implémente strictement l’interface subcommands.Command
et rien de plus:
top2.Name()
renvoie le nom de la commandetop2.Synopsis
renvoie une description résumée de la commande, sur une seule lignetop2.Usage()
renvoie un exemple d’utilisation de la commande, qui peut s’étaler sur plusieurs lignes. Elle est utilisé par la commandehelp
.top2.SetFlags()
définit les drapeaux locaux de la commande, en l’occurence aucun.top2.Execute()
réalise la fonctionnalité proprement dite de la commande.- le
context.Context
permet de passer des variables d’invocation globales, qui seront héritées par les éventuelles sous-commandes, et de transmettre des timeouts comme dans le traitement de requêtes réseau. - le
flag.FlagSet
contient les drapeaux locaux et arguments de la commande.- À ce stade, le nom du programme et les drapeaux globaux comme les drapeaux
locaux de la commande ont été analysés, et
fs.Args()
contient les arguments de la commande. - En invoquant
demo top2 hello world
, le résultat defs.Args()
sera[]string{"hello", "world"}
.
- À ce stade, le nom du programme et les drapeaux globaux comme les drapeaux
locaux de la commande ont été analysés, et
- le troisième paramètre n’est pas utilisé à ce stade.
- le
Il ne reste plus qu’à déclarer les 3 commandes (top1
, top2
, et l’alias 1
)
à subcommands
, en les ajoutant à la liste des commandes enregistrées aux côtés de
commands
, flags
et help
, dans root.go
:
|
|
À ce stade, notre programme reconnaît ces deux commandes spécifiques, ainsi
que l’alias "1"
pour la commande top1
Beaucoup d’applications n’auront pas besoin de plus.
$ go run . commands
commands
flags
help
top1
top2
1
$ go run . top2 hello world
In top2 [hello world]
$
1.3 Transmettre des arguments non-CLI
Le code de ce niveau est disponible sur level1.3-non_cli_arguments.
Dans l’exemple précédent, la commande top2
recevait des arguments passés par
l’utilisateur du programme depuis la ligne de commande.
Dans certains cas, toutefois, le code peut avoir besoin de transmettre des arguments communs à toutes les commandes, calculés durant la mise en place des commandes et indépendants des arguments de ligne de commande.
C’est là le rôle du paramètre variadique de subcommands.Execute
,
qui se traduit sous la forme de l’argument variadique reçu par les implémentations
de la méthode Command.Execute()
. Ici, nous allons l’illustrer en passant deux
valeurs
|
|
Celles-ci seront traitées dans top1
:
|
|
$ go run . top1
In top1.
Non-CLI args: []interface {}{"meaning", 42}
$
Le point important ici est la nécessité de coordonner les types et l’ordre des
valeurs passées ainsi, puisqu’elles sont transmises sous forme de type interface{}
(any
à partir de Go 1.18),
ce qui nécessitera en général dans la méthode Execute
une assertion de type
pour obtenir la valeur typée prête à l’emploi.
1.4 Grouper les commandes
Le code de ce niveau est disponible sur level1.4-command_groups.
Dans un projet de complexité croissante,
il peut être utile de regrouper les commandes en groupes ayant un thème commun.
C’est le rôle du second paramètre string
de la fonction subcommands.Register
,
que nous utilisons dans root.go
: il désigne pour chaque commande le
subcommands.Group auquel la commande appartient.
Dans cet exemple, nous séparons les commandes fournies par google/subcommands,
dans le groupe help
, de nos commandes spécifiques, dans le groupe top
.
|
|
La commande help
affiche maintenant nos commandes dans deux groupes séparés :
$ go run . help
Usage: subcommands_demo <flags> <subcommand> <subcommand args>
Subcommands for help:
commands list all command names
flags describe all known top-level flags
help describe subcommands and their syntax
Subcommands for top:
top1, 1 top1 is an example top-level custom command without arguments
top2 top2 is an example top-level custom command with arguments
$
1.5 Ajouter des drapeaux globaux ou locaux
Le code de ce niveau est disponible sur level1.5-flags.
Pour beaucoup de programmes, il est important de pouvoir définir des drapeaux
globaux, comme -v
qui augmente souvent la quantité d’informations affichée
par les commandes ; ou des drapeaux locaux qui permettent de préciser le
comportement d’un programme.
Ajoutons tout d’abord un drapeau global booléen nommé -v
à nos deux commandes.
Cela se passe en fait avec le paquet standard, flag
couplé
— de façon invisible de l’extérieur — avec google/subcommands.
Pour cela, en application des bonnes pratiques Go, nous définissons un
type spécifique non exporté pour la clef de contexte, et une variable de ce type,
dans notre fichier root.go
:
|
|
Ensuite, dans notre fonction Execute
, nous définissons un drapeau avec flag
,
et nous l’ajoutons au contexte :
|
|
Il ne reste plus aux commandes qu’à obtenir la valeur depuis le contexte, comme
ici dans top1.go
:
|
|
Comme nous avons défini le type du drapeau comme booléen et que la clef, grâce à son type unique, ne peut pas rencontrer de collision même dans un plus grand programme, l’assertion de type réussira obligatoirement.
Pour ce drapeau global, nous n’avons donc pas utilisé google/subcommands du tout,
et il est bien reconnu par la commande flags
intégrée, mais pas listé dans
l’aide intégrée fournie par la commande help
:
$ go run . help
Usage: subcommands_demo <flags> <subcommand> <subcommand args>
Subcommands for help:
commands list all command names
flags describe all known top-level flags
help describe subcommands and their syntax
Subcommands for top:
top1, 1 top1 is an example top-level custom command without arguments
top2 top2 is an example top-level custom command with arguments
Use "subcommands_demo flags" for a list of top-level flags
$ go run . flags
-v Be more verbose
$
En revanche, définissons maintenant un drapeau local sur la commande top1
.
Comme il est propre à la commande, et se place plus loin dans la liste des arguments
du programme, il n’est pas possible d’utiliser directement le paquet flag
sur
les arguments de ligne de commande comme pour les drapeaux globaux.
Les drapeaux locaux de chacune des commandes sont définis par les commandes elles-mêmes,
en implémentant leur méthode SetFlags
pour positionner une valeur dans les
données d’instance de la commande.
Pour cela, dans le fichier top1.go
, nous allons ajouter un champ au type implémentant
notre commande, pour pouvoir stocker cette valeur qui sera un champ string
.
|
|
Ensuite, nous implémentons la méthode top1.SetFlags
pour définir ce drapeau
de façon à modifier ce champ sur l’instance de commande :
|
|
La commande help
dispose dès lors de notre drapeau local:
$ go run . help top1
top1 -prefix string
Add a prefix to the result
$
Lorsque subcommands.Execute
invoque notre instance de commande top1
,
il commence par créer un flag.FlagSet
spécifique, qu’il passe à la méthode
SetFlags
de l’instance de commande, ce qui lui permet de positionner la valeur
des propriétés définies sur l’instance de commande, valeurs que nous pouvons ensuite
récupérer dans notre méthode top1.Execute
, en accédant directement à la
valeur du champ sur l’instance de commande comme à la ligne 40.
$ go run . top1 -prefix today
today: hello
$
Il aurait aussi été possible d’extraire directement la valeur depuis le paramètre
fs *FlagSet
de la méthode top1.Execute
, en ne définissant par d’emplacement
de stockage pour le drapeau, mais le code correspondant serait beaucoup moins lisible.
C’est pourquoi il est préférable d’utiliser la méthode précédente.
func (cmd *top1) SetFlags(fs *flag.FlagSet) {
_ = fs.String("prefix", "", "Add a prefix to the result")
}
func (cmd *top1) Execute(ctx context.Context, fs *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
prefix := fs.Lookup("prefix").Value.(fmt.Stringer).String()
fmt.Println(strings.Join(append([]string{cmd.prefix}, "hello"), ": "))
return subcommands.ExitSuccess
}
1.6 Définir des drapeaux comme importants
Le code de ce niveau est disponible sur level1.6-important_flags.
Certains drapeaux globaux peuvent être d’usage fréquent, et il est possible
de les faire afficher par help
.
Imaginons que notre drapeau global -v
soit important, et que nous ayons aussi
un drapeau -debug
non important. Nous pouvons le signaler à google/subcommands
lors de la déclaration des commandes dans notre fichier root.go
:
|
|
Sans la ligne subcommands.ImportantFlag("v")
, la commande help
ne liste aucun drapeau, comme nous l’avons vu au niveau précédent.
Mais avec cette ligne, le drapeau important -v
est listé tandis que le drapeau
ordinaire -debug
demeure ignoré.
$ go run . help
Usage: subcommands_demo <flags> <subcommand> <subcommand args>
Subcommands for help:
commands list all command names
flags describe all known top-level flags
help describe subcommands and their syntax
Subcommands for top:
top1, 1 top1 is an example top-level custom command without arguments
top2 top2 is an example top-level custom command with arguments
Top-level flags (use "subcommands_demo flags" for a full list):
-v=false: Be more verbose
$
Les deux drapeaux demeurent visibles dans la commande flags
dédiée à leur affichage:
$ go run . flags
-debug
Show debug information
-v Be more verbose
$
À ce stade, tous les besoins de la majorité des petites applications sont couverts. Les niveaux suivants concernent les projets plus complexes, ou avec des exigences techniques plus fortes comme la présence de tests unitaires jusque sur les commandes, et non seulement sur le code fonctionnel invoqué par celles-ci, ce qui est la situation la plus courante.
Niveau 2 : Réutiliser du code de commande
Le code de ce niveau est disponible sur level2.1-reuse.
Jusqu’à présent, nous avons créé un type défini pour chaque commande, hormis les alias, comme le suggère l’exemple fourni par la documentation — minimale — de google/subcommands. Cela permet de stocker les propriétés spécifiques telles que les drapeaux locaux sur le type défini pour chaque commande.
En pratique, toutefois, nombre de commandes n’ont pas de drapeaux, ou partagent les mêmes drapeaux, et cette répétition de code n’est pas entièrement satisfaisante. Mais rien n’oblige à avoir des types spécifiques pour des commandes distinctes, surtout si elles partagent la même structure.
La principale raison d’avoir des types distincts est la méthode Command.Execute
,
qui diffère d’une commande à l’autre puisque les commandes exécutent des tâches différentes.
Il suffit donc de rendre cette méthode variable par instance.
Diverses approches sont possibles. Par exemple, en utilisant un type unique,
disons top
, il est possible d’avoir une méthode Execute
unique qui va
examiner une propriété sur l’instance de commande pour sélectionner une fonction
exécutrice concrète, comme dans cet exemple:
|
|
|
|
Un défaut de cette approche est le couplage entre
- le code de la fonction partagée
Execute
et les diverses commandes. - le code de la fabrique partagée
NewTop
, susceptible de nécessiter des ajustements pour les diverses commandes.
Le modèle recommandé sera plus direct, évitant ce couplage,
et conservant les fonctions fabriques pour chaque commande, au moyen des champs fonctions.
Ainsi, dans root.go
, nous retrouvons les fabriques dédiées :
|
|
Le fichier top.go
va contenir le type réutilisable top
, avec des champs pour stocker :
- les valeurs dont les méthodes
Name
,Synopsis
etUsage
seront les getters. - les drapeaux locaux communs aux deux commandes, ici
prefix
, déclaré parSetFlags
- et surtout, un champ fonction stockant l’executeur concret pour la méthode
Execute
.
|
|
La limitation des champs fonction en comparaison avec les méthodes est le fait
que les fonctions invoquées ainsi ne reçoivent pas l’instance comme premier paramètre caché,
à la différence des méthodes, de sorte qu’elles n’y ont pas naturellement accès.
L’instance de commande doit donc leur être passée comme paramètre additionnel explicite,
d’où le paramètre *top
dans la signature, ligne 15.
Pour imiter au plus près une expression de méthode standard, ce paramètre devrait être le premier des fonctions, mais cela entre en conflit avec l’usage voulant que le contexte soit toujours le premier paramètre d’une fonction, d’où le placement ici en deuxième position.
Les commandes utilisant le type partagé, comme top2
, définissent une fonction
exécutrice avec cette signature, pour disposer de l’accès à l’instance,
et leur fonction fabrique affecte cette fonction à l’instance lors de son initialisation,
comme ici dans top2.go
:
|
|
De la sorte le couplage disparaît.
Un nombre illimité de commandes peut réutiliser le type de commande partagé,
sans avoir à modifier la méthode top.Execute
.
$ go run . top1 -prefix today
today: hello
$ go run . top2 -prefix "the answer is" 42
the answer is: 42
$
Niveau 3: Les commandeurs
3.1 API procédurale et API objet
Le code de ce niveau est disponible sur level3.1-object_api.
Jusqu’à présent, nous avons utilisé les fonctions publiques de google/subcommands:
subcommands.Alias
etsubcommands.Register
pour enregistrer les commandessubcommands.ImportantFlag
pour marquer les drapeaux globauxsubcommands.CommandsCommand
,subcommands.FlagsCommand
etsubcommands.HelpCommand
pour le mécanisme intégré d’aide et documentation
Cette API procédurale n’est en fait qu’une façade simplifiant l’API objet
sous-jacente de google/subcommands : hormis Alias
, qui ne dépend pas d’un Commander
,
chacune de ces fonctions est une simple invocation de la méthode homonyme du type
subcommands.Commander
sur l’instance par défaut de ce type,
subcommands.DefaultCommander
, créée lors de l’import du paquet dans subcommands.go
par
la pseudo-fonction init
,
comme l’illustre de fragment du fichier subcommands.go
:
|
|
C’est là l’application d’un modèle fréquent en Go, utilisé par exemple par les
paquets flag
(flag.CommandLine
) ou http
(http.DefaultClient
) pour les plus connus.
Nous pouvons donc basculer sur ce modèle en modifiant seulement le fichier root.go
,
dans lequel nous remplaçons chaque appel procédural (sauf Alias
) par une invocation de méthode
sur l’instance de Commander
par défaut:
|
|
Le programme fonctionne de façon strictement identique à la version précédente.
3.2 Utiliser des commandeurs personnalisés
Le code de ce niveau est disponible sur level3.2-custom_commander.
Pour créer un Commander
personnalisé plutôt que d’utiliser l’instance globale
mutable fournie par google/subcommands, dont nous ne maîtrisons pas l’initialisation
ni les mutations, il est préférable de créer une instance en propre dans la
fonction Execute
de notre root.go
:
|
|
Rien d’autre ne change par rapport à la version précédente, mais nous avons
fait un pas de plus vers une structure de code testable, en nous affranchissant
de la dépendance à la variable globale subcommands.DefaultCommander
Il est temps de sauter le pas et de rendre Execute
totalement testable en
retirant les autres dépendances à des variables globales.
3.3 Créer une structure de commande testable
Le code de ce niveau est disponible sur level3.3.
Les obstacles à la testabilité dans notre code sont encore multiples :
Problème | Solution |
---|---|
sorties standard et d’erreur globales | injecter depuis main |
os.Args globale | injecter depuis main |
flag.CommandLine globale | remplacer par un flag.FlagSet spécifique |
flag.Bool , flag.Parse globales | remplacer par les méthode homonymes du flag.FlagSet |
utilisation masquée de log.std | remplacer par un log.Logger spécifique |
invocation d’os.Exit par flag | modifier les options de création du flag.FlagSet |
commandes instanciées dans Execute | injecter depuis une fabrique |
Notre nouvelle version de main.go
devient donc le seul emplacement utilisant
des variables globales:
|
|
Notre fichier root.go
va dorénavant comporter la fabrique de commandes Describe
:
|
|
…et une nouvelle version d’Execute
recevant toutes ses données par injection au
lieu d’aller référencer des variables importées ou de les créer elle-même.
|
|
- 40-41:
outW
eterrW
sont les deux sorties reçues depuismain()
. - 41,47:
errW
est utilisée pour créer unlog.Logger
- 44:
describe
est la fabrique de descriptions de commande, reçue depuismain()
- 55: un
FlagSet
est créé à partir des paramètres pour retirer la dépendance àos.Args
- 58: un
Commander
est créé à partir duFlagSet
et des arguments pour retirer la dépendance àflag.CommandLine
, et indirectement àos.Args
- 66:
outW
et le logger sont passées àdescribe
pour qu’il les injecte dans les fabriques de commandes, afin que les commandes reçoivent elles aussi des sorties injectées au lieu d’utiliser des variables globales. - 72, 73: les drapeaux globaux sont définis sur le
FlagSet
créé au lieu deflag.CommandLine
- 77: l’analyse des drapeaux est menée sur le
FlagSet
créé et non surflag.CommandLine
Execute
n’utilise plus aucune variable globale, et transmet les dépendances
injectées aux commandes top1
et top2
.
Ces dernières sont donc modifiées en conséquence pour utiliser ces éléments injectés.
Ainsi, dans top1.go
:
|
|
Le code de top2
est similaire, et le type top
dans top.go
a été étendu
comme montré aux lignes 34-40 ci-dessus pour stocker les dépendances injectées.
Ceci permet d’obtenir une couverture de test de 100%
très simplement, sans nécessiter d’outil de mocking.
$ go test -race -count=1 -cover ./cmd
ok github.com/fgm/subcommands_demo/cmd 0.035s coverage: 100.0% of statements
$
Niveau 4: Ajouter des sous-commandes
Le code de ce niveau est disponible sur level4.1-nesting.
Les applications les plus complexes présentent généralement une hiérarchie de commandes et sous-commandes, mais la fonctionnalité de base de google/subcommands ne décrit pas leur mise en œuvre.
Elle est en fait la conséquence de l’utilisation des paramètres injectés à
subcommands.NewCommander
: puisque les drapeaux en sont définis par un FlagSet
quelconque et par les arguments passés à cette fonction, il suffit,
dans l’implémentation d’une commande devant accepter des sous-commandes,
de créer un Commander
en lui passant les arguments qui n’ont pas encore été consommés.
C’est ce que réalise notre nouvelle commande top3
, avec ses sous-commandes
sub31
et sub32
, dans top3.go
. Sur le fond, elle est similaire à top1
et
top2
, mais c’est dans sa fonction exécutrice top31Execute
que va se trouver
la logique de sous-commandes:
|
|
- dans un premier temps (29-31), la commande vérifie si elle a reçu des arguments, qui pourraient être des sous-commandes. Si ce n’est pas le cas, elle transfère l’exécution à son exécution en propre, qui réalise le comportement souhaité en cas d’absence de sous-commande.
- dans le cas contraire, elle reprend la structure de notre fonction racine
Execute
:- créer un
FlagSet
, - créer un
Commander
qui l’utilise ainsi que les arguments disponibles, - enregistrer les sous-commandes dessus, dont les trois commandes intégrées,
- analyser les arguments disponibles pour rechercher les drapeaux locaux des sous-commandes
- exécuter la sous-commande désignée par les arguments.
- créer un
$ go run . top3
hello top3
$ go run . top3 commands
commands
flags
help
sub31
sub32
$ go run . top3 help sub31
sub31 -prefix string
Add a prefix to the result
$ go run . top3 sub31 -prefix today
today: hello sub31
$
Niveau 5: Au-delà de NewCommander
5.1 Maîtriser les sorties
Le code de ce niveau est disponible sur level5.1-newcommander.
À ce stade, nous avons bien injecté tout ce qui semblait être injectable, mais
certaines sorties demeurent non maîtrisées, émises par subcommands
ou flag
dans les situations d’erreur, et visibles en particulier durant les tests,
ce qui était prévisible puisque NewCommander
ne permet pas d’injecter les sorties
standard ou d’erreur dans l’instance qu’elle renvoie.
$ go test -race -count=1 -v ./cmd 2>&1 | grep -vE '(CONT|RUN|PASS|PAUSE)'
flag provided but not defined: -bad
Usage: Test_Execute <flags> <subcommand> <subcommand args>
Subcommands for help:
...snip (33 lines total)...
flag provided but not defined: -bad
top1 -prefix string
Add a prefix to the result
flag provided but not defined: -bad
c -prefix string
Add a prefix to the result
ok github.com/fgm/subcommands_demo/cmd 0.036s
$
Dans les faits, tant les FlagSet
que les Commander
acceptent de rediriger
leur sortie d’erreur, mais cela doit être appliqué après la création, ici dans root.go
pour les commandes de premier niveau et les drapeaux globaux:
|
|
La redirection de ces propriétés additionnelles permet d’éliminer le plus gros des messages parasites:
$ go test -race -count=1 -v ./cmd 2>&1 | grep -vE '(CONT|RUN|PASS|PAUSE)'
flag provided but not defined: -bad
flag provided but not defined: -bad
ok github.com/fgm/subcommands_demo/cmd 0.045s
$
Les deux messages résiduels ne sont pas capturables avec la version 1.2.0 de google/subcommands, ce qui est a priori un bogue du paquet pour lequel une requête de fusion existe.
Nous sommes à ce stade au bout des fonctionnalités utiles du paquet. Restent deux fonctionnalités à l’utilisation improbable.
5.2 Introspection des commandeurs
Le code de ce niveau est disponible sur level5.2-visit.
Le type subcommands.Commander
dispose de méthodes publiques destinées à
permettre aux utilisateurs du paquet d’examiner la structure interne du jeu de
commandes et de drapeaux.
Nous allons donc créer une commande de premier niveau visit
, similaire à
toutes celles que nous avons vues jusqu’à présent, et qui démontre ces méthodes:
tout se trouve dans le fichier visit.go
- La méthode
VisitAll
est un Visiteur, qui permet d’examiner l’état des drapeaux duCommander
en lui passant une fonction invoquée pour chaque drapeau:
|
|
Running "demo visit":
VisitAll show all the commander flags:
|Name |Default |Value |Usage |
|debug |false |false |Show debug information |
|v |false |true |Be more verbose |
- La méthode
VisitAllImportant
est similaire, mais limitée aux drapeaux marqués comme importants
VisitAllImportant only shows the "important" flags:
|Name |Default |Value |Usage |
|v |false |true |Be more verbose |
- La méthode
VisitGroups
est un Visiteur, qui permet d’examiner les propriétés desCommandGroup
créés à partir des noms de groupes déclarés lors de l’inscription des commandes. Son intérêt est limité, les méthodes publiques du typeCommandGroup
ne permettant pas l’introspection des commandes présentes dans ces groupes.
|
|
VisitGroups only visits the command groups, not the commands:
|Name |Len |
|help |3 |
|top |5 |
- La méthode
VisitCommands
est un Visiteur qui remédie aux limitations deVisitGroups
, en parcourant les commandes enregistrées sur leCommander
, y compris leurCommandGroup
, en fournissant l’accès à la commande elle-même, ce qui permet d’en observer les drapeaux:
|
|
VisitCommands visits the commands themselves:
|Group |Name |Synopsis |Flags |
|help |commands |list all command names | |
|help |flags |describe all known top-level flags | |
|help |help |describe subcommands and their syntax | |
|top |top1 |top1 is an exemple top-level custom command without arguments |prefix |
|top |top2 |top2 is an exemple top-level custom command with arguments |prefix |
|top |1 |top1 is an exemple top-level custom command without arguments |prefix |
|top |top3 |top3 is an exemple top-level custom command with nested subcommands |prefix |
|top |visit |demoes commander Visit* functions | |
(command usage omitted for readability)
Pour disposer de l’accès à l’instance de Commander
active, la commande visit
a besoin de se voir injecter le commandeur, puisque les commandes n’ont par
défaut pas connaissance du Commander
qui les invoque. Ceci est réalisé dans root.go
:
|
|
|
|
Notre commande visit
est d’un type visitCmd
avec un champ de type subcommands.Commander
et un setter visitCmd.SetCommander
qui en fait une implémentation de CommanderAware
,
ce qui permet à Execute
de lui injecter le Commander
actif.
5.3 Remplacer les briques de base
Le code de ce niveau est disponible sur level5.3-explain.
Enfin, dernier niveau esotérique de google/subcommands, le remplacement des
commands intégrées CommandsCommand
, FlagsCommand
et HelpCommand
est
également possible, grâce aux champs fonctions du type subcommands.Commander
:
Explain
est un champ de typefunc(io.Writer)
, initialisé dansNewCommander
par l’expression de méthode privéecdr.writer
ExplainGroup
est un champ de typefunc(io.Writer, *CommandGroup)
, initialisé dansNewCommander
par la fonction privéeexplainGroup
ExplainCommand
est un champ de typefunc(w io.Writer, c subcommands.Command)
initialisé dansNewCommander
par la fonction privéeexplain
Notre nouvelle commande explain
, dans cmd/explain.go
, est CommanderAware
,
et procède à ce remplacement sur le Commander
reçu par injection :
Tous les exemples illustrent l’appel intégré, suivi de l’appel personnalisé:
ExplainCommand
: version intégrée, puis version API au format YAML:
Running "demo explain":
Demoes overriding ExplainCommand to describe top3.
- Builtin version using private explain:
top3 -prefix string
Add a prefix to the result
- Custom version in YAML format:
top3:
flags:
- name: prefix
default: ""
usage: Add a prefix to the result
synopsis: top3 is an exemple top-level custom command with nested subcommands
usage: top3
ExplainGroup
: version intégrée, puis version API au format YAML:
Demoes overriding ExplainGroup.
- Builtin version using private explainGroup:
Subcommands for help:
commands list all command names
flags describe all known top-level flags
help describe subcommands and their syntax
...snip...
- Custom version in YAML format, without access to group contents:
help: 3
top: 6
Explain
: version intégrée, puis version neutre invitant à l’écriture
Demoes overriding Explain.
- Builtin version using private commander.explain:
Usage: demo <flags> <subcommand> <subcommand args>
Top-level flags (use "demo flags" for a full list):
-v=false: Be more verbose
- Custom version, build from commander methods:
Use any commander.(Explain|Visit)* methods
Conclusions
- Les plus
google/subcommands
est très bien adapté aux projets simples- il est particulièrement léger et très stable, du fait de l’absence de dépendances
- il n’est pas bloquant lorsque les projets s’enrichissent et nécessitent une hiérarchie de commandes à plusieurs niveaux, ou un mélange de drapeaux locaux et globaux
- il n’empêche pas de créer des jeux de commande intégralement testables
- Les moins
- il n’intègre pas directement la fusion des drapeaux CLI avec les variables d’environnement
- il n’intègre par directement le chargement de fichiers de configuration et leurs fusion
- la création de commandes complexes avec des étapes de validation n’est pas supportée
- il n’intègre pas directement la notion de drapeaux persistants, c’est à dire définis à un niveau et également transmis aux sous-commandes inférieures
- la création de hiérarchies de commandes multi-niveaux est plus complexe qu’avec d’autres outils
C’est donc probablement votre meilleur outil pour le prochain utilitaire CLI ou le prochain micro-service, mais pas le meilleur pour un grand projet monolithique.