Compartir a través de


System.CommandLine Guía de migración 2.0.0-beta5

Importante

System.CommandLine actualmente está en versión preliminar y esta documentación es para la versión 2.0 beta 5. Cierta información se relaciona con el producto de versión preliminar que puede modificarse sustancialmente antes de su lanzamiento. Microsoft no ofrece ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

El objetivo principal de la versión 2.0.0-beta5 era mejorar las API y dar un paso para publicar una versión estable de System.CommandLine. Las API se han simplificado y hecho más coherentes y coherentes con las directrices de diseño del marco. En este artículo se describen los cambios importantes realizados en la versión 2.0.0-beta5 y el razonamiento subyacente.

Cambiar nombre

En 2.0.0-beta4, no todos los tipos y miembros siguieron las directrices de nomenclatura. Algunos no eran coherentes con las convenciones de nomenclatura, como el uso del Is prefijo para las propiedades booleanas. En 2.0.0-beta5, se ha cambiado el nombre de algunos tipos y miembros. En la tabla siguiente se muestran los nombres antiguos y nuevos:

Nombre antiguo Nuevo nombre
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

Para permitir que se notifiquen varios errores para el mismo símbolo, la ErrorMessage propiedad se convirtió en un método y se cambió el nombre a AddError.

Exposición de colecciones mutables

La versión 2.0.0-beta4 tenía numerosos Add métodos que se usaban para agregar elementos a colecciones, como argumentos, opciones, subcomandos, validadores y finalizaciones. Algunas de estas colecciones se expusieron a través de propiedades como colecciones de solo lectura. Debido a eso, era imposible quitar elementos de esas colecciones.

En la versión 2.0.0-beta5, cambiamos las API para exponer colecciones mutables en lugar de métodos y (a veces) colecciones de Add solo lectura. Esto le permite no solo agregar elementos ni enumerarlos, sino también quitarlos. En la tabla siguiente se muestran el método antiguo y los nombres de propiedad nuevos:

Nombre del método antiguo Propiedad nueva
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

Los RemoveAlias métodos y HasAlias también se quitaron, ya que la Aliases propiedad es ahora una colección mutable. Puede usar el Remove método para quitar un alias de la colección. Use el Contains método para comprobar si existe un alias.

Nombres y alias

Antes de la versión 2.0.0-beta5, no había ninguna separación clara entre el nombre y los alias de un símbolo. Cuando name no se proporcionó para el Option<T> constructor, el símbolo notificó su nombre como el alias más largo con prefijos como --, -o / quitado. Eso fue confuso.

Además, para obtener el valor analizado, los usuarios tenían que almacenar una referencia a una opción o un argumento y, a continuación, usarlo para obtener el valor de ParseResult.

Para promover la simplicidad y la explícitaidad, el nombre de un símbolo es ahora un parámetro obligatorio para cada constructor de símbolos (incluido Argument<T>). También separamos el concepto de un nombre y alias; ahora los alias son solo alias y no incluyen el nombre del símbolo. Por supuesto, son opcionales. Como resultado, se realizaron los siguientes cambios:

  • name ahora es un argumento obligatorio para cada constructor público de Argument<T>, Option<T>y Command. En el caso de Argument<T>, no se usa para el análisis, sino para generar la ayuda. En el caso de Option<T> y Command, se usa para identificar el símbolo durante el análisis y también para obtener ayuda y finalizaciones.
  • La Symbol.Name propiedad ya virtualno es ; ahora es de solo lectura y devuelve el nombre tal como se proporcionó cuando se creó el símbolo. Debido a eso, Symbol.DefaultName se quitó y Option.Name ya no quita , ---o / cualquier otro prefijo del alias más largo.
  • La Aliases propiedad expuesta por Option y Command ahora es una colección mutable. Esta colección ya no incluye el nombre del símbolo.
  • System.CommandLine.Parsing.IdentifierSymbol se quitó (era un tipo base para y CommandOption).

Tener el nombre siempre presente permite obtener el valor analizado por nombre:

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

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

Creación de opciones con alias

En el pasado, Option<T> expone muchos constructores, algunos de los cuales aceptaron el nombre. Dado que el nombre es ahora obligatorio y esperamos que los alias se proporcionen con frecuencia para Option<T>, solo hay un único constructor. Acepta el nombre y una params matriz de alias.

