Delen via


System.CommandLine Migratiehandleiding voor 2.0.0-beta5+

De belangrijkste focus voor de release 2.0.0-beta5 was het verbeteren van de API's en het nemen van een stap voor het vrijgeven van een stabiele versie van System.CommandLine. De API's zijn vereenvoudigd en coherenter en consistenter gemaakt met de ontwerprichtlijnen voor frameworks. In dit artikel worden de belangrijke wijzigingen beschreven die zijn aangebracht in 2.0.0-beta5 en 2.0.0-beta7 en de redenering erachter.

Hernoemen

In 2.0.0-beta4 hebben niet alle typen en leden de naamgevingsrichtlijnen gevolgd. Sommige waren niet consistent met de naamconventies, zoals het gebruik van het Is voorvoegsel voor Boole-eigenschappen. In 2.0.0-beta5 zijn sommige typen en leden hernoemd. In de volgende tabel ziet u de oude en nieuwe namen:

Oude naam Nieuwe naam
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)

† Om toe te staan dat meerdere fouten voor hetzelfde symbool worden gerapporteerd, is de ErrorMessage eigenschap geconverteerd naar een methode en gewijzigd in AddError.

Onveranderbare verzamelingen opties en validators

Versie 2.0.0-beta4 had talloze Add methoden die werden gebruikt om items toe te voegen aan verzamelingen, zoals argumenten, opties, subopdrachten, validators en voltooiingen. Sommige van deze verzamelingen werden blootgesteld via eigenschappen als alleen-lezen verzamelingen. Daarom was het onmogelijk om items uit die verzamelingen te verwijderen.

In 2.0.0-beta5 zijn de API's gewijzigd om veranderlijke verzamelingen beschikbaar te maken in plaats van Add methoden en (soms) alleen-lezenverzamelingen. Hiermee kunt u niet alleen items toevoegen of opsommen, maar ook verwijderen. In de volgende tabel ziet u de oude methode en nieuwe eigenschapsnamen:

Oude methodenaam Nieuwe eigenschap
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

De RemoveAlias en HasAlias methoden zijn ook verwijderd, omdat de Aliases eigenschap nu een onveranderbare verzameling is. U kunt de Remove methode gebruiken om een alias uit de verzameling te verwijderen. Gebruik de Contains methode om te controleren of er een alias bestaat.

Namen en aliassen

Vóór 2.0.0-beta5 was er geen duidelijke scheiding tussen de naam en aliassen van een symbool. Als name niet werd opgegeven voor de Option<T> constructor, rapporteerde het symbool zijn naam als de langste alias met voorvoegsels zoals --, -, of / verwijderd. Dat was verwarrend.

Om de geparseerde waarde op te halen, moest u bovendien een verwijzing naar een optie of argument opslaan en deze vervolgens gebruiken om de waarde op te halen uit ParseResult.

Ter bevordering van eenvoud en explicietheid is de naam van een symbool nu een verplichte parameter voor elke symboolconstructor (inclusief Argument<T>). Het concept van een naam en aliassen is nu gescheiden: aliassen zijn alleen aliassen en bevatten niet de naam van het symbool. Natuurlijk zijn ze optioneel. Als gevolg hiervan zijn de volgende wijzigingen aangebracht:

  • name is nu een verplicht argument voor elke openbare constructor van Argument<T>, Option<T>en Command. In het geval van Argument<T>, wordt het niet gebruikt voor parseren, maar om de help te genereren. In het geval van Option<T> en Commandwordt het gebruikt om het symbool te identificeren tijdens het parseren en ook voor hulp en voltooiingen.
  • De Symbol.Name eigenschap is niet meer virtual; deze is nu alleen-lezen en retourneert de naam zoals deze is opgegeven toen het symbool werd gemaakt. Daarom werd Symbol.DefaultName verwijderd en verwijdert Symbol.Name niet langer het --, -, / of een ander voorvoegsel uit de langste alias.
  • De Aliases eigenschap die wordt weergegeven door Option en Command is nu een onveranderbare verzameling. Deze verzameling bevat niet langer de naam van het symbool.
  • System.CommandLine.Parsing.IdentifierSymbol is verwijderd (het was een basistype voor beide Command en Option).

Als u de naam altijd aanwezig hebt, kunt u de geparseerde waarde op naam ophalen:

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

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

Opties maken met aliassen

In het verleden werden Option<T> veel constructors blootgelegd, waarvan sommige de naam accepteerden. Omdat de naam nu verplicht is en aliassen vaak worden opgegeven Option<T>, is er slechts één constructor. De naam en een params matrix met aliassen worden geaccepteerd.

Vóór 2.0.0-beta5 had Option<T> een constructor die een naam en een beschrijving ontving. Daarom kan het tweede argument nu worden behandeld als een alias in plaats van een beschrijving. Dit is de enige bekende wijziging die fouten veroorzaakt in de API die geen compilerfout veroorzaakt.

