Septiembre de 2016

Volumen 31, número 9

Essential .NET: procesamiento de la línea de comandos con .NET Core 1.0

Por Mark Michaelis

Mark MichaelisEn la columna de esta semana de Essential .NET, continúo con mi investigación sobre las distintas características de .NET Core, esta vez con una versión completamente publicada (y no en beta o en el eufemismo versión candidata para lanzamiento). En concreto, me centraré en sus utilidades de la línea de comandos (que se encuentran dentro de la biblioteca Common de .NET Core de github.com/aspnet/Common) y en cómo se pueden aprovechar para analizar una línea de comandos. Debo confesar que estoy entusiasmado porque por fin se ha integrado la compatibilidad con análisis de la línea de comandos en .NET Core, que es algo que he deseado desde .NET Framework 1.0. Espero que una biblioteca integrada de .NET Core pueda ayudar a estandarizar, aunque solo sea un poquito, la estructura y el formato de la línea de comandos entre programas. No considero que los detalles de cómo es el estándar sean tan importantes como que haya una convención que la gente pueda seguir de forma predeterminada, en lugar de que cada uno cree la suya propia.

Una convención de línea de comandos

El grueso de la funcionalidad de la línea de comandos se encuentra dentro del paquete NuGet Microsoft.Extensions.CommandLineUtils. Se incluye en el ensamblado una clase CommandLineApplication que ofrece análisis de la línea de comandos con compatibilidad con nombres cortos y largos para opciones, valores (uno o varios) asignados a un signo de dos puntos o bien a un signo igual, y símbolos como -? para la ayuda. Y hablando de la ayuda, esta clase incluye compatibilidad para mostrar el texto de ayuda automáticamente. En la Figura 1 se muestran algunas líneas de comandos de ejemplo que se admitirían.

Figura 1. Líneas de comandos de ejemplo

Opciones Program.exe -f=Inigo, -l Montoya –hello –names Princess –names Buttercup

Opción -f con el valor "Inigo"

Opción -l con el valor "Montoya"

Opción –hello con el valor "on"

Opción –names con los valores "Princess" y "Buttercup"

Comandos con argumentos Program.exe "hello", "Inigo", "Montoya", "It", "is", "a", "pleasure", "to", "meet", "you."

Comando "hello"

Argumento "Inigo"

Argumento "Montoya"

Argumento Greetings con los valores "It", "is", "a", "pleasure", "to", "meet", "you."

Símbolos Program.exe -? Mostrar ayuda

Como se detalla a continuación, existen muchos tipos de argumentos, uno de los cuales se denomina "Argument". En inglés, la sobrecarga del término argument para referirse a los valores especificados en la línea de comandos respecto a los datos de configuración de la línea de comandos puede generar una notable ambigüedad. Por lo tanto, y teniendo en cuenta que este artículo ha sido escrito originalmente en inglés, durante el resto del artículo se hará una distinción entre un argumento genérico de cualquier tipo (especificado después del nombre del ejecutable) y el tipo de argumento denominado "Argument" (que comienza en mayúscula), ya que es la forma de diferenciar las dos palabras en inglés (idioma en el que la palabra clave y el sustantivo genérico se escriben igual). De la misma manera, se diferenciarán los otros tipos de argumentos, Option y Command, mediante el uso de la inicial en mayúscula, mientras que los términos que de forma genérica se refieren al argumento se escribirán en minúscula. Esta será la nomenclatura que se utilizará en el resto del artículo, aunque en este artículo traducido al español no tiene la misma relevancia que en el original.

