Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
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 deArgument<T>
,Option<T>
yCommand
. En el caso deArgument<T>
, no se usa para el análisis, sino para generar la ayuda. En el caso deOption<T>
yCommand
, se usa para identificar el símbolo durante el análisis y también para obtener ayuda y finalizaciones. - La
Symbol.Name
propiedad yavirtual
no 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ó yOption.Name
ya no quita ,--
-
o/
cualquier otro prefijo del alias más largo. - La
Aliases
propiedad expuesta porOption
yCommand
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 yCommand
Option
).
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 Parse
métodos de extensión , Invoke
y 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 unParse
método que devuelve unParseResult
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 losInvoke
métodos yInvokeAsync
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 yCommandLineBuilder
se quitó. La creación de una configuración ahora es tan sencilla como crear una instancia deCommandLineConfiguration
y establecer las propiedades que desea personalizar. Además, la creación de una nueva instancia de configuración es el equivalente de llamar alCommandLineBuilder
método .UseDefaults
- Todos
Parse
los métodos ahora aceptan un parámetro opcionalCommandLineConfiguration
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 paraCommandLineParser
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 dosParse
métodos de análisis: uno acepta unIReadOnlyList<string> args
y otro acepta unstring args
. Este último usaCommandLineParser.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 deCommandLineConfiguration
denominada ProcessTerminationTimeout. Está habilitado de forma predeterminada, con un tiempo de espera de 2s. Establézcalo ennull
para deshabilitarlo.EnableDirectives
,UseEnvironmentVariableDirective
,UseParseDirective
yUseSuggestDirective
se quitaron. Se introdujo un nuevo tipo de directiva y rootCommand ahora exponeSystem.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 oEnvironmentVariablesDirective
.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 propiedadfalse
CommandLineConfiguration.EnableBundling en .RegisterWithDotnetSuggest
se quitó al realizar una operación costosa, normalmente durante el inicio de la aplicación. Ahora debe registrar comandos condotnet suggest
manualmente.UseExceptionHandler
se quitó. El controlador de excepciones predeterminado ahora está habilitado de forma predeterminada, puede deshabilitarlo estableciendo la propiedad CommandLineConfiguration.EnableDefaultExceptionHandler enfalse
. Esto resulta útil cuando desea controlar excepciones de forma personalizada, simplemente encapsulando losInvoke
métodos oInvokeAsync
en un bloque try-catch.UseHelp
yUseVersion
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
yUseTypoCorrections
se quitaron. Los errores de análisis ahora se notifican de forma predeterminada al invocarParseResult
. Puede configurarlo mediante laParseErrorAction
propiedad expuesta porParseResult.Action
.ParseResult result = rootCommand.Parse("myArgs", config); if (result.Action is ParseErrorAction parseError) { parseError.ShowTypoCorrections = true; parseError.ShowHelp = false; }
UseLocalizationResources
yLocalizationResources
se quitaron. Ladotnet
CLI usó esta característica principalmente para agregar traducciones que faltan aSystem.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 laSystem.CommandLine.CommandLineConfiguration.ResponseFileTokenReplacer
propiedadnull
en . 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 , StringWriter
que 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.CommandLineAction
base 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
yAsynchronousCommandLineAction
se introdujeron. - Se cambió el nombre del
Command.SetHandler
método aSetAction
. - Se ha cambiado el nombre de la
Command.Handler
propiedad aCommand.Action
.Option
se extendió conOption.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).