Werk alle code bij die een beschrijving heeft doorgegeven aan de constructor om de nieuwe constructor te gebruiken die een naam en aliassen gebruikt en stel de Description eigenschap vervolgens afzonderlijk in. Voorbeeld:

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."
};

Standaardwaarden en aangepaste parsering

In 2.0.0-beta4 kunt u standaardwaarden instellen voor opties en argumenten met behulp van de SetDefaultValue methoden. Deze methoden hebben een object waarde geaccepteerd, die niet veilig was en kan leiden tot runtimefouten als de waarde niet compatibel was met de optie of het argumenttype:

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

Bovendien hebben sommige van de Option en Argument constructors een parseringsdelegatie (parse) en een Booleaanse waarde (isDefault) geaccepteerd die aangeeft of de gemachtigde een aangepaste parser of een standaardwaardeprovider was, wat verwarrend was.

Option<T> en Argument<T> klassen hebben nu een DefaultValueFactory eigenschap die u kunt gebruiken om een gemachtigde in te stellen die kan worden aangeroepen om de standaardwaarde voor de optie of het argument op te halen. Deze gemachtigde wordt aangeroepen wanneer de optie of het argument niet wordt gevonden in de geparseerde opdrachtregelinvoer.

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

Argument<T> en Option<T> wordt ook geleverd met een CustomParser eigenschap die u kunt gebruiken om een aangepaste parser in te stellen voor het symbool:

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;
    }
};

CustomParser Bovendien accepteert u een gemachtigde van het typeFunc<ParseResult,T>, in plaats van de vorige ParseArgument gedelegeerde. Deze en enkele andere aangepaste gemachtigden zijn verwijderd om de API te vereenvoudigen en het aantal typen te verminderen dat door de API wordt weergegeven, waardoor de opstarttijd tijdens JIT-compilatie wordt verminderd.

Voor meer voorbeelden van hoe u DefaultValueFactory en CustomParser kunt gebruiken, zie Hoe parsering en validatie aan te passen in System.CommandLine.

Scheiding van parseren en aanroepen

In 2.0.0-beta4 was het mogelijk om het parseren en aanroepen van opdrachten te scheiden, maar het was niet duidelijk hoe het moest. Command heeft geen Parse methode blootgelegd, maar CommandExtensions bood wel Parse, Invoke, en InvokeAsync extensiemethoden voor Command. Dit was verwarrend, omdat het niet duidelijk was welke methode moest worden gebruikt en wanneer. De volgende wijzigingen zijn aangebracht om de API te vereenvoudigen:

  • Command maakt nu een Parse methode beschikbaar die een ParseResult object retourneert. Deze methode wordt gebruikt om de opdrachtregelinvoer te parseren en het resultaat van de parseringsbewerking te retourneren. Bovendien wordt duidelijk dat de opdracht niet wordt aangeroepen, maar geparseerd, en alleen synchroon.
  • ParseResult maakt nu zowel als InvokeInvokeAsync methoden beschikbaar die u kunt gebruiken om de opdracht aan te roepen. Dit patroon maakt het duidelijk dat de opdracht wordt aangeroepen na parseren en dat zowel synchrone als asynchrone aanroep mogelijk is.
  • De CommandExtensions klasse is verwijderd, omdat deze niet meer nodig is.

Configuratie

Vóór 2.0.0-beta5 was het mogelijk om de parsering aan te passen, maar alleen met een aantal openbare Parse methoden. Er was een Parser klasse die twee openbare constructors bood: één die een Command accepteert en een andere die een CommandLineConfiguration accepteert. CommandLineConfiguration onveranderbaar was en om het te maken, moest u een opbouwpatroon gebruiken dat door de CommandLineBuilder klasse wordt weergegeven. De volgende wijzigingen zijn aangebracht om de API te vereenvoudigen:

  • CommandLineConfiguration is gesplitst in twee veranderlijke klassen (in 2.0.0-beta7): ParserConfiguration en InvocationConfiguration. Het maken van een aanroepconfiguratie is nu net zo eenvoudig als het maken van een exemplaar van InvocationConfiguration en het instellen van de eigenschappen die u wilt aanpassen.
  • Elke Parse methode accepteert nu een optionele ParserConfiguration parameter die u kunt gebruiken om de parsering aan te passen. Wanneer deze niet is opgegeven, wordt de standaardconfiguratie gebruikt.
  • Om naamconflicten te voorkomen, is Parser hernoemd naar CommandLineParser om verwarring met andere parsertypen te vermijden. Omdat het staatloos is, is het nu een statische klasse met alleen statische methoden. Er zijn twee Parse parsermethoden: één die een IReadOnlyList<string> args accepteert en een andere die een string args accepteert. De laatste gebruikt CommandLineParser.SplitCommandLine(String) (ook openbaar) om de opdrachtregelinvoer in tokens te splitsen voordat deze wordt geparserd.