Cada uno de los tipos de argumentos se describe como se indica a continuación:

  • Option: los elementos Option se identifican mediante un nombre, que viene precedido de un guion simple (-) o doble (--). Los nombres de Option se definen mediante programación con plantillas, y una plantilla puede incluir uno o varios de los tres designadores siguientes: short name, long name, symbol. Además, un elemento Option puede tener un valor asociado a él. Por ejemplo, una plantilla podría ser "-n | --name | -# <Full Name>", que permitiría que cualquiera de los tres designadores identificase la opción de nombre completo (no obstante, la plantilla no necesita los tres designadores). Tenga en cuenta que será el uso de un guion simple o doble lo que determine si se especifica un nombre corto o largo, independientemente de la longitud real del nombre.
    Para asociar un valor con una opción, puede usar o bien un espacio o bien el operador de asignación (=). -f=Inigo y -l Montoya son, por tanto, ejemplos de la especificación del valor de una opción.
    Si se usan números en la plantilla, formarán parte de los nombres cortos o largos, no del símbolo.
  • Argument: los elementos Argument se identifican según el orden en que aparecen, en lugar de mediante un nombre. Por tanto, un valor en la línea de comandos que no vaya precedido de un nombre de opción es un argumento. El argumento al que corresponde el valor se determina en función del orden en que aparece (los elementos Option y Command no se incluyen en este recuento).
  • Command: los elementos Command ofrecen una agrupación de argumentos y opciones. Por ejemplo, puede tener un nombre de comando "hello" seguido de una combinación de elementos Argument y Option (o incluso de subelementos Command). Los elementos Command se identifican mediante una palabra clave configurada, el nombre del comando, que agrupa todos los valores que se incluyen después del nombre del comando para que forme parte de la definición de ese elemento Command.

Configuración de la línea de comandos

La programación de la línea de comandos después de hacer referencia a Microsoft.Extensions.CommandLineUtils de .NET Core comienza con la clase CommandLineApplication. Con esta clase podrá configurar cada elemento Command, Option y Argument. Cuando cree una instancia de CommandLineApplication, el constructor tendrá un valor booleano opcional que configure la línea de comandos para que genere una excepción (la predeterminada) si aparece un argumento que no se haya configurado de forma concreta.

Dada una instancia de CommandLineApplication, puede configurar los argumentos mediante los métodos Option, Argument y Command. Imagine, por ejemplo, que quiere admitir una sintaxis de la línea de comandos como se indica a continuación, en la que los elementos de los corchetes son opcionales y los que están entre corchetes angulares son argumentos o valores especificados por el usuario:

Program.exe <-g|--greeting|-$ <greeting>> [name <fullname>] 
     [-?|-h|--help] [-u|--uppercase]

En la Figura 2 se configura la funcionalidad de análisis básica.

Figura 2. Configuración de la línea de comandos

public static void Main(params string[] args)
{
    // Program.exe <-g|--greeting|-$ <greeting>> [name <fullname>]
    // [-?|-h|--help] [-u|--uppercase]
  CommandLineApplication commandLineApplication =
    new CommandLineApplication(throwOnUnexpectedArg: false);
  CommandArgument names = null;
  commandLineApplication.Command("name",
    (target) =>
      names = target.Argument(
        "fullname",
        "Enter the full name of the person to be greeted.",
        multipleValues: true));
  CommandOption greeting = commandLineApplication.Option(
    "-$|-g |--greeting <greeting>",
    "The greeting to display. The greeting supports"
    + " a format string where {fullname} will be "
    + "substituted with the full name.",
    CommandOptionType.SingleValue);
  CommandOption uppercase = commandLineApplication.Option(
    "-u | --uppercase", "Display the greeting in uppercase.",
    CommandOptionType.NoValue);
  commandLineApplication.HelpOption("-? | -h | --help");
  commandLineApplication.OnExecute(() =>
  {
    if (greeting.HasValue())
    {
      Greet(greeting.Value(), names.Values, uppercase.HasValue());
    }
    return 0;
  });
  commandLineApplication.Execute(args);
}
private static void Greet(
  string greeting, IEnumerable<string> values, bool useUppercase)
{
  Console.WriteLine(greeting);
}

Comienza con CommandLineApplication

Para empezar, crearé una instancia de CommandLineApplication, especificando si el análisis de la línea de comandos será estricto (si throwOnUnexpectedArg tiene un valor true) o relajado. Si especifico que se genere una excepción cuando no se espere un argumento, todos los argumentos deberán configurarse de forma explícita. Como alternativa, si throwOnUnexpectedArg tiene un valor false, los argumentos que la configuración no reconozca se almacenarán en el campo CommandLineApplication.Remaining­Arguments.

Configuración de un elemento Command y sus argumentos

