Partager via


System.CommandLine Guide de migration 2.0.0-beta5+

L’objectif principal de la version 2.0.0-beta5 était d’améliorer les API et de prendre un pas vers la publication d’une version stable de System.CommandLine. Les API ont été simplifiées et rendues plus cohérentes et cohérentes avec les directives de conception du framework. Cet article décrit les changements cassants qui ont été apportés dans la version 2.0.0-beta5 et 2.0.0-beta7, et le raisonnement derrière eux.

Changement de nom

Dans la version 2.0.0-beta4, tous les types et membres n’ont pas suivi les instructions d’affectation de noms. Certains n’étaient pas cohérents avec les conventions d’affectation de noms, telles que l’utilisation du Is préfixe pour les propriétés booléennes. Dans la version 2.0.0-beta5, certains types et membres ont été renommés. Le tableau suivant présente les anciens noms et les nouveaux noms :

Ancien nom Nouveau nom
System.CommandLine.Parsing.Parser CommandLineParser
System.CommandLine.Parsing.OptionResult.IsImplicit Implicit
System.CommandLine.Option.IsRequired Required
System.CommandLine.Symbol.IsHidden Hidden
System.CommandLine.Option.ArgumentHelpName HelpName
System.CommandLine.Parsing.OptionResult.Token IdentifierToken
System.CommandLine.Parsing.ParseResult.FindResultFor GetResult
System.CommandLine.Parsing.SymbolResult.ErrorMessage AddError(String)

† Pour autoriser plusieurs erreurs pour le même symbole à signaler, la ErrorMessage propriété a été convertie en méthode et renommée en AddError.

Collections mutables d’options et de validateurs

La version 2.0.0-beta4 a de nombreuses Add méthodes utilisées pour ajouter des éléments à des collections, telles que des arguments, des options, des sous-commandes, des validateurs et des achèvements. Certaines de ces collections étaient exposées via des propriétés en tant que collections en lecture seule. En raison de cela, il était impossible de supprimer des éléments de ces collections.

Dans la version 2.0.0-beta5, les API ont été modifiées pour exposer des collections mutables au lieu de Add méthodes et (parfois) de collections en lecture seule. Cela vous permet non seulement d’ajouter des éléments ou de les énumérer, mais également de les supprimer. Le tableau suivant présente l’ancienne méthode et les nouveaux noms de propriétés :

Nom de l’ancienne méthode Nouvelle propriété
Command.AddArgument Command.Arguments.Add
Command.AddOption Command.Options.Add
Command.AddCommand Command.Subcommands.Add
Command.AddValidator Command.Validators.Add
Option.AddValidator Option.Validators.Add
Argument.AddValidator Argument.Validators.Add
Command.AddCompletions Command.CompletionSources.Add
Option.AddCompletions Option.CompletionSources.Add
Argument.AddCompletions Argument.CompletionSources.Add
Command.AddAlias Command.Aliases.Add
Option.AddAlias Option.Aliases.Add

Les méthodes RemoveAlias et HasAlias ont également été supprimées, car la propriété Aliases est désormais une collection mutable. Vous pouvez utiliser la Remove méthode pour supprimer un alias de la collection. Utilisez la Contains méthode pour vérifier si un alias existe.

Noms et alias

Avant la version 2.0.0-beta5, il n’y avait aucune séparation claire entre le nom et les alias d’un symbole. Lorsqu’il name n’a pas été fourni pour le Option<T> constructeur, le symbole a signalé son nom en tant que l'alias le plus long après suppression de préfixes comme --, -, ou /. C’était déroutant.

En outre, pour obtenir la valeur analysée, vous devez stocker une référence à une option ou à un argument, puis l’utiliser pour obtenir la valeur à partir de ParseResult.

Pour promouvoir la simplicité et l’explicite, le nom d’un symbole est désormais un paramètre obligatoire pour chaque constructeur de symboles (y compris Argument<T>). Le concept d’un nom et d’alias est désormais distinct : les alias sont simplement des alias et n’incluent pas le nom du symbole. Bien sûr, ils sont facultatifs. Par conséquent, les modifications suivantes ont été apportées :

  • name est maintenant un argument obligatoire pour chaque constructeur public de Argument<T>, Option<T>et Command. Dans le cas de Argument<T>, il n’est pas utilisé pour l’analyse, mais pour générer l’aide. Dans le cas et Option<T>Command, il est utilisé pour identifier le symbole pendant l’analyse et également pour obtenir de l’aide et des achèvements.
  • La Symbol.Name propriété n’est plus virtual; elle est désormais en lecture seule et renvoie le nom tel qu’il a été fourni lors de la création du symbole. En raison de cela, Symbol.DefaultName a été supprimé et Symbol.Name ne supprime plus le --, -ou / tout autre préfixe de l’alias le plus long.
  • La propriété Aliases exposée par Option et Command est désormais une collection mutable. Cette collection n’inclut plus le nom du symbole.
  • System.CommandLine.Parsing.IdentifierSymbol a été supprimé (il s’agissait d’un type de base pour les deux Command et Option).

