Dela via


System.CommandLine Migreringsguide för 2.0.0-beta5+

Viktigt!

System.CommandLine är för närvarande i förhandsversion. Den här dokumentationen gäller version 2.0 beta 7. Viss information gäller förhandsversionsprodukt som kan ändras avsevärt innan den släpps. Microsoft lämnar inga garantier, uttryckliga eller underförstådda, med avseende på den information som tillhandahålls här.

Huvudfokus för 2.0.0-beta5-versionen var att förbättra API:erna och ta ett steg mot att släppa en stabil version av System.CommandLine. API:erna har förenklats och gjorts mer konsekventa och förenliga med ramverkets designriktlinjer. Den här artikeln beskriver de icke-bakåtkompatibla ändringar som gjordes i 2.0.0-beta5 och 2.0.0-beta7, samt motiveringen bakom dem.

Döpa om

I 2.0.0-beta4 följde inte alla typer och medlemmar namngivningsriktlinjerna. Vissa var inte konsekventa med namngivningskonventionerna, till exempel att använda prefixet Is för booleska egenskaper. I 2.0.0-beta5 har vissa typer och medlemmar bytt namn. I följande tabell visas de gamla och nya namnen:

Gammalt namn Nytt namn
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)

För att tillåta att flera fel för samma symbol kan rapporteras, konverterades egenskapen ErrorMessage till en metod och bytte namn till AddError.

Föränderliga samlingar med alternativ och validatorer

Version 2.0.0-beta4 hade många Add metoder som användes för att lägga till objekt i samlingar, till exempel argument, alternativ, underkommandon, validatorer och slutföranden. Vissa av dessa samlingar exponerades via egenskaper som skrivskyddade samlingar. Därför var det omöjligt att ta bort objekt från dessa samlingar.

I 2.0.0-beta5 ändrades API:erna för att exponera föränderliga samlingar i stället för Add metoder och (ibland) skrivskyddade samlingar. På så sätt kan du inte bara lägga till objekt eller räkna upp dem, utan även ta bort dem. I följande tabell visas den gamla metoden och de nya egenskapsnamnen:

Namn på gammal metod Ny egenskap
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

Metoderna RemoveAlias och HasAlias togs också bort eftersom egenskapen Aliases nu är en föränderlig samling. Du kan använda Remove metoden för att ta bort ett alias från samlingen. Contains Använd metoden för att kontrollera om det finns ett alias.

Namn och alias

Före 2.0.0-beta5 fanns det ingen tydlig uppdelning mellan namnet och aliasen för en symbol. När name inte angavs för Option<T>-konstruktorn angav symbolen sitt namn som det längsta aliaset med prefixer som --, -, eller / har tagits bort. Det var förvirrande.

För att hämta det parsade värdet var du dessutom tvungen att lagra en referens till ett alternativ eller ett argument och sedan använda det för att hämta värdet från ParseResult.

För att främja enkelhet och explicititet är namnet på en symbol nu en obligatorisk parameter för varje symbolkonstruktor (inklusive Argument<T>). Begreppet namn och alias är nu separat: alias är bara alias och innehåller inte namnet på symbolen. Naturligtvis är de valfria. Därför gjordes följande ändringar:

  • name är nu ett obligatoriskt argument för varje offentlig konstruktor för Argument<T>, Option<T>och Command. När det gäller Argument<T>används den inte för parsning, utan för att generera hjälpen. När det gäller Option<T> och Commandanvänds den för att identifiera symbolen under parsning och även för hjälp och slutföranden.
  • Symbol.Name-egenskapen är inte längre virtual; den är nu skrivskyddad och returnerar namnet som det angavs när symbolen skapades. Eftersom Symbol.DefaultName togs bort och Option.Name tar inte längre bort --, -, eller / eller något annat prefix från det längsta aliaset.
  • Egenskapen Aliases som exponeras av Option och Command är nu en föränderlig samling. Den här samlingen innehåller inte längre namnet på symbolen.
  • System.CommandLine.Parsing.IdentifierSymbol togs bort (det var en bastyp för både Command och Option).

Med det namn som alltid finns kan du hämta det parsade värdet efter namn:

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

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

Skapa alternativ med alias

