Setembro de 2016

Volume 31 – Número 9

Essential .NET – Processamento da Linha de Comando com o .NET Core 1.0

De Mark Michaelis

Mark MichaelisNa coluna desse mês do Essential .NET, continuo minha pesquisa sobre os diversos recursos do .NET Core e, dessa vez, com uma versão completa (e não uma versão beta ou uma versão Release Candidate). Vou me focar especificamente nos utilitários de linha de comando (que podem ser encontrados na biblioteca .NET Core em github.com/aspnet/Common) e como usá-los para analisar uma linha de comando. Confesso que estou particularmente empolgado de finalmente ver um suporte para linha de comando integrado no NET Core já que isso é algo que eu queria desde o .NET Framework 1.0. Espero que uma biblioteca interna no .NET Core ajude a padronizar, mesmo que seja um pouco, a estrutura/formato da linha de comando entre os programas. O padrão em si não é tão importante para mim desde que haja uma convenção que todos usem como padrão, em vez de cada um criar seu próprio padrão.

Uma Convenção para Linha de Comando

A maior parte da funcionalidade da linha de comando pode ser encontrada no pacote NuGet Microsoft.Extensions.CommandLineUtils. Uma classe CommandLineApplication está incluída no assembly e fornece uma análise da linha de comando com suporte para nomes curtos e longos para opções, valores (um ou mais) atribuídos com dois pontos ou com um sinal de igual e símbolos como -? para ajuda. Falando em ajuda, a classe inclui suporte para exibição automática do texto de ajuda. A Figura 1 mostra exemplos de linha de comando que seriam suportados.

Figura 1 Exemplo de Linhas de Comando

Opções Program.exe -f=Inigo, -l Montoya –hello –names Princess –names Buttercup

Opção -f com o valor “Inigo”

Opção -l com o valor “Montoya”

Opção –hello com o valor “on”

Opção –names com os valores “Princess” e “Buttercup”

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

Comando “hello”

Argumento “Inigo”

Argumento “Montoya”

Argumento Greetings com os valores “It,” “is,” “a,” “pleasure,” “to," “meet,” “you.”

Símbolos Program.exe -? Exibir ajuda

Conforme descrito a seguir, existem vários tipos de argumento, um dos quais chamado “Argument.” O uso excessivo do termo argumento para referir-se aos valores especificados na linha de comando versus os dados de configuração da linha de comando pode resultar em uma ambiguidade significativa. Portanto, para o restante do artigo, irei diferenciar entre um argumento genérico de um tipo qualquer, especificado após o nome do executável, e o tipo de argumento chamado “Argument” (com letra maiúscula) pelo uso de letras maiúsculas e minúsculas. Farei o mesmo para diferenciar os outros tipos de argumento, Opção e Comando, usando letra maiúscula em vez dos termos em letra minúscula que referem-se de forma genérica ao argumento. Tenha isso em mente pois será importante ao longo do restante do artigo.

A seguir, veja a descrição de cada um dos tipos de argumento:

  • Opções: As opções são identificadas por um nome, onde o nome é prefixado com um (-) ou dois (--) traços. Os nomes de opção são definidos por programação através do uso de modelos e um modelo pode incluir um ou mais dos três designadores a seguir: nome curto, nome longo, símbolo. Além disso, uma Opção pode ter um valor associado. Por exemplo, um modelo pode ser “-n | --name | -# <Full Name>,” permitindo que a opção de nome completo seja identificada por qualquer um dos três designadores. (No entanto, o modelo não precisa de todos os três designadores.) Observe que é o uso de um traço simples ou duplo que determina se um nome curto ou longo é especificado, independentemente do comprimento real do nome.
    Para associar um valor a uma opção, você pode usar um espaço ou o operador de atribuição (=). -f=Inigo e -l Montoya, portanto, são ambos exemplos de especificação de um valor de opção.
    Se forem usados números no modelo, eles serão parte do nome curto ou longo, não do símbolo.
  • Arguments: Os argumentos são identificados pela ordem em que aparecem e não pelo nome. Dessa forma, um valor na linha de comando que não está prefixado por um nome de opção é um argumento. O argumento ao qual o valor corresponde é baseado na ordem em que o argumento aparece (Opções e Comandos são excluídos da contagem).
  • Comandos: Os comandos oferecem um agrupamento de argumentos e opções. Por exemplo, você pode ter um nome de comando “hello” seguido de uma combinação de Argumentos e Opções (ou até sub-Comandos). Os comandos são identificados por uma palavra-chave configurada, o nome do comando que agrupa todos os valores que aparecem depois do nome do comando para formar a definição do Comando.