Le fait d’avoir le nom toujours présent vous permet d’obtenir la valeur analysée par nom :

RootCommand command = new("The description.")
{
    new Option<int>("--number")
};

ParseResult parseResult = command.Parse(args);
int number = parseResult.GetValue<int>("--number");

Création d’options avec des alias

Auparavant, Option<T> exposait de nombreux constructeurs, dont certains acceptaient le nom. Étant donné que le nom est désormais obligatoire et que les alias sont fréquemment fournis, Option<T>il n’y a qu’un seul constructeur. Il accepte le nom et un ensemble params d’alias.

Avant 2.0.0-beta5, Option<T> avait un constructeur acceptant un nom et une description. En raison de cela, le deuxième argument peut maintenant être traité comme un alias plutôt qu’une description. Il s’agit du seul changement cassant connu dans l’API qui n’entraîne pas d’erreur du compilateur.

Mettez à jour tout code qui a passé une description au constructeur pour utiliser le nouveau constructeur qui prend un nom et des alias, puis définissez la Description propriété séparément. Par exemple:

Option<bool> beta4 = new("--help", "An option with aliases.");
beta4b.Aliases.Add("-h");
beta4b.Aliases.Add("/h");

Option<bool> beta5 = new("--help", "-h", "/h")
{
    Description = "An option with aliases."
};

Valeurs par défaut et analyse personnalisée

Dans la version 2.0.0-beta4, vous pouvez définir des valeurs par défaut pour les options et les arguments à l’aide des SetDefaultValue méthodes. Ces méthodes ont accepté une object valeur, qui n’était pas de type sécurisé et pouvait entraîner des erreurs d’exécution si la valeur n’était pas compatible avec l’option ou le type d’argument :

Option<int> option = new("--number");
// This is not type safe, as the value is a string, not an int:
option.SetDefaultValue("text");

De plus, certains des Option constructeurs ont Argument accepté un délégué d’analyse (parse) et un booléen (isDefault) indiquant si le délégué était un analyseur personnalisé ou un fournisseur de valeurs par défaut, ce qui était confus.

Option<T> et Argument<T> les classes ont maintenant une DefaultValueFactory propriété que vous pouvez utiliser pour définir un délégué qui peut être appelé pour obtenir la valeur par défaut de l’option ou de l’argument. Ce délégué est appelé lorsque l’option ou l’argument n’est pas trouvé dans l’entrée de ligne de commande analysée.

Option<int> number = new("--number")
{
    DefaultValueFactory = _ => 42
};

Argument<T> et Option<T> sont également fournis avec une CustomParser propriété que vous pouvez utiliser pour définir un analyseur personnalisé pour le symbole :

Argument<Uri> uri = new("arg")
{
    CustomParser = result =>
    {
        if (!Uri.TryCreate(result.Tokens.Single().Value, UriKind.RelativeOrAbsolute, out var uriValue))
        {
            result.AddError("Invalid URI format.");
            return null;
        }

        return uriValue;
    }
};

En outre, CustomParser accepte un délégué de type Func<ParseResult,T>, plutôt que le délégué précédent ParseArgument . Ce délégué et quelques autres délégués personnalisés ont été supprimés afin de simplifier l'API et de réduire le nombre de types exposés par l'API, ce qui réduit le temps de démarrage pendant la compilation JIT.

Pour plus d'exemples sur la façon d'utiliser DefaultValueFactory et CustomParser, consultez Comment personnaliser l’analyse et la validation dans System.CommandLine.

Séparation de l'analyse syntaxique et de l'appel