Tidigare Option<T> exponerade många konstruktorer, varav några accepterade namnet. Eftersom namnet nu är obligatoriskt och alias ofta anges för Option<T>finns det bara en enda konstruktor. Den accepterar namnet och en params matris med alias.

Innan 2.0.0-beta5 hade Option<T> en konstruktor som tog ett namn och en beskrivning. Därför kan det andra argumentet nu behandlas som ett alias i stället för en beskrivning. Det är den enda kända icke-bakåtkompatibla ändringen i API:et som inte orsakar något kompilatorfel.

Uppdatera all kod som skickade en beskrivning till konstruktorn för att använda den nya konstruktorn som tar ett namn och alias och ange Description sedan egenskapen separat. Till exempel:

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

Standardvärden och anpassad parsning

I 2.0.0-beta4 kan du ange standardvärden för alternativ och argument med hjälp SetDefaultValue av metoderna. Dessa metoder accepterade ett object värde som inte var typsäkert och kan leda till körningsfel om värdet inte var kompatibelt med alternativet eller argumenttypen:

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

Dessutom accepterade några av Option konstruktorerna och Argument ett parse-ombud (parse) och ett booleskt (isDefault) som anger om ombudet var en anpassad parser eller en standardvärdeprovider, vilket var förvirrande.

Option<T> och Argument<T> klasser har nu en DefaultValueFactory egenskap som du kan använda för att ange ett ombud som kan anropas för att hämta standardvärdet för alternativet eller argumentet. Det här ombudet anropas när alternativet eller argumentet inte hittas i de parsade kommandoradsindata.

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

Argument<T> och Option<T> levereras också med en CustomParser egenskap som du kan använda för att ange en anpassad parser för symbolen:

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

Dessutom CustomParser accepterar ett ombud av typen Func<ParseResult,T>, i stället för föregående ParseArgument ombud. Detta och några andra anpassade ombud har tagits bort för att förenkla API:et och minska antalet typer som exponeras av API:et, vilket minskar starttiden under JIT-kompilering.

Fler exempel på hur du använder DefaultValueFactory och CustomParserfinns i Anpassa parsning och validering i System.CommandLine.

Separering av analys och anrop

I 2.0.0-beta4 var det möjligt att separera parsningen och anropa kommandona, men det var inte klart hur man gör det. Command exponerade inte en Parse metod, men CommandExtensions angav Parse, Invokeoch InvokeAsync tilläggsmetoder för Command. Detta var förvirrande, eftersom det inte var klart vilken metod som ska användas och när. Följande ändringar har gjorts för att förenkla API:et:

  • Command exponerar nu en Parse metod som returnerar ett ParseResult objekt. Den här metoden används för att parsa kommandoradsindata och returnera resultatet av parsningsåtgärden. Dessutom är det tydligt att kommandot inte anropas utan parsas och endast på synkront sätt.
  • ParseResult exponerar nu både Invoke metoder och InvokeAsync metoder som du kan använda för att anropa kommandot. Det här mönstret gör det tydligt att kommandot anropas efter parsning och tillåter både synkron och asynkron anrop.
  • Klassen CommandExtensions har tagits bort eftersom den inte längre behövs.

Konfiguration

Före 2.0.0-beta5 var det möjligt att anpassa parsningen, men bara med några av de offentliga Parse metoderna. Det fanns en Parserklass som exponerade två offentliga konstruktorer: en som accepterade en Command och en annan som accepterade en CommandLineConfiguration. CommandLineConfiguration var oföränderlig och för att skapa den var du tvungen att använda ett byggmönster som exponerades av CommandLineBuilder klassen. Följande ändringar har gjorts för att förenkla API:et:

  • CommandLineConfiguration delades upp i två föränderliga klasser (i 2.0.0-beta7): ParserConfiguration och InvocationConfiguration. Att skapa en konfiguration för anrop är nu lika enkelt som att skapa en instans av InvocationConfiguration och ange de egenskaper som du vill anpassa.
  • Varje Parse metod accepterar nu en valfri ParserConfiguration parameter som du kan använda för att anpassa parsningen. När den inte anges används standardkonfigurationen.
  • För att undvika namnkonflikter ändrades Parser till CommandLineParser för att särskilja den från andra parsertyper. Eftersom den är tillståndslös är den nu en statisk klass med endast statiska metoder. Den exponerar två Parse parsningsmetoder: en accepterar en IReadOnlyList<string> args och en annan accepterar en string args. Den senare använder CommandLineParser.SplitCommandLine(String) (även offentligt) för att dela upp kommandoradsindata i token innan den parsas.