Configuração da Linha de Comando

Programar a linha de comando após referenciar o Microsoft.Extensions.CommandLineUtils do .NET Core começa com a classe CommandLineApplication. Com essa classe você pode configurar cada Comando, Opção e Argumento. Após criar uma instância do CommandLineApplication, o construtor possui um valor booliano opcional que configura a linha de comando para jogar uma exceção (padrão) se um argumento parecer que não foi especificamente configurado.

Após a instância do CommandLineApplication ser criada, você configura os argumentos usando os métodos Opção, Argumento e Comando. Imagine, por exemplo, que você deseja ter suporte para uma sintaxe de linha de comando conforme a seguir, onde os itens entre colchetes são opcionais e aqueles entre parênteses são valores ou argumentos especificados pelo usuário:

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

A Figura 2 configura a capacidade de análise básica.

Figura 2 Configuração da Linha de Comando

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

A configuração começa com CommandLineApplication

Para começar, crio uma instância para CommandLineApplication, especificando se a análise da linha de comando será mais rigorosa, throwOnUnexpectedArg é true, ou menos rigorosa. Se eu especificar para jogar uma exceção quando um argumento não for esperado, todos os argumentos deverão ser explicitamente configurados. Alternativamente, se throwOnUnexpectedArg for false, então qualquer argumento que não seja reconhecido pela configuração será armazenado no campo CommandLineApplication.Remaining­Arguments.

Configuração de um Comando e seu Argumento

A próxima etapa na Figura 2 é configurar o Comando “name”. A palavra-chave que identificará o comando em uma lista de argumentos é o primeiro parâmetro da função Comando, name. O segundo parâmetro é um delegado de Action<CommandLineApplication> chamado configuração, no qual todos os sub-argumentos de nome Comando são configurados. Neste caso, existe somente um, que é um argumento do tipo CommandArgument com o nome de variável “greeting.” No entanto, é perfeitamente possível adicionar Argumentos, Opções e até sub-Comandos adicionais no delegado de configuração. Além disso, o parâmetro de destino do delegado, um CommandLineApplication, possui uma propriedade Pai que aponta de volta para commandLineArgument, o CommandLineArgument pai do destino sob o qual o Comando do nome é configurado.

Observe que ao configurar o Argumento dos nomes eu identifiquei especificamente que haverá suporte para multipleValues. Ao fazer isso, eu permiti que mais de um valor fosse especificado, vários nomes nesse caso. Cada um desses valores aparece após o identificador do argumento “name” até que outro identificador de argumento ou opção apareça. Os dois primeiros parâmetros da função Argumento são o nome, referindo-se ao nome do Argumento para que você possa identificá-lo em uma lista de Argumentos, e a descrição.

Uma última coisa a destacar na configuração do Comando nome é o fato de que você precisa salvar o retorno da função Argumento (e a função Opção caso exista uma). Isso é necessário para que você possa recuperar mais tarde os argumentos associados ao Argumento dos nomes. Se a referência não for salva, você acaba tendo que pesquisar na coleção commandLineApplication.Commands[0].Arguments para recuperar os dados do Argumento.

Uma forma elegante de salvar todos os dados da linha de comando é colocá-la em uma classe separada com atributos do repositório ASP.NET Scaffolding (github.com/aspnet/Scaffolding), especificamente a pasta src/Microsoft.VisualStudio.Web.CodeGeneration.Core/CommandLine folder. Para mais informações, consulte “Implementação de uma Classe de Linha de Comando com .NET Core” (bit.ly/296SluA).

Configuração de uma Opção

