Partager via


System.CommandLine Guide de migration 2.0.0-beta5

Important

System.CommandLine est actuellement disponible en préversion et cette documentation concerne la version 2.0 bêta 5. Certaines informations se rapportent à un produit en version préliminaire qui peut être sensiblement modifié avant sa sortie. Microsoft n’offre aucune garantie, expresse ou implicite, en ce qui concerne les informations fournies ici.

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 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 préfixe pour les Is 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 System.CommandLine.Parsing.CommandLineParser
System.CommandLine.Parsing.OptionResult.IsImplicit System.CommandLine.Parsing.OptionResult.Implicit
System.CommandLine.Option.IsRequired System.CommandLine.Option.Required
System.CommandLine.Symbol.IsHidden System.CommandLine.Symbol.Hidden
System.CommandLine.Option.ArgumentHelpName System.CommandLine.Option.HelpName
System.CommandLine.Parsing.OptionResult.Token System.CommandLine.Parsing.OptionResult.IdentifierToken
System.CommandLine.Parsing.ParseResult.FindResultFor System.CommandLine.Parsing.ParseResult.GetResult
System.CommandLine.Parsing.SymbolResult.ErrorMessage System.CommandLine.Parsing.SymbolResult.AddError

Pour permettre à plusieurs erreurs pour le même symbole d’être signalé, la ErrorMessage propriété a été convertie en méthode et renommée AddErroren .

Exposition de collections mutables

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 ont été 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, nous avons modifié les API 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é
System.CommandLine.Command.AddArgument System.CommandLine.Command.Arguments.Add
System.CommandLine.Command.AddOption System.CommandLine.Command.Options.Add
System.CommandLine.Command.AddCommand System.CommandLine.Command.Subcommands.Add
System.CommandLine.Command.AddValidator System.CommandLine.Command.Validators.Add
System.CommandLine.Option.AddValidator System.CommandLine.Option.Validators.Add
System.CommandLine.Argument.AddValidator System.CommandLine.Argument.Validators.Add
System.CommandLine.Command.AddCompletions System.CommandLine.Command.CompletionSources.Add
System.CommandLine.Option.AddCompletions System.CommandLine.Option.CompletionSources.Add
System.CommandLine.Argument.AddCompletions System.CommandLine.Argument.CompletionSources.Add
System.CommandLine.Command.AddAlias System.CommandLine.Command.Aliases.Add
System.CommandLine.Option.AddAlias System.CommandLine.Option.Aliases.Add

Les RemoveAlias méthodes et HasAlias les méthodes ont également été supprimées, car la Aliases propriété 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 comme l’alias le plus long avec des préfixes tels que --, -ou / supprimé. C’était déroutant.

En outre, pour obtenir la valeur analysée, les utilisateurs ont dû 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>). Nous avons également séparé le concept d’un nom et d’alias ; désormais, 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 Command, il est utilisé pour identifier le symbole pendant l’analyse et également pour obtenir de Option<T> 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 Option.Name ne supprime plus le --, -ou / tout autre préfixe de l’alias le plus long.
  • La Aliases propriété exposée par Option et Command est maintenant 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 que le nom soit toujours présent 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

Dans le passé, Option<T> il a exposé de nombreux constructeurs, dont certains ont accepté le nom. Étant donné que le nom est désormais obligatoire et que nous nous attendons à ce que les alias soient fréquemment fournis, Option<T>il n’y a qu’un seul constructeur. Il accepte le nom et un params tableau d’alias.

Avant 2.0.0-beta5, Option<T> avait un constructeur qui prenait 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.

L’ancien code qui a utilisé le constructeur avec une description doit être mis à jour pour utiliser le nouveau constructeur qui prend un nom et des alias, puis définir 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, les utilisateurs peuvent 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");
option.SetDefaultValue("text"); // This is not type-safe, as the value is a string, not an int.

De plus, certains des Option constructeurs ont Argument accepté un délégué d’analyse et une valeur booléenne indiquant si le délégué était un analyseur personnalisé ou un fournisseur de valeurs par défaut. C’était déroutant.

Option<T> et Argument<T> les classes ont désormais une DefaultValueFactory propriété qui peut être utilisée 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 est introuvable 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é qui peut être utilisée 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 . Cela et quelques autres délégués personnalisés ont été supprimés pour simplifier l’API et réduire le nombre de types exposés par l’API, ce qui réduit le temps de démarrage passé pendant la compilation JIT.

Pour plus d’exemples d’utilisation DefaultValueFactory et CustomParserde validation, consultez Comment personnaliser l’analyse et la validation dans System.CommandLine.