CommandLineBuilderExtensions togs också bort. Så här kan du mappa dess metoder till de nya API:erna:

  • CancelOnProcessTermination är nu en egenskap i InvocationConfiguration som heter ProcessTerminationTimeout. Den är aktiverad som standard, med en tidsgräns på 2 sekunder. Om du vill inaktivera den ställer du in den på null. För mer information, se Tidsbegränsning för processavslut.

  • EnableDirectives, UseEnvironmentVariableDirective, UseParseDirectiveoch UseSuggestDirective togs bort. En ny direktivtyp introducerades och RootCommand exponerar nu en Directives egenskap. Du kan lägga till, ta bort och iterera direktiv med hjälp av den här samlingen. Förslagsdirektivet ingår som standard. Du kan också använda andra direktiv som DiagramDirective eller EnvironmentVariablesDirective.

  • EnableLegacyDoubleDashBehavior har tagits bort. Alla omatchade token exponeras nu av egenskapen ParseResult.UnmatchedTokens . Mer information finns i Omatchade token.

  • EnablePosixBundling har tagits bort. Paketeringen är nu aktiverad som standard, du kan inaktivera den genom att ange ParserConfiguration.EnablePosixBundling egenskap till false. Mer information finns i EnablePosixBundling.

  • RegisterWithDotnetSuggest togs bort eftersom den utförde en dyr åtgärd, vanligtvis under programstarten. Nu måste du registrera kommandon med dotnet suggestmanuellt.

  • UseExceptionHandler har tagits bort. Standardfelhanteraren är nu aktiverad som standard. du kan inaktivera den genom att ange egenskapen InvocationConfiguration.EnableDefaultExceptionHandler till false. Detta är användbart när du vill hantera undantag på ett anpassat sätt genom att bara omsluta Invoke metoderna eller InvokeAsync i ett try-catch-block. Mer information finns i EnableDefaultExceptionHandler.

  • UseHelp och UseVersion togs bort. Hjälpen och versionen exponeras nu av HelpOption och VersionOption offentliga typer. Båda ingår som standard i de alternativ som definieras av RootCommand. Mer information finns i Anpassa hjälputdata och versionsalternativ.

  • UseHelpBuilder har tagits bort. Mer information om hur du anpassar hjälputdata finns i Anpassa hjälp i System.CommandLine.

  • AddMiddleware har tagits bort. Det saktade ned programmets start och funktioner kan uttryckas utan det.

  • UseParseErrorReporting och UseTypoCorrections togs bort. Parsningsfelen rapporteras nu som standard när du anropar ParseResult. Du kan konfigurera det med hjälp av åtgärden ParseErrorAction som exponeras av egenskapen ParseResult.Action .

    ParseResult result = rootCommand.Parse("myArgs", config);
    if (result.Action is ParseErrorAction parseError)
    {
        parseError.ShowTypoCorrections = true;
        parseError.ShowHelp = false;
    }
    
  • UseLocalizationResources och LocalizationResources togs bort. Den här funktionen användes främst av dotnet CLI för att lägga till saknade översättningar i System.CommandLine. Alla dessa översättningar flyttades till System.CommandLine sig själv, så den här funktionen behövs inte längre. Om stöd för ditt språk saknas kan du rapportera ett problem.

  • UseTokenReplacer har tagits bort. Svarsfiler är aktiverade som standard, men du kan inaktivera dem genom att ange ResponseFileTokenReplacer egenskapen till null. Du kan också tillhandahålla en anpassad implementering för att anpassa hur svarsfiler bearbetas.

Sist men inte minst togs IConsole och alla relaterade gränssnitt (IStandardOut, IStandardError, IStandardIn) bort. InvocationConfiguration exponerar två TextWriter egenskaper: Output och Error. Du kan ange dessa egenskaper till valfri TextWriter instans, till exempel en StringWriter, som du kan använda för att samla in utdata för testning. Motiveringen till den här ändringen var att exponera färre typer och återanvända befintliga abstraktioner.