Dans la version 2.0.0-beta4, il était possible de séparer l’analyse et l’appel de commandes, mais il n’était pas clair comment le faire. Command n’a pas exposé de Parse méthode, mais CommandExtensions fourni Parse, Invokeet InvokeAsync les méthodes d’extension pour Command. C’était déroutant, car il n’était pas clair quelle méthode utiliser et quand. Les modifications suivantes ont été apportées pour simplifier l’API :

  • Command expose maintenant une Parse méthode qui retourne un ParseResult objet. Cette méthode est utilisée pour analyser l’entrée de ligne de commande et retourner le résultat de l’opération d’analyse. En outre, il indique clairement que la commande n’est pas appelée, mais analysée, et uniquement de manière synchrone.
  • ParseResult expose désormais à la fois Invoke et InvokeAsync que vous pouvez utiliser pour invoquer la commande. Ce modèle indique clairement que la commande est appelée après l’analyse et permet d’appeler de manière synchrone et asynchrone.
  • La CommandExtensions classe a été supprimée, car elle n’est plus nécessaire.

Paramétrage

Avant 2.0.0-beta5, il était possible de personnaliser l’analyse, mais seulement avec certaines méthodes publiques Parse . Il y avait une Parser classe qui expose deux constructeurs publics : l’un acceptant un Command et l’autre acceptant un CommandLineConfiguration. CommandLineConfiguration était immuable et pour le créer, vous deviez utiliser un modèle de générateur exposé par la CommandLineBuilder classe. Les modifications suivantes ont été apportées pour simplifier l’API :

  • CommandLineConfiguration a été divisé en deux classes mutables (dans 2.0.0-beta7) : ParserConfiguration et InvocationConfiguration. La création d’une configuration d’appel est désormais aussi simple que la création d’une instance de InvocationConfiguration et la définition des propriétés que vous souhaitez personnaliser.
  • Chaque Parse méthode accepte désormais un paramètre facultatif ParserConfiguration que vous pouvez utiliser pour personnaliser l’analyse. Lorsqu’elle n’est pas fournie, la configuration par défaut est utilisée.
  • Pour éviter les conflits de noms, Parser a été renommé en CommandLineParser pour lever l’ambiguïté par rapport aux autres types d'analyseurs. Étant donné qu’il est sans état, il s’agit maintenant d’une classe statique avec uniquement des méthodes statiques. Il expose deux Parse méthodes d’analyse : l’une acceptant une IReadOnlyList<string> args et une autre acceptant un string args. Ce dernier utilise CommandLineParser.SplitCommandLine(String) (également public) pour fractionner l’entrée de ligne de commande en jetons avant de l’analyser.

CommandLineBuilderExtensions a également été supprimé. Voici comment mapper ses méthodes aux nouvelles API :

  • CancelOnProcessTermination est maintenant une propriété de InvocationConfiguration, appelée ProcessTerminationTimeout. Elle est activée par défaut, avec un délai d’expiration de 2 secondes. Pour le désactiver, définissez-le sur null. Pour plus d’informations, consultez Délai d’expiration du délai d’arrêt du processus.

  • EnableDirectives, UseEnvironmentVariableDirective, UseParseDirectiveet UseSuggestDirective ont été supprimés. Un nouveau type de directive a été introduit et RootCommand expose désormais une Directives propriété. Vous pouvez ajouter, supprimer et itérer des directives à l’aide de cette collection. SuggestDirective est incluse par défaut, vous pouvez également utiliser d’autres directives telles que DiagramDirective ou EnvironmentVariablesDirective.

  • EnableLegacyDoubleDashBehavior a été supprimé. Tous les jetons sans correspondance sont désormais exposés par la ParseResult.UnmatchedTokens propriété. Pour plus d’informations, consultez Jetons sans correspondance.

  • EnablePosixBundling a été supprimé. Le regroupement est désormais activé par défaut, vous pouvez le désactiver en définissant la propriété ParserConfiguration.EnablePosixBundling à false. Pour plus d’informations, consultez EnablePosixBundling.

  • RegisterWithDotnetSuggest a été supprimé lors de l’exécution d’une opération coûteuse, généralement pendant le démarrage de l’application. Vous devez maintenant inscrire des commandes avec dotnet suggestmanuellement.

  • UseExceptionHandler a été supprimé. Le gestionnaire d’exceptions par défaut est désormais activé par défaut ; vous pouvez le désactiver en définissant la InvocationConfiguration.EnableDefaultExceptionHandler propriété sur false. Cela est utile lorsque vous souhaitez gérer les exceptions d'une manière personnalisée, en enveloppant simplement les méthodes Invoke ou InvokeAsync dans un bloc try-catch. Pour plus d’informations, consultez EnableDefaultExceptionHandler.

  • UseHelp et UseVersion ont été supprimés. L’aide et la version sont désormais exposées par les types publics HelpOption et VersionOption. Ils sont tous deux inclus par défaut dans les options définies par RootCommand. Pour plus d’informations, consultez Personnaliser la sortie de l’aide et Option de version.

  • UseHelpBuilder a été supprimé. Pour plus d’informations sur la personnalisation de la sortie d’aide, consultez Comment personnaliser l’aide dans System.CommandLine.

  • AddMiddleware a été supprimé. Il ralentit le démarrage de l’application et les fonctionnalités peuvent être exprimées sans elle.

  • UseParseErrorReporting et UseTypoCorrections ont été supprimés. Les erreurs d’analyse sont désormais signalées par défaut lors de l’appel ParseResult. Vous pouvez le configurer à l’aide de l’action ParseErrorAction exposée par la ParseResult.Action propriété.

    ParseResult result = rootCommand.Parse("myArgs", config);
    if (result.Action is ParseErrorAction parseError)
    {
        parseError.ShowTypoCorrections = true;
        parseError.ShowHelp = false;
    }
    
  • UseLocalizationResources et LocalizationResources ont été supprimés. Cette fonctionnalité a été utilisée principalement par l’interface dotnet CLI pour ajouter des traductions manquantes à System.CommandLine. Toutes ces traductions ont été déplacées vers le System.CommandLine lui-même, de sorte que cette fonctionnalité n’est plus nécessaire. Si la prise en charge de votre langue est manquante, signalez un problème.

  • UseTokenReplacer a été supprimé. Les fichiers de réponse sont activés par défaut, mais vous pouvez les désactiver en définissant la propriété ResponseFileTokenReplacer à null. Vous pouvez également fournir une implémentation personnalisée pour personnaliser le traitement des fichiers de réponse.