El paso siguiente de la Figura 2 es la configuración del elemento Command "name". La palabra clave que identificará el comando dentro de una lista de argumentos es el primer parámetro de la función Command: name. El segundo parámetro es un delegado Action<CommandLineApplication> denominado configuration, dentro del cual se configuran todos los argumentos del elemento Command name. En este caso, solo existe uno, un elemento Argument de tipo CommandArgument con el nombre de variable "greeting". No obstante, es perfectamente posible agregar elementos adicionales Argument, Option e incluso subelementos Command, dentro del delegado configuration. Además, el parámetro target del delegado, un elemento CommandLineApplication, tiene una propiedad Parent que apunta de vuelta a commandLineArgument: el elemento CommandLineArgument principal de target en el que se configura el elemento Command name.

Observe que cuando configuro el elemento Argument names, identifico específicamente que admitirá multipleValues. Al hacerlo así, permito que se especifique más de un valor (varios nombres en este caso). Cada uno de estos valores aparece después del identificador de argumento "name" hasta que aparezca otro identificador de opción o argumento. Los dos primeros parámetros de la función Argument son name, que hace referencia al nombre del elemento Argument para que lo pueda identificar en una lista de elementos Argument, y description.

Una última cosa que merece la pena destacar de la configuración del elemento Command name es el hecho de que debe guardar el valor devuelto por la función Argument (y la función Option, si la hay). Esto es necesario para que se puedan recuperar más tarde los argumentos asociados con el elemento Argument names. Sin guardar una referencia, termina teniendo que buscar por la colección commandLineApplication.Commands[0].Arguments para recuperar los datos del elemento Argument.

Una forma elegante de guardar todos los datos de la línea de comandos es colocarlos en una clase separada que esté decorada con los atributos del repositorio Scaffolding de ASP.NET (github.com/aspnet/Scaffolding), en concreto la carpeta src/Microsoft.VisualStudio.Web.CodeGeneration.Core/CommandLine. Para más información, consulte "Implementing a Command-Line Class with .NET Core" (Implementación de una clase de la línea de comandos con .NET Core) en bit.ly/296SluA.

Configuración de una opción

El siguiente argumento que se configura en la Figura 2 es el elemento Option greeting, que es de tipo CommandOption. La configuración de un elemento Option se realiza mediante la función Option, en la que el primer parámetro es un parámetro de cadena denominado template. Observe que puede especificar tres nombres distintos (por ejemplo, -$, -g, y -greeting) para la opción y cada uno de ellos se usará para identificar la opción en la lista de argumentos. Además, una plantilla puede especificar de forma opcional un valor asociado a ella por medio de un nombre entre corchetes angulares a continuación de los identificadores de opción. Después del parámetro description, la función Option incluye un parámetro requerido CommandOptionType. Esta opción identifica:

  1. Si se debe especificar algún valor después del identificador de opción. Si se especifica un valor para CommandOptionType de NoValue, la función CommandOption.Value se establecerá en "on" si la opción aparece dentro de la lista de argumentos. El valor "on" se devuelve incluso si se especifica un valor distinto seguido del identificador de opción y, de hecho, si se especifica un valor. Para ver un ejemplo, eche un vistazo a la opción uppercase de la Figura 2.
  2. De forma alternativa, si el valor de CommandOptionType es SingleValue y se especifica el identificador de opción, pero no aparece un valor, se generará una excepción CommandParsingException que indicará que la opción no se identificó (porque no coincidía con la plantilla). En otras palabras, SingleValue ofrece una forma de comprobar que se ha proporcionado el valor, suponiendo que el identificador de opción siquiera aparezca.
  3. Por último, puede proporcionar un elemento CommandOptionType de Multiple­Value. No obstante, a diferencia de los múltiples valores asociados con un comando, cuando existen varios valores en el caso de una opción se permite que la misma opción se especifique varias veces. Por ejemplo, program.exe -name Inigo -name Montoya.

Observe que ninguna de las opciones de configuración se configurará de forma que la opción sea obligatoria. Y, en realidad, lo mismo se cumple para un argumento. Para indicar como error que no se ha especificado un valor, es necesario comprobar si la función HasValue notifica un error si devuelve un valor false. En el caso de un elemento CommandArgument, la propiedad Value devolverá null si no se especifica un valor. Para notificar el error, considere la posibilidad de mostrar un mensaje de error seguido del texto de ayuda, de forma que los usuarios tengan más información sobre lo que deben hacer para corregir el problema.