Antes de la versión 2.0.0-beta5, Option<T> tenía un constructor que tomaba un nombre y una descripción. Debido a eso, el segundo argumento podría tratarse ahora como un alias en lugar de una descripción. Es el único cambio importante conocido en la API que no va a provocar un error del compilador.

El código antiguo que usó el constructor con una descripción debe actualizarse para usar el nuevo constructor que toma un nombre y alias y, a continuación, establecer la Description propiedad por separado. Por ejemplo:

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

Valores predeterminados y análisis personalizado

En 2.0.0-beta4, los usuarios podrían establecer valores predeterminados para opciones y argumentos mediante los SetDefaultValue métodos . Esos métodos aceptaron un object valor, que no era seguro para tipos y podrían provocar errores en tiempo de ejecución si el valor no era compatible con la opción o el tipo de argumento:

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

Además, algunos de los Option constructores y Argument aceptaron un delegado de análisis y un valor booleano que indica si el delegado era un analizador personalizado o un proveedor de valores predeterminado. Esto fue confuso.

Option<T> Las clases y Argument<T> ahora tienen una DefaultValueFactory propiedad que se puede usar para establecer un delegado al que se puede llamar para obtener el valor predeterminado de la opción o argumento. Este delegado se invoca cuando no se encuentra la opción o el argumento en la entrada de la línea de comandos analizada.

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

Argument<T> y Option<T> también incluyen una CustomParser propiedad que se puede usar para establecer un analizador personalizado para el símbolo:

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

Además, CustomParser acepta un delegado de tipo Func<ParseResult, T>, en lugar del delegado anterior ParseArgument . Esto y algunos otros delegados personalizados se quitaron para simplificar la API y reducir el número de tipos expuestos por la API, lo que reduce el tiempo de inicio invertido durante la compilación JIT.

Para obtener más ejemplos de cómo usar DefaultValueFactory y CustomParser, vea Cómo personalizar el análisis y la validación en System.CommandLine.

Separación de análisis e invocación

En 2.0.0-beta4, era posible separar el análisis y invocar comandos, pero no estaba claro cómo hacerlo. Command no expondía un Parse método, pero CommandExtensions proporcionaba Parsemétodos de extensión , Invokey InvokeAsync para Command. Esto fue confuso, ya que no estaba claro qué método usar y cuándo. Se realizaron los siguientes cambios para simplificar la API:

  • Command ahora expone un Parse método que devuelve un ParseResult objeto . Este método se usa para analizar la entrada de la línea de comandos y devolver el resultado de la operación de análisis. Además, deja claro que el comando no se invoca, sino que solo se analiza y solo de manera sincrónica.
  • ParseResult ahora expone los Invoke métodos y InvokeAsync que se pueden usar para invocar el comando. Esto hace evidente que el comando se invoca después del análisis y permite la invocación sincrónica y asincrónica.
  • La CommandExtensions clase se quitó, ya que ya no es necesaria.

Configuración

Antes de la versión 2.0.0-beta5, era posible personalizar el análisis, pero solo con algunos de los métodos públicos Parse . Había una Parser clase que expone dos constructores públicos: uno aceptando Command y otro aceptando .CommandLineConfiguration CommandLineConfiguration era inmutable y para crearlo, tenía que usar un patrón de generador expuesto por la CommandLineBuilder clase . Se realizaron los siguientes cambios para simplificar la API:

  • CommandLineConfiguration se hizo mutable y CommandLineBuilder se quitó. La creación de una configuración ahora es tan sencilla como crear una instancia de CommandLineConfiguration y establecer las propiedades que desea personalizar. Además, la creación de una nueva instancia de configuración es el equivalente de llamar al CommandLineBuildermétodo .UseDefaults
  • Todos Parse los métodos ahora aceptan un parámetro opcional CommandLineConfiguration que se puede usar para personalizar el análisis. Cuando no se proporciona, se usa la configuración predeterminada.
  • Parser se cambió el nombre a para CommandLineParser desambiguar de otros tipos de analizador para evitar conflictos de nombres. Puesto que no tiene estado, ahora es una clase estática con solo métodos estáticos. Expone dos Parse métodos de análisis: uno acepta un IReadOnlyList<string> args y otro acepta un string args. Este último usa CommandLineParser.SplitCommandLine (también público) para dividir la entrada de la línea de comandos en tokens antes de analizarla.