Séparation de l’analyse 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 était assez difficile de le faire. Command n’a pas exposé une 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 uniquement analysée et uniquement de manière synchrone.
  • ParseResult expose désormais les deux Invoke méthodes InvokeAsync qui peuvent être utilisées pour appeler la commande. Cela permet d’indiquer clairement que la commande est appelée après l’analyse et autorise l’appel 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é mutable et CommandLineBuilder a été supprimé. La création d’une configuration est désormais aussi simple que la création d’une instance et la définition des CommandLineConfiguration propriétés que vous souhaitez personnaliser. En outre, la création d’une nouvelle instance de configuration est l’équivalent de la méthode d’appelCommandLineBuilderUseDefaults.
  • Chaque Parse méthode accepte désormais un paramètre facultatif CommandLineConfiguration qui peut être utilisé pour personnaliser l’analyse. Lorsqu’elle n’est pas fournie, la configuration par défaut est utilisée.
  • Parser a été renommé pour CommandLineParser lever l’ambiguïté des autres types d’analyseurs afin d’éviter les conflits de noms. É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 (é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é appelée CommandLineConfigurationProcessTerminationTimeout. Elle est activée par défaut, avec un délai d’expiration de 2s. Définissez-le pour null le désactiver.

  • EnableDirectives, UseEnvironmentVariableDirective, UseParseDirectiveet UseSuggestDirective ont été supprimés. Un nouveau type de directive a été introduit et rootCommand expose désormais la System.CommandLine.RootCommand.Directives propriété. Vous pouvez ajouter, supprimer et itérer des directives à l’aide de cette collection. La directive suggest 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 propriété ParseResult.UnmatchedTokens .

  • EnablePosixBundling a été supprimé. Le regroupement est désormais activé par défaut, vous pouvez le désactiver en définissant la propriété falseCommandLineConfiguration.EnableBundling sur .

  • 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 manuellementdotnet suggest.

  • 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 propriété falseCommandLineConfiguration.EnableDefaultExceptionHandler sur . Cela est utile lorsque vous souhaitez gérer les exceptions d’une manière personnalisée, en encapsulant simplement les méthodes ou InvokeAsync les Invoke méthodes dans un bloc try-catch.

  • 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.

  • 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 la ParseErrorAction propriété exposée ParseResult.Action .

    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 nous ne prenons pas en charge votre langue, 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 System.CommandLine.CommandLineConfiguration.ResponseFileTokenReplacer propriété nullsur . Vous pouvez également fournir une implémentation personnalisée pour personnaliser le traitement des fichiers de réponse.

Enfin, mais pas le moins, les IConsole interfaces associées (IStandardOut, IStandardError, IStandardIn) ont été supprimées. System.CommandLine.CommandLineConfiguration expose deux TextWriter propriétés : Output et Error. Celles-ci peuvent être définies sur n’importe quelle TextWriter instance, telle qu’un StringWriter, qui peut être utilisée pour capturer la sortie pour les tests. Notre motivation é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 System.CommandLine.CommandLineActionde base abstraite et deux classes dérivées : System.CommandLine.SynchronousCommandLineAction et System.CommandLine.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 System.CommandLine.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 un System.CommandLine.ParseResult et CancellationToken des paramètres et retourne un Task<int> (ou Task pour obtenir le code de sortie par défaut retourné).

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

Dans le passé, le CancellationToken passage à 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 majorité de nos utilisateurs n’obtenaient pas ce jeton et le transmettaient plus loin. Nous avons fait CancellationToken un argument obligatoire pour les actions asynchrones, afin que le compilateur produise un avertissement lorsqu’il n’est pas passé plus loin (CA2016).

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

Suite à ces modifications et à d’autres modifications forementées, 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 Command.SetHandler méthode a été renommée SetActionen .
  • La Command.Handler propriété a été renommée Command.Actionen . Option a été étendu avec Option.Action.
  • InvocationContext a été supprimé. La ParseResult valeur est désormais transmise 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

Nous espérons que les modifications apportées dans la version 2.0.0-beta5 feront en sorte que l’API soit plus cohérente, plus future 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 les 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

Il nous a permis de réduire la taille de la bibliothèque de 32% et la taille de l’application NativeAOT suivante de 20%:

Option<bool> boolOption = new Option<bool>(new[] { "--bool", "-b" }, "Bool option");
Option<string> stringOption = new Option<string>(new[] { "--string", "-s" }, "String option");

RootCommand command = new RootCommand
{
    boolOption,
    stringOption
};

command.SetHandler<bool, string>(Run, boolOption, stringOption);

return new CommandLineBuilder(command).UseDefaults().Build().Invoke(args);

static void Run(bool boolean, string text)
{
    Console.WriteLine($"Bool option: {text}");
    Console.WriteLine($"String option: {boolean}");
}
Option<bool> boolOption = new Option<bool>("--bool", "-b") { Description = "Bool option" };
Option<string> stringOption = new Option<string>("--string", "-s") { Description = "String option" };

RootCommand command = new ()
{
    boolOption,
    stringOption,
};

command.SetAction(parseResult => Run(parseResult.GetValue(boolOption), parseResult.GetValue(stringOption)));

return new CommandLineConfiguration(command).Invoke(args);

static void Run(bool boolean, string text)
{
    Console.WriteLine($"Bool option: {text}");
    Console.WriteLine($"String option: {boolean}");
}

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.

Pour l’application la plus simple présentée précédemment, nous avons obtenu 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 nous 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, lorsque nous excluons 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 et 13,66 ms).

Voir aussi