Parser une ligne de commande facilement avec le nouveau projet System.CommandLine de Microsoft
Il a toujours été simple mais fastidieux de parser les paramètres d'une application console car il existe pléthore de SDK et pas forcément de solutions officielles. Le repository github dotnet vient de s'enrichir d'un nouvel outil pour solutionner cela : System.CommandLine. Cet article a pour vocation de le présenter !
Pourquoi l'utiliser ? Qu'est ce que cela apporte ?
L'idée avancée est d'unifier au sein des Frameworks Microsoft la façon d'interpréter les arguments passés en ligne de commande et de vous enlever le travail fastidieux de lecture des paramètres lorsque vous créez une application en ligne de commande.
Différentes fonctionnalités sont apportées de base :
- Création d'une vue "d'aide" par défaut basée sur la documentation de votre code ou vos propres textes.
- Ajout automatique de l'option "-v" pour afficher la version de votre CLI.
- La possibilité d'afficher comment sont interprétés les paramètres passés.
- Intégration automatique au système de suggestion de complétion via tab.
- Un système d'affichage de contenu compatible avec les différentes sorties possibles (Powershell, bash, fichier, etc.)
Attention, nous sommes encore sur une version peu stable et très récente (0.2 seulement !).
Un peu de terminologie
Command : une commande représente une action possible de votre application. La commande racine correspond à l'application elle-même et elle peut posséder des sous-commandes. Par exemple, dans "dotnet restore", "dotnet" est votre commande racine et "restore" est votre sous-commande. Les commandes et sous-commandes sont matérialisées par la classe Command, exceptée la commande racine qui utilise RootCommand.
Option : une option représente un paramètre possible d'une commande. Je parle ici du nom du paramètre et non pas de sa valeur potentielle. Il est possible de choisir par configuration le pattern d'écriture à utiliser. Par exemple, on peut proposer POSIX (--mon-option 42) ou le format Windows (/mon-option 42). La valeur par défaut est pour le moment configurée à POSIX.
Argument : représente la valeur d'une option. Il est possible de définir des valeurs par défaut pour chacun des arguments d'une option.
Directives : un concept assez inhabituel (pour moi en tout cas), la possibilité de spécifier une directive entre crochets pour donner une sorte de contexte. L'exemple donné montre comment indiquer que l'on est en debug.
monapp [debug] --mon-option 42
Ajouter System.CommandLine à une application console
Il suffit de partir d'une application console de base (dotnet new console -o monApp) et d'ajouter le package nuget System.CommandLine.Experimental. Attention, il est encore en version pre-release !
Configuration de la commande racine.
La suite est assez fastidieuse et consiste à définir les différentes commandes et options possibles pour votre application.
On va donc commencer par définir la commande racine en instanciant une RootCommand et en spécifiant sa description. On prendra aussi le soin de définir la méthode de traitement associé au programme. Pour cela, on assigne une action à la propriété Handler de notre RootCommand. Notre méthode Main retourne ensuite le résultat de l'appel à la méthode InvokeAsync().
static async Task Main(string[] args) { Action handler = ()=>{}; var rootCommand = new RootCommand { Description = "Cette aplication fait le café !", Handler = CommandHandler.Create(handler) }; await rootCommand.InvokeAsync(args); }
Cela reste pour le moment assez basique car nous n'avons ni option ni argument mais en l'état cela permet déjà d'appeler l'aide et d'avoir cet affichage !
Ajouter des options et paramètres
Ajouter des options est assez simple : on crée une instance de la classe Option et on renseigne ses différentes propriétés, les plus importantes étant :
- Alias : l'alias principal de votre option, passé directement dans le constructeur.
- Description : la description qui sera affichée dans l'aide.
- Argument : une instance de la classe Argument qui décrit le type d'argument attendu.
On peut alors aussi appeler différentes méthodes de l'instance crée pour ajouter une valeur par défaut, spécifier la cardinalité (combien de valeurs sont attendues) de chaque argument, etc.
var options = new Option("--flavour-type") { Description = "Quel type de saveur ?", Name = "flavour-type", Argument = new Argument<FlavourType> { Arity = new ArgumentArity(0, 2), Name = "FlavourType", } }; options.AddAlias("--ft"); options.Argument.SetDefaultValue(FlavourType.Souveraine); rootCommand.AddOption(options); options = new Option("--length") { Description = "Combien vous en prendrez (en ml) ?", Name = "length", Argument = new Argument<int>() }; options.AddAlias("--l"); rootCommand.AddOption(options);
Utiliser réellement les valeurs !
Il est maintenant temps de redéfinir la propriété Handler précédemment définie comme une action bête et méchante pour utiliser maintenant en paramètre les valeurs passées par l'utilisateur. Attention, le nom des paramètres est très important car c'est ainsi que le parser saura quel paramètre associer à quel argument de votre méthode "handler".
public static void Handler(FlavourType flavourType, int length) { Console.WriteLine($"Flavour : {flavourType:G}"); Console.WriteLine($"Length : {length}"); }
Comment écrire moins de code (mais avec moins de fonctionnalités)
Il y a une manière d'aller encore plus vite en référençant le package nuget System.CommandLine.DragonFruit. Une fois celui-ci ajouté, il n'y aura plus qu'une seule chose à faire : définir directement sur la méthode Main les paramètres attendus. Le code que l'on a précédemment écrit se résumera ainsi à :
/// <summary> /// Cette application fait le café ! /// </summary> /// <param name="flavourType">Quel type de saveur ?</param> /// <param name="length">Combien vous en prendrez (en ml) ?</param> /// <returns></returns> static async Task Main(int length, FlavourType flavourType = FlavourType.Heureuse) { Console.WriteLine($"Flavour : {flavourType:G}"); Console.WriteLine($"Length : {length}"); }
Les défauts de cette méthode sont à connaître :
- Impossible de définir un alias pour un paramètre,
- Impossible d'utiliser des sous-commandes.
Cela reste quand même très pratique !
Pour aller plus loin
Quelques liens utiles :
- L'historique du projet.
- La liste complète des features visées.
- Une autre façon de faire est démontrée dans un précédent article.
Happy coding :)
Commentaires