Otro comportamiento importante del mecanismo de análisis de CommandLineApplication es que distingue mayúsculas de minúsculas. Y, de hecho, en este momento no hay ninguna opción de configuración sencilla que permita que no distinga mayúsculas de minúsculas. Por lo tanto, deberá cambiar las mayúsculas y minúsculas de los propios argumentos que se han pasado a CommandLineApplication (mediante el método Execute, como detallaré en un momento) con anterioridad para conseguir que no se distingan mayúsculas de minúsculas (de forma alternativa, podría intentar enviar una solicitud de incorporación de cambios a github.com/aspnet/Common para habilitar esta opción).

Mostrar la ayuda y la versión

Integrada en CommandLineApplication se encuentra una función ShowHelp que muestra el texto de ayuda asociado con la configuración de la línea de comandos automáticamente. Por ejemplo, en la Figura 3 se muestra la salida de ShowHelp de la Figura 2.

Figura 3. Visualización de la salida de ShowHelp

Usage:  [options] [command]
Options:
  -$|-g |--greeting <greeting>  The greeting to display. 
                                The greeting supports a format string 
                                where {fullname} will be substituted 
                                with the full name.
  -u | --uppercase              Display the greeting in uppercase.
  -? | -h | --help              Show help information
Commands:
  name 
Use " [command] --help" for more information about a command.

Lamentablemente, la ayuda que se muestra no identifica si una opción o un comando son opcionales. En otras palabras, el texto de ayuda presupone y muestra (mediante corchetes) que todas las opciones y los comandos son opcionales.

Aunque puede llamar a ShowHelp de forma explícita, por ejemplo cuando se controla un error de la línea de comandos personalizado, se invocará automáticamente si se especifica un argumento que coincida con la plantilla HelpOption. Y la plantilla HelpOption se especifica mediante un argumento al método CommandLineApplication.HelpOption.

De manera similar, existe un método ShowVersion para mostrar la versión de la aplicación. Como ShowHelp, se configura mediante uno de dos métodos:

public CommandOption VersionOption(
  string template, string shortFormVersion, string longFormVersion = null).
public CommandOption VersionOption(
  string template, Func<string> shortFormVersionGetter,
  Func<string> longFormVersionGetter = null)

Observe que los dos métodos requieren que la información de la versión que quiere mostrar se especifique en la llamada a VersionOption.

Análisis y lectura de los datos de la línea de comandos

Hasta ahora he revisado en detalle cómo configurar el elemento CommandLineApplication, pero aún no he discutido el proceso tan crítico de desencadenar el análisis de la línea de comandos, ni lo que sucederá inmediatamente después de la invocación del análisis.

Para desencadenar el análisis de la línea de comandos, es necesario invocar la función CommandLineApplication.Execute y pasarle la lista de argumentos especificados en la línea de comandos. En la Figura 1, los argumentos se especifican en el parámetro args de Main, de forma que se pasan a la función Execute directamente (recuerde controlar primero si la distinción de mayúsculas y minúsculas no es deseable). Es el método Execute el que establece los datos de la línea de comandos asociados con cada elemento Argument y Option que se configure.

Observe que CommandLineAppliction incluye una función OnExecute(Func<int> invoke) a la que puede pasarle un delegado Func<int> que se ejecutará automáticamente cuando se complete el análisis. En la Figura 2, el método OnExecute acepta un delegado sencillo que comprueba que el comando greet se haya especificado antes invocar una función Greet.

Observe también que el valor entero devuelto por el delegado de invoke está diseñado como una forma de especificar un valor devuelto por Main. De hecho, cualquiera que sea el valor devuelto de invoke corresponderá al valor devuelto de Execute. Además, como el análisis se considera una operación relativamente lenta (supongo que todo es relativo), Execute admite una sobrecarga que acepta un elemento Func<Task<int>>, y por tanto habilita una invocación asincrónica del análisis de la línea de comandos.

Directrices: elementos Command, Argument y Option

Merece la pena revisar rápidamente cuál de los tres tipos de comandos disponibles se debe usar en cada ocasión.