Dernier point mais non le moindre, le IConsole et toutes les interfaces associées (IStandardOut, IStandardError, IStandardIn) ont été supprimées. InvocationConfiguration expose deux TextWriter propriétés : Output et Error. Vous pouvez définir ces propriétés sur n’importe quelle TextWriter instance, telle qu’un StringWriter, que vous pouvez utiliser pour capturer la sortie pour les tests. La motivation de ce changement était d’exposer moins de types et de réutiliser les abstractions existantes.

Appel

Dans la version 2.0.0-beta4, l’interface ICommandHandler a exposé Invoke et InvokeAsync les méthodes utilisées pour appeler la commande analysée. Cela facilite la combinaison de code synchrone et asynchrone, par exemple en définissant un gestionnaire synchrone pour une commande, puis en l’appelant de manière asynchrone (ce qui peut entraîner un blocage). De plus, il était possible de définir un gestionnaire uniquement pour une commande, mais pas pour une option (comme l’aide, qui affiche de l’aide) ou une directive.

Une nouvelle classe CommandLineAction de base abstraite et deux classes dérivées, et SynchronousCommandLineAction, AsynchronousCommandLineAction ont été introduites. L’ancien est utilisé pour les actions synchrones qui retournent un code de int sortie, tandis que celui-ci est utilisé pour les actions asynchrones qui retournent un Task<int> code de sortie.

Vous n’avez pas besoin de créer un type dérivé pour définir une action. Vous pouvez utiliser la Command.SetAction méthode pour définir une action pour une commande. L’action synchrone peut être un délégué qui prend un System.CommandLine.ParseResult paramètre et retourne un code de int sortie (ou rien, puis un code de sortie par défaut 0 est retourné). L’action asynchrone peut être un délégué qui prend System.CommandLine.ParseResult et CancellationToken comme paramètres et retourne un Task<int> (ou Task pour récupérer le code de sortie par défaut).

rootCommand.SetAction(ParseResult parseResult =>
{
    FileInfo parsedFile = parseResult.GetValue(fileOption);
    ReadFile(parsedFile);
});

Dans le passé, le CancellationToken, transmis à InvokeAsync, a été exposé au gestionnaire via une méthode de InvocationContext:

rootCommand.SetHandler(async (InvocationContext context) =>
{
    string? urlOptionValue = context.ParseResult.GetValueForOption(urlOption);
    var token = context.GetCancellationToken();
    returnCode = await DoRootCommand(urlOptionValue, token);
});

La plupart des utilisateurs n’obtenaient pas ce jeton et ne le transmettaient plus. CancellationToken est maintenant un argument obligatoire pour les actions asynchrones, de sorte que le compilateur génère un avertissement lorsqu’il n’est pas passé plus loin (voir CA2016).