O argumento configurado a seguir na Figura 2 é a Opção de saudação, que é do tipo CommandOption. A configuração de uma Opção é feita através da função Opção, onde o primeiro parâmetro é um parâmetro de cadeia de caracteres chamado modelo. Observe que você pode especificar três nomes diferentes (por exemplo, -$, -g e -greeting) para a opção e cada uma desses nomes será usado para identificar a opção da lista de argumentos. Além disso, um modelo pode também especificar um valor associado a ele mesmo por meio de um nome entre parênteses após os identificadores da opção. Após o parâmetro de descrição, a função Opção inclui um parâmetro CommandOptionType exigido. Essa opção identifica:

  1. Se um valor pode ser especificado após o identificador da opção. Se um CommandOptionType do NoValue for especificado, então a função CommandOption.Value será definida para “on” se a opção aparecer na lista de argumentos. O valor “on” é retornado mesmo se um valor diferente for especificado após o identificador da opção e, na verdade, se um valor for especificado. Para ver um exemplo, veja a opção em letra maiúscula na Figura 2.
  2. Alternativamente, se CommandOptionType for SingleValue e o identificador da opção for especificado, mas nenhum valor for exibido, um CommandParsingException será jogado identificando que a opção não foi identificada, pois a mesma não corresponde ao modelo. Em outras palavras, SingleValue fornece um meio de verificar se o valor é fornecido, considerando que o identificador da opção é exibido.
  3. Por último, você pode fornecer um CommandOptionType de Multiple­Value. Ao contrário dos múltiplos valores associados a um comando, entretanto, múltiplos valores no caso de uma opção permitem que a mesma opção seja especificada várias vezes. Por exemplo, program.exe -name Inigo -name Montoya.

Observe que nenhuma das opções de configuração irá configurar então a opção é necessária. E, de fato, o mesmo é válido para um argumento. Para eliminar a chance de um valor não ser especificado, você deve verificar se a função HasValue informa um erro se a mesma retornar false. No caso de CommandArgument, a propriedade Valor retornará null se nenhum valor for especificado. Para informar o erro, considere exibir uma mensagem de erro seguida por um texto de ajuda para que os usuários possam ter mais informações sobre o que precisam fazer para corrigir o problema.

Outro comportamento importante do mecanismo de análise CommandLineApplication é que o mesmo diferencia letras maiúsculas de minúsculas. Na verdade, até o momento não existe uma opção de configuração fácil que permita que você não diferencie letras maiúsculas de minúsculas. Portanto, antes de qualquer outra coisa, você deverá alterar as letras maiúsculas e minúsculas dos argumentos realmente passados para CommandLineApplication (através do método Executar, conforme descreverei resumidamente) para fazer com que as letras maiúsculas e minúsculas não sejam diferenciadas. (Alternativamente, você pode tentar enviar uma solicitação pull github.com/aspnet/Common para ativar essa opção.)

Exibir Ajuda e Versão

Há uma função ShowHelp interna do CommandLineApplication que exibe o texto de ajuda associado à configuração da linha de comando automaticamente. Por exemplo, a Figura 3 mostra o resultado de ShowHelp para a Figura 2.

Figura 3 Exibição 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.

Infelizmente, a ajuda exibida não identifica se uma opção ou comando são opcionais. Em outras palavras, o texto de ajuda considera e exibe (através dos colchetes) todas as opções e comandos como sendo opcionais.

Apesar de você poder, por exemplo, chamar explicitamente o ShowHelp, ao tratar de um erro de linha de comando personalizada, o mesmo será invocado automaticamente sempre que um argumento correspondente ao modelo HelpOption for especificado. E o modelo HelpOption é especificado através de um argumento para o método CommandLineApplication.HelpOption.

Da mesma forma, existe um método ShowVersion para exibir a versão do seu aplicativo. Assim como ShowHelp, a configuração é feita de um de dois 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 ambos os métodos exigem a informação da versão que você deseja exibir para ser especificada ao chamar VerisionOption.

Análise e Leitura dos Dados da Linha de Comando

Até agora analisei detalhadamente como configurar o CommandLineApplication, mas ainda não discuti o processo sempre crítico de acionar a análise da linha de comando ou o que acontecerá imediatamente após a invocação da análise.

Para acionar a análise da linha de comando você precisa invocar a função CommandLineApplication.Execute e passar a lista de argumentos especificada na linha de comando. Na Figura 1, os argumentos são especificados no parâmetro args de Main e depois passados diretamente para a função Executar (lembre-se de primeiro tratar das letras maiúsculas e minúsculas se a diferenciação das mesmas não for desejada). É o método Executar que define os dados da linha de comando associados com cada Argumento e Opção configurados.

Observe que o CommandLineAppliction inclui uma função OnExecute(Func<int> invoke) na qual você passa um delegado Func<int> que será executado automaticamente após a conclusão da análise. Na Figura 2, o método OnExecute pega um delegado simples que verifica se o comando de saudação foi especificado antes de invocar uma função de Saudação.