CommandLineBuilderExtensions también se quitó. Aquí se muestra cómo puede asignar sus métodos a las nuevas API:

  • CancelOnProcessTermination ahora es una propiedad de CommandLineConfiguration denominada ProcessTerminationTimeout. Está habilitado de forma predeterminada, con un tiempo de espera de 2s. Establézcalo en null para deshabilitarlo.

  • EnableDirectives, UseEnvironmentVariableDirective, UseParseDirectivey UseSuggestDirective se quitaron. Se introdujo un nuevo tipo de directiva y rootCommand ahora expone System.CommandLine.RootCommand.Directives la propiedad . Puede agregar, quitar e iterar directivas mediante esta colección. La directiva Suggest se incluye de forma predeterminada; También puede usar otras directivas como DiagramDirective o EnvironmentVariablesDirective.

  • EnableLegacyDoubleDashBehavior se quitó. Ahora todos los tokens no coincidentes se exponen mediante la propiedad ParseResult.UnmatchedTokens .

  • EnablePosixBundling se quitó. La agrupación ahora está habilitada de forma predeterminada, puede deshabilitarla estableciendo la propiedad falseCommandLineConfiguration.EnableBundling en .

  • RegisterWithDotnetSuggest se quitó al realizar una operación costosa, normalmente durante el inicio de la aplicación. Ahora debe registrar comandos con dotnet suggestmanualmente.

  • UseExceptionHandler se quitó. El controlador de excepciones predeterminado ahora está habilitado de forma predeterminada, puede deshabilitarlo estableciendo la propiedad CommandLineConfiguration.EnableDefaultExceptionHandler en false. Esto resulta útil cuando desea controlar excepciones de forma personalizada, simplemente encapsulando los Invoke métodos o InvokeAsync en un bloque try-catch.

  • UseHelp y UseVersion se quitaron. Los tipos públicos HelpOption y VersionOption exponen la ayuda y la versión. Ambos se incluyen de forma predeterminada en las opciones definidas por RootCommand.

  • UseHelpBuilder se quitó. Para obtener más información sobre cómo personalizar la salida de ayuda, vea Cómo personalizar la ayuda en System.CommandLine.

  • AddMiddleware se quitó. Ralentiza el inicio de la aplicación y las características se pueden expresar sin ella.

  • UseParseErrorReporting y UseTypoCorrections se quitaron. Los errores de análisis ahora se notifican de forma predeterminada al invocar ParseResult. Puede configurarlo mediante la ParseErrorAction propiedad expuesta por ParseResult.Action .

    ParseResult result = rootCommand.Parse("myArgs", config);
    if (result.Action is ParseErrorAction parseError)
    {
        parseError.ShowTypoCorrections = true;
        parseError.ShowHelp = false;
    }
    
  • UseLocalizationResources y LocalizationResources se quitaron. La dotnet CLI usó esta característica principalmente para agregar traducciones que faltan a System.CommandLine. Todas esas traducciones se movieron a sí System.CommandLine misma, por lo que esta característica ya no es necesaria. Si falta soporte técnico para su idioma, notifique un problema.

  • UseTokenReplacer se quitó. Los archivos de respuesta están habilitados de forma predeterminada, pero puede deshabilitarlos estableciendo la System.CommandLine.CommandLineConfiguration.ResponseFileTokenReplacer propiedad nullen . También puede proporcionar una implementación personalizada para personalizar cómo se procesan los archivos de respuesta.

Por último, pero no menos importante, se quitaron las IConsole interfaces relacionadas (IStandardOut, IStandardError, IStandardIn). System.CommandLine.CommandLineConfiguration expone dos TextWriter propiedades: Output y Error. Se pueden establecer en cualquier TextWriter instancia, como , StringWriterque se puede usar para capturar la salida de las pruebas. Nuestra motivación era exponer menos tipos y reutilizar abstracciones existentes.

Invocación