Use elementos Command cuando identifique semánticamente una acción como compilar, importar o realizarcopiadeseguridad.

Use elementos Option para habilitar información de configuración para todo el programa o para un comando concreto.

Dé preferencia a un verbo para el nombre de un comando y a un adjetivo o un nombre para el nombre de una opción (como -color, -paralelo, -nombreproyecto).

Independientemente del tipo de argumento que configure, tenga presentes estas directrices:

Revise el uso de mayúsculas y minúsculas en los nombres de los indentificadores de argumentos. Podría ser muy confuso para un usuario que especifique -NombreCompleto o -nombrecompleto si la línea de comandos espera un uso de mayúsculas y minúsculas distinto.

Escriba comprobaciones para el análisis de la línea de comandos. Los métodos como Execute y OnExecute permiten que esta tarea sea relativamente sencilla.

Use elementos Argument cuando identificar argumentos particulares por su nombre sea engorroso, o cuando se permitan varios valores y agregar un prefijo a cada uno con un identificador de opción sea poco manejable.

Considere la posibilidad de hacer uso de IntelliTect.AssertConsole (itl.tc/Command­LineUtils) para redirigir la entrada y salida de la consola a fin de insertar y capturar la consola para comprobaciones.

Existe un posible inconveniente relacionado con el uso de Command­LineUtils de .NET Core y es que está basado en inglés y no está localizado. El texto para mostrar como el que se encuentra en ShowHelp (junto con los mensajes de excepción que normalmente no están localizados) está enteramente en inglés. Normalmente, esto podría no ser un problema, pero como una línea de comandos forma parte de la interfaz de una aplicación con el usuario, probablemente existirán escenarios en los que no sea aceptable que solo esté disponible en inglés. Por este motivo:

Considere la posibilidad de escribir funciones personalizadas para ShowHelp y ShowHint si la localización es importante.

Compruebe CommandLineApplication.RemainingArguments cuando CommandLineApplication se haya configurado para que no genere excepciones (throwOnUnexpectedArg = false).

Resumen

En los tres últimos años, .NET Framework ha pasado por varias transiciones de envergadura:

  • Ahora tiene compatibilidad multiplataforma, incluida compatibilidad con iOS, Android y Linux.
  • Ha migrado de un enfoque secreto y propietario a un desarrollo en un módulo completamente abierto (código abierto).
  • Se ha producido una significativa refactorización de las API de BCL de la .NET Standard Library a una (multi)plataforma altamente modular que puede aprovecharse en una amplia gama de tipos de aplicaciones disponibles, ya sea como software como servicio, móvil, local, Internet de las cosas, de escritorio y muchos otros.
  • Ha habido un resurgimiento de .NET, después de la era de Windows 8 en la que se mantuvo ignorado y la estrategia y la hoja de ruta eran prácticamente inexistentes.

Y esto es todo lo que tenía que contar, si aún no ha comenzado a indagar sobre el nuevo .NET Core 1.0, ahora es un momento idóneo para hacerlo, ya que obtendrá el mayor período de tiempo para amortizar la curva de aprendizaje. En otras palabras, si contempla la posibilidad de actualizar a él desde versiones anteriores, hágalo ahora. Hay bastantes probabilidades de que actualice en algún momento, y cuanto antes lo haga, antes podrá aprovechar sus nuevas características.


Mark Michaelis es el fundador de IntelliTect y trabaja de arquitecto técnico como jefe y formador. Durante casi dos décadas, ha sido MVP de Microsoft y director regional de Microsoft desde 2007. Michaelis trabaja con varios equipos de revisión de diseño de software de Microsoft, como C#, Microsoft Azure, SharePoint y Visual Studio ALM. Hace presentaciones en conferencias de desarrolladores y ha escrito varios libros, el más reciente de ellos "Essential C# 6.0 (5th Edition)" (itl.tc/EssentialCSharp). Póngase en contacto con él en Facebook en facebook.com/Mark.Michaelis, en su blog IntelliTect.com/Mark, en Twitter @markmichaelis o a través de la dirección de correo electrónico mark@IntelliTect.com.

Gracias a los siguientes expertos técnicos de IntelliTect por revisar este artículo: Phil Spokas y Michael Stokesbary