Observe também que o int retornado do delegado invocado é uma forma de especificar o valor de retorno para Main. E, de fato, qualquer que seja o valor retornado da invocação corresponderá ao retorno de Executar. Além disso, como a análise é considerada uma operação relativamente lenta (suponho que isso seja relativo), Executar oferece suporte a uma sobrecarga que contenha Func<Task<int>>, permitindo, dessa forma, uma invocação assíncrona da análise da linha de comando.

Diretrizes: Comandos, Argumentos e Opções

Com esses três tipos de comandos disponíveis, vale a pena analisar rapidamente qual dever ser usado e quando.

Sempre use Comandos ao identificar semanticamente uma ação como compilar, importar ou fazer backup.

Sempre use Opções para habilitar informações de configuração para o programa como um todo ou para um comando específico.

Prefira um verbo para o nome de um comando e um adjetivo ou substantivo para o nome de uma opção (como -color, -parallel, -projectname).

Independentemente do tipo de argumento configurado, considere as diretrizes a seguir:

Sempre analise as letras maiúsculas e minúsculas dos nomes do identificador do argumento. Pode ser bastante confuso para um usuário que especifica -FullName ou -fullname quando a linha de comando está procurando por uma letra maiúscula ou minúscula.

Sempre grave os testes para análise da linha de comando. Métodos como Execute e OnExecute facilitam isso.

Sempre use Argumentos quando identificar argumentos específicos por nome não for conveniente ou quando múltiplos valores forem permitidos, mas prefixar cada um com um identificador de opção seja difícil.

Considere usar IntelliTect.AssertConsole (itl.tc/Command­LineUtils) para redirecionar a entrada e a saída do console para injetar e capturar o console para que o mesmo seja testado.

Existe um possível contratempo ao usar o CommandLineUtils do .NET Core. Ele pode estar em inglês e não está localizado. O texto mostrado em ShowHelp (junto com as mensagens de exceção que geralmente não estão localizadas), por exemplo, estão todos em inglês. Normalmente, isso pode não ser um problema, mas como uma linha de comando é parte de uma interface do aplicativo com o usuário, existe a possibilidade de existirem cenários em que o inglês como único idioma seja inaceitável. Por essa razão:

Considere escrever funções personalizadas para ShowHelp e ShowHint se a localização for importante.

Sempre analise CommandLineApplication.RemainingArguments quando o CommandLineApplication for configurado para não jogar exceções (throwOnUnexpectedArg = false).

Conclusão

Ao longo dos últimos três anos, o .NET Framework passou por grandes transições:

  • Agora possui suporte para plataforma cruzada, incluindo suporte para iOS, Android e Linux – Uau!!
  • Migrou de uma abordagem secreta e proprietária para o desenvolvimento de um módulo totalmente aberto, de software livre.
  • Houve uma refatoração significativa das APIS de BCL para a Biblioteca Padrão .NET para um plataforma altamente modular (cruzada) que pode ser usada para uma vasta gama de tipos de aplicativo disponíveis no mercado, sejam eles Software como Serviço, móveis, locais, Internet das Coisas, desktop e muito mais.
  • Houve um ressurgimento do .NET após a era do Windows 8 onde o mesmo foi ignorado sem muita estratégia ou sem um roteiro.

Tudo isso para dizer que se você ainda não começou a explorar o novo .NET Core 1.0, mais aprofundadamente, agora é um ótimo momento para fazê-lo, pois você terá um período de tempo maior para amortizar a curva de aprendizagem. Em outras palavras, se você está pensando em fazer uma atualização para versões mais recentes, faça isso agora. É bem provável que você passará pelo processo de atualização em algum momento e quanto mais cedo você o fizer, mais cedo você aproveitará as novas funcionalidades.


Mark Michaelis é fundador da IntelliTect, onde atua como arquiteto técnico principal e instrutor. Há quase 20 anos trabalha como Microsoft MVP, e é Diretor Regional da Microsoft desde 2007. Michaelis atua em diversas equipes de análise de design de software da Microsoft, incluindo C#, Microsoft Azure, SharePoint e Visual Studio ALM. Ele dá palestras em conferências de desenvolvedores e escreveu diversos livros, incluindo o mais recente, "Essential C# 6.0 (5th Edition)" (itl.tc/­EssentialCSharp). Você pode contatá-lo pelo Facebook, em facebook.com/Mark.Michaelis, pelo seu blog IntelliTect.com/Mark, no Twitter: @markmichaelis ou pelo email mark@IntelliTect.com.

Agradecemos aos seguintes especialistas técnicos da IntelliTect pela revisão deste artigo: Phil Spokas e Michael Stokesbary