CommandLineBuilderExtensions is ook verwijderd. U kunt de methoden als volgt toewijzen aan de nieuwe API's:

  • CancelOnProcessTermination is nu een eigenschap van InvocationConfiguration genaamd ProcessTerminationTimeout. Deze functie is standaard ingeschakeld, met een time-out van 2 seconden. Als u het wilt uitschakelen, stelt u deze in op null. Zie Time-out voor procesbeëindiging voor meer informatie.

  • EnableDirectives, UseEnvironmentVariableDirective, UseParseDirectiveen UseSuggestDirective zijn verwijderd. Er is een nieuw Directive-type geïntroduceerd en RootCommand stelt nu een Directives eigenschap beschikbaar. U kunt instructies toevoegen, verwijderen en herhalen met behulp van deze verzameling. Suggestie-instructie is standaard opgenomen; u kunt ook andere instructies gebruiken, zoals DiagramDirective of EnvironmentVariablesDirective.

  • EnableLegacyDoubleDashBehavior is verwijderd. Alle niet-overeenkomende tokens worden nu weergegeven door de ParseResult.UnmatchedTokens eigenschap. Zie Niet-gerelateerde tokens voor meer informatie.

  • EnablePosixBundling is verwijderd. De bundeling is nu standaard ingeschakeld. U kunt deze uitschakelen door de ParserConfiguration.EnablePosixBundling eigenschap in te stellen op false. Zie EnablePosixBundling voor meer informatie.

  • RegisterWithDotnetSuggest is verwijderd omdat het een dure bewerking heeft uitgevoerd, meestal tijdens het opstarten van de toepassing. Nu moet u opdrachten dotnet suggesthandmatig registreren.

  • UseExceptionHandler is verwijderd. De standaarduitzonderingshandler is nu standaard ingeschakeld; u kunt deze uitschakelen door de InvocationConfiguration.EnableDefaultExceptionHandler eigenschap in te stellen op false. Dit is handig als u uitzonderingen op een aangepaste manier wilt verwerken door de Invoke of InvokeAsync methoden in een try-catch-blok te verpakken. Zie EnableDefaultExceptionHandler voor meer informatie.

  • UseHelp en UseVersion zijn verwijderd. De help en versie worden nu toegankelijk gemaakt door de HelpOption en VersionOption openbare typen. Ze zijn beide standaard opgenomen in de opties die zijn gedefinieerd door RootCommand. Zie Help-uitvoer aanpassen en versieoptie voor meer informatie.

  • UseHelpBuilder is verwijderd. Voor meer informatie over hoe u de help-uitvoer kunt aanpassen, zie Hoe help aan te passen in System.CommandLine.

  • AddMiddleware is verwijderd. Het vertraagde het opstarten van de toepassing, en functies kunnen zonder deze worden uitgevoerd.

  • UseParseErrorReporting en UseTypoCorrections zijn verwijderd. De parseringsfouten worden nu standaard gerapporteerd bij het aanroepen ParseResult. U kunt deze configureren met behulp van de ParseErrorAction actie die door de ParseResult.Action eigenschap wordt weergegeven.

    ParseResult result = rootCommand.Parse("myArgs", config);
    if (result.Action is ParseErrorAction parseError)
    {
        parseError.ShowTypoCorrections = true;
        parseError.ShowHelp = false;
    }
    
  • UseLocalizationResources en LocalizationResources zijn verwijderd. Deze functie is voornamelijk gebruikt door de dotnet CLI om ontbrekende vertalingen toe te voegen aan System.CommandLine. Al deze vertalingen zijn verplaatst naar het System.CommandLine zelf, dus deze functie is niet meer nodig. Als er ondersteuning voor uw taal ontbreekt, meldt u een probleem.

  • UseTokenReplacer is verwijderd. Antwoordbestanden zijn standaard ingeschakeld, maar u kunt ze uitschakelen door de ResponseFileTokenReplacer eigenschap in te stellen op null. U kunt ook een aangepaste implementatie bieden om aan te passen hoe antwoordbestanden worden verwerkt.

Ten slotte zijn de IConsole en alle gerelateerde interfaces (IStandardOut, IStandardError, IStandardIn) verwijderd. InvocationConfiguration maakt twee TextWriter eigenschappen beschikbaar: Output en Error. U kunt deze eigenschappen instellen op elk TextWriter exemplaar, zoals een StringWriter, dat u kunt gebruiken om uitvoer vast te leggen voor testen. De motivatie voor deze wijziging was om minder typen weer te geven en bestaande abstracties opnieuw te gebruiken.

Aanroep