rootCommand.SetAction((ParseResult parseResult, CancellationToken token) =>
{
    string? urlOptionValue = parseResult.GetValue(urlOption);
    return DoRootCommandAsync(urlOptionValue, token);
});

Suite à ces changements et à d’autres changements mentionnés ci-dessus, la InvocationContext classe a également été supprimée. La ParseResult valeur est désormais transmise directement à l’action. Vous pouvez donc accéder aux valeurs et options analysées directement à partir de celle-ci.

Pour résumer ces modifications :

  • L’interface ICommandHandler a été supprimée. SynchronousCommandLineAction et AsynchronousCommandLineAction ont été introduits.
  • La méthode Command.SetHandler a été renommée SetAction.
  • La propriété Command.Handler a été renommée en Command.Action. Option a été étendu avec Option.Action.
  • InvocationContext a été supprimé. ParseResult est désormais transmis directement à l’action.

Pour plus d’informations sur l’utilisation d’actions, consultez Comment analyser et appeler des commandes dans System.CommandLine.

Avantages de l’API simplifiée

Les modifications apportées dans la version 2.0.0-beta5 rendent l’API plus cohérente, plus durable et plus facile à utiliser pour les utilisateurs existants et nouveaux.

De nouveaux utilisateurs doivent apprendre moins de concepts et de types, car le nombre d’interfaces publiques a diminué de 11 à 0, et les classes publiques (et les structs) ont diminué de 56 à 38. Le nombre de méthodes publiques est passé de 378 à 235, et celui des propriétés publiques de 118 à 99.

Le nombre d’assemblys référencés par System.CommandLine est réduit de 11 à 6 :

System.Collections
- System.Collections.Concurrent
- System.ComponentModel
System.Console
- System.Diagnostics.Process
System.Linq
System.Memory
- System.Net.Primitives
System.Runtime
- System.Runtime.Serialization.Formatters
+ System.Runtime.InteropServices
- System.Threading

La taille de la bibliothèque est réduite (de 32%) et la taille des applications NativeAOT qui utilisent la bibliothèque.

La simplicité a également amélioré les performances de la bibliothèque (c’est un effet secondaire du travail, et non l’objectif principal de celui-ci). Les benchmarks montrent que l’analyse et l’appel de commandes sont désormais plus rapides que dans la version 2.0.0-beta4, en particulier pour les commandes volumineuses avec de nombreuses options et arguments. Les améliorations des performances sont visibles dans les scénarios synchrones et asynchrones.

L’application la plus simple, présentée précédemment, produit les résultats suivants :

BenchmarkDotNet v0.15.0, Windows 11 (10.0.26100.4061/24H2/2024Update/HudsonValley)
AMD Ryzen Threadripper PRO 3945WX 12-Cores 3.99GHz, 1 CPU, 24 logical and 12 physical cores
.NET SDK 9.0.300
  [Host]     : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2
  Job-JJVAFK : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2

EvaluateOverhead=False  OutlierMode=DontRemove  InvocationCount=1
IterationCount=100  UnrollFactor=1  WarmupCount=3

| Method                  | Args           | Mean      | StdDev   | Ratio |
|------------------------ |--------------- |----------:|---------:|------:|
| Empty                   | --bool -s test |  63.58 ms | 0.825 ms |  0.83 |
| EmptyAOT                | --bool -s test |  14.39 ms | 0.507 ms |  0.19 |
| SystemCommandLineBeta4  | --bool -s test |  85.80 ms | 1.007 ms |  1.12 |
| SystemCommandLineNow    | --bool -s test |  76.74 ms | 1.099 ms |  1.00 |
| SystemCommandLineNowR2R | --bool -s test |  69.35 ms | 1.127 ms |  0.90 |
| SystemCommandLineNowAOT | --bool -s test |  17.35 ms | 0.487 ms |  0.23 |

Comme vous pouvez le voir, l’heure de démarrage (les benchmarks signalent le temps nécessaire à l’exécution d’un exécutable donné) s’est améliorée de 12% par rapport à 2.0.0-beta4. Si vous compilez l’application avec NativeAOT, il ne s’agit que de 3 ms plus lentement qu’une application NativeAOT qui n’analyse pas du tout les arguments (EmptyAOT dans le tableau ci-dessus). En outre, si vous excluez la surcharge d’une application vide (63,58 ms), l’analyse est de 40% plus rapide que dans 2.0.0-beta4 (22,22 ms vs 13.66 ms).

Voir aussi