Åkallan

I version 2.0.0-beta4 exponerades gränssnittet ICommandHandler och metoderna Invoke och InvokeAsync som användes för att anropa det tolkade kommandot. Detta gjorde det enkelt att blanda synkron och asynkron kod, till exempel genom att definiera en synkron hanterare för ett kommando och sedan anropa den asynkront (vilket kan leda till ett dödläge). Dessutom var det möjligt att definiera en hanterare endast för ett kommando, men inte för ett alternativ (t.ex. hjälp, som visar hjälp) eller ett direktiv.

En ny abstrakt basklass CommandLineAction och två härledda klasser, SynchronousCommandLineAction och AsynchronousCommandLineAction, har introducerats. Den förra används för synkrona åtgärder som returnerar en int slutkod, medan den senare används för asynkrona åtgärder som returnerar en Task<int> slutkod.

Du behöver inte skapa en härledd typ för att definiera en åtgärd. Du kan använda Command.SetAction metoden för att ange en åtgärd för ett kommando. Den synkrona åtgärden kan vara ett ombud som tar en System.CommandLine.ParseResult parameter och returnerar en int slutkod (eller ingenting, och sedan returneras en standardavslutskod 0 ). Den asynkrona åtgärden kan vara ett ombud som tar en System.CommandLine.ParseResult och CancellationToken -parametrar och returnerar en Task<int> (eller Task för att få standardavslutskoden returnerad).

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

Tidigare exponerades den CancellationToken som skickades till InvokeAsync för hanteraren via en metod för InvocationContext:

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

De flesta användare erhöll inte denna token och skickade den inte vidare. CancellationToken är nu ett obligatoriskt argument för asynkrona åtgärder, så att kompilatorn skapar en varning när den inte skickas vidare (se CA2016).

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

Som ett resultat av dessa och andra ovan nämnda ändringar InvocationContext togs klassen också bort. ParseResult Skickas nu direkt till åtgärden, så att du kan komma åt de tolkade värdena och alternativen direkt från den.

Så här sammanfattar du dessa ändringar:

  • Gränssnittet ICommandHandler har tagits bort. SynchronousCommandLineAction och AsynchronousCommandLineAction introducerades.
  • Metoden Command.SetHandler har bytt namn till SetAction.
  • Egenskapen Command.Handler har bytt namn till Command.Action. Option utökades med Option.Action.
  • InvocationContext har tagits bort. Nu skickas ParseResult direkt till åtgärden.

Mer information om hur du använder åtgärder finns i Så här parsar och anropar du kommandon i System.CommandLine.

Fördelarna med det förenklade API:et

Ändringarna i 2.0.0-beta5 gör API:et mer konsekvent, framtidssäkert och enklare att använda för befintliga och nya användare.

Nya användare behöver lära sig färre begrepp och typer, eftersom antalet offentliga gränssnitt minskade från 11 till 0, och offentliga klasser (och structs) minskade från 56 till 38. Antalet offentliga metoder sjönk från 378 till 235 och offentliga egenskaper från 118 till 99.

Antalet sammansättningar som refereras av System.CommandLine minskas från 11 till 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

Storleken på biblioteket minskas (med 32%) och så är storleken på NativeAOT-appar som använder biblioteket.

Enkelhet har också förbättrat bibliotekets prestanda (det är en bieffekt av arbetet, inte huvudmålet för det). Riktmärkena visar att parsningen och anropet av kommandon nu är snabbare än i 2.0.0-beta4, särskilt för stora kommandon med många alternativ och argument. Prestandaförbättringarna visas i både synkrona och asynkrona scenarier.

Den enklaste appen, som presenterades tidigare, ger följande resultat:

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 |

Som du ser har starttiden (riktmärkena rapporterar den tid som krävs för att köra den körbara filen) förbättrats med 12% jämfört med 2.0.0-beta4. Om du kompilerar appen med NativeAOT är det bara 3 ms långsammare än en NativeAOT-app som inte parsar args alls (EmptyAOT i tabellen ovan). Om du undantar omkostnaderna för en tom app (63,58 ms) är parsningen dessutom 40% snabbare än i 2.0.0-beta4 (22.22 ms jämfört med 13.66 ms).

Se även