In 2.0.0-beta4 stelde de ICommandHandler interface de Invoke en InvokeAsync methoden beschikbaar, die werden gebruikt om de geparseerde opdracht aan te roepen. Hierdoor is het eenvoudig om synchrone en asynchrone code te combineren, bijvoorbeeld door een synchrone handler voor een opdracht te definiëren en deze vervolgens asynchroon aan te roepen (wat kan leiden tot een impasse). Bovendien was het mogelijk om alleen een handler te definiëren voor een opdracht, maar niet voor een optie (zoals help, die help weergeeft) of een instructie.

Er is een nieuwe abstracte basisklasse en twee afgeleide klassen geïntroduceerd, en CommandLineActioner is een nieuwe abstracte basisklasse SynchronousCommandLineAction en twee afgeleide klassen AsynchronousCommandLineAction geïntroduceerd. De vorige wordt gebruikt voor synchrone acties die een int afsluitcode retourneren, terwijl de laatste wordt gebruikt voor asynchrone acties die een Task<int> afsluitcode retourneren.

U hoeft geen afgeleid type te maken om een actie te definiëren. U kunt de Command.SetAction methode gebruiken om een actie voor een opdracht in te stellen. De synchrone actie kan een gemachtigde zijn die een System.CommandLine.ParseResult parameter accepteert en een int afsluitcode retourneert (of niets, en vervolgens wordt er een standaardafsluitcode 0 geretourneerd). De asynchrone actie kan een delegate zijn die System.CommandLine.ParseResult en CancellationToken parameters ontvangt en een Task<int> retourneert (of Task voor de standaard afsluitcode).

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

In het verleden werd de CancellationToken doorgegeven aan InvokeAsync handler beschikbaar gesteld via een methode van InvocationContext:

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

De meeste gebruikers hebben dit token niet verkregen en deze verder doorgegeven. CancellationToken is nu een verplicht argument voor asynchrone acties, zodat de compiler een waarschuwing genereert wanneer deze niet verder wordt doorgegeven (zie CA2016).

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

Als gevolg van deze en andere bovengenoemde wijzigingen is de InvocationContext klasse ook verwijderd. De ParseResult waarde wordt nu rechtstreeks doorgegeven aan de actie, zodat u rechtstreeks toegang hebt tot de geparseerde waarden en opties.

Ga als volgt te werk om deze wijzigingen samen te vatten:

  • De ICommandHandler interface is verwijderd. SynchronousCommandLineAction en AsynchronousCommandLineAction werden geïntroduceerd.
  • De naam van de Command.SetHandler methode is gewijzigd in SetAction.
  • De naam van de Command.Handler eigenschap is gewijzigd in Command.Action. Option werd uitgebreid met Option.Action.
  • InvocationContext is verwijderd. De ParseResult wordt nu rechtstreeks aan de actie doorgegeven.

Zie Opdrachten parseren en aanroepen in System.CommandLinevoor meer informatie over het gebruik van acties.

De voordelen van de vereenvoudigde API

De wijzigingen die zijn aangebracht in 2.0.0-beta5 maken de API consistenter, toekomstbestendiger en eenvoudiger te gebruiken voor bestaande en nieuwe gebruikers.

Nieuwe gebruikers moeten minder concepten en typen leren, omdat het aantal openbare interfaces afneemt van 11 tot 0 en openbare klassen (en structs) is gedaald van 56 tot 38. Het aantal openbare methoden is gedaald van 378 tot 235 en openbare eigenschappen van 118 tot 99.

Het aantal assembly's waarnaar System.CommandLine wordt verwezen, wordt verminderd van 11 tot en met 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

De grootte van de bibliotheek wordt verkleind (met 32%) en dus ook de grootte van NativeAOT-apps die de bibliotheek gebruiken.

Eenvoud heeft ook de prestaties van de bibliotheek verbeterd (het is een neveneffect van het werk, niet het belangrijkste doel ervan). De benchmarks laten zien dat het parseren en aanroepen van opdrachten nu sneller is dan in 2.0.0-beta4, met name voor grote opdrachten met veel opties en argumenten. De prestatieverbeteringen zijn zichtbaar in zowel synchrone als asynchrone scenario's.

De eenvoudigste app, die eerder wordt gepresenteerd, produceert de volgende resultaten:

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 |

Zoals u kunt zien, is de opstarttijd (de benchmarks rapporteren de tijd die nodig is om het opgegeven uitvoerbare bestand uit te voeren) verbeterd met 12% vergeleken met 2.0.0-beta4. Als u de app compileert met NativeAOT, is deze slechts 3 ms langzamer dan een NativeAOT-app die de argumenten helemaal niet parseert (EmptyAOT in de bovenstaande tabel). Als u de overhead van een lege app (63,58 ms) uitsluit, is de parsering 40% sneller dan in 2.0.0-beta4 (22.22 ms vs 13.66 ms).

Zie ook