En 2.0.0-beta4, la ICommandHandler interfaz expuesta Invoke y InvokeAsync los métodos que se usaron para invocar el comando analizado. Esto facilita la combinación de código sincrónico y asincrónico, por ejemplo, mediante la definición de un controlador sincrónico para un comando y, a continuación, invocarlo de forma asincrónica (lo que podría provocar un interbloqueo). Además, era posible definir un controlador solo para un comando, pero no para una opción (como ayuda, que muestra ayuda) o una directiva.

Una nueva clase System.CommandLine.CommandLineActionbase abstracta y dos clases derivadas: System.CommandLine.SynchronousCommandLineAction y System.CommandLine.AsynchronousCommandLineAction se han introducido. El primero se usa para acciones sincrónicas que devuelven un int código de salida, mientras que el último se usa para acciones asincrónicas que devuelven un Task<int> código de salida.

No es necesario crear un tipo derivado para definir una acción. Puede usar el System.CommandLine.Command.SetAction método para establecer una acción para un comando. La acción sincrónica puede ser un delegado que toma un System.CommandLine.ParseResult parámetro y devuelve un int código de salida (o nada) y, a continuación, se devuelve un código de salida predeterminado 0 . La acción asincrónica puede ser un delegado que toma un System.CommandLine.ParseResult parámetro y CancellationToken y devuelve Task<int> (o Task para obtener el código de salida predeterminado devuelto).

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

En el pasado, el CancellationToken objeto pasado InvokeAsync se expondía al controlador a través de un método de InvocationContext:

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

La mayoría de nuestros usuarios no obtenían este token y lo pasaban más. Hemos realizado CancellationToken un argumento obligatorio para las acciones asincrónicas, para que el compilador genere una advertencia cuando no se pasa más (CA2016).

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

Como resultado de estos y otros cambios con subproceso, la InvocationContext clase también se quitó. ParseResult Ahora se pasa directamente a la acción, por lo que puede acceder a los valores y opciones analizados directamente desde ella.

Para resumir estos cambios:

  • Se quitó la ICommandHandler interfaz. SynchronousCommandLineAction y AsynchronousCommandLineAction se introdujeron.
  • Se cambió el nombre del Command.SetHandler método a SetAction.
  • Se ha cambiado el nombre de la Command.Handler propiedad a Command.Action. Option se extendió con Option.Action.
  • InvocationContext se quitó. ParseResult Ahora se pasa directamente a la acción.

Para obtener más información sobre cómo usar acciones, vea Cómo analizar e invocar comandos en System.CommandLine.

Las ventajas de la API simplificada

Esperamos que los cambios realizados en la versión 2.0.0-beta5 hagan que la API sea más coherente, insonorizada y fácil de usar para los usuarios nuevos y existentes.

Los nuevos usuarios necesitan aprender menos conceptos y tipos, ya que el número de interfaces públicas disminuyó de 11 a 0 y las clases públicas (y estructuras) disminuyeron de 56 a 38. El recuento de métodos públicos se quitó de 378 a 235 y propiedades públicas de 118 a 99.

El número de ensamblados a los que hace System.CommandLine referencia se reduce de 11 a 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

Nos permitió reducir el tamaño de la biblioteca en 32% y el tamaño de la siguiente aplicación NativeAOT en 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 simplicidad también ha mejorado el rendimiento de la biblioteca (es un efecto secundario del trabajo, no el objetivo principal de ella). Las pruebas comparativas muestran que el análisis y la invocación de comandos ahora son más rápidos que en la versión 2.0.0-beta4, especialmente para comandos grandes con muchas opciones y argumentos. Las mejoras de rendimiento son visibles en escenarios sincrónicos y asincrónicos.

Para la aplicación más sencilla presentada anteriormente, obtuvimos los siguientes resultados:

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 |

Como puede ver, el tiempo de inicio (los puntos de referencia notifican el tiempo necesario para ejecutar un ejecutable determinado) ha mejorado en 12% en comparación con 2.0.0-beta4. Si compilamos la aplicación con NativeAOT, es solo 3 ms más lenta que una aplicación NativeAOT que no analiza los argumentos en absoluto (EmptyAOT en la tabla anterior). Además, cuando se excluye la sobrecarga de una aplicación vacía (63.58 ms), el análisis es de 40% más rápido que en 2.0.0-beta4 (22.22 ms frente a 13.66 ms).

Consulte también