Uma CLI com Spring Shell

Uma CLI com Spring Shell

1. Visão geral

Simplificando, o Spring Shellproject fornece um shell interativo para processar comandos e construir uma CLI completa usando o modelo de programação Spring.

Neste artigo, vamos explorar seus recursos, classes principais e anotações, e implementar vários comandos personalizados e personalizações.

2. Dependência do Maven

Primeiro, precisamos adicionar a dependênciaspring-shell ao nossopom.xml:


    org.springframework.shell
    spring-shell
    1.2.0.RELEASE

A versão mais recente deste artefato pode ser encontradahere.

3. Acessando o Shell

Existem duas maneiras principais de acessar o shell em nossos aplicativos.

O primeiro é inicializar o shell no ponto de entrada do nosso aplicativo e permitir que o usuário insira os comandos:

public static void main(String[] args) throws IOException {
    Bootstrap.main(args);
}

A segunda é obter umJLineShellComponente executar os comandos de forma programática:

Bootstrap bootstrap = new Bootstrap();
JLineShellComponent shell = bootstrap.getJLineShellComponent();
shell.executeCommand("help");

Usaremos a primeira abordagem, uma vez que é mais adequada para os exemplos neste artigo, no entanto, no código-fonte você pode encontrar casos de teste que usam a segunda forma.

4. Comandos

Já existem vários comandos integrados no shell, comoclear,help,exit, etc., que fornecem a funcionalidade padrão de cada CLI.

Os comandos personalizados podem ser expostos adicionando métodos marcados com a anotação@CliCommand dentro de um componente Spring implementando a interfaceCommandMarker.

Cada argumento desse método deve ser marcado com uma anotação@CliOption, se não fizermos isso, encontraremos vários erros ao tentar executar o comando.

4.1. Adicionando Comandos ao Shell

Primeiro, precisamos deixar o shell saber onde estão nossos comandos. Para isso, é necessário que o arquivoMETA-INF/spring/spring-shell-plugin.xml esteja presente em nosso projeto, aí podemos usar a funcionalidade de escaneamento de componentes do Spring:


    

Depois que os componentes são registrados e instanciados pelo Spring, eles são registrados com o analisador de shell e suas anotações são processadas.

Vamos criar dois comandos simples, um para pegar o conteúdo de um URL e exibi-lo e outro para salvar esse conteúdo em um arquivo:

@Component
public class SimpleCLI implements CommandMarker {

    @CliCommand(value = { "web-get", "wg" })
    public String webGet(
      @CliOption(key = "url") String url) {
        return getContentsOfUrlAsString(url);
    }

    @CliCommand(value = { "web-save", "ws" })
    public String webSave(
      @CliOption(key = "url") String url,
      @CliOption(key = { "out", "file" }) String file) {
        String contents = getContentsOfUrlAsString(url);
        try (PrintWriter out = new PrintWriter(file)) {
            out.write(contents);
        }
        return "Done.";
    }
}

Observe que podemos passar mais de uma string para os atributosvalueekey de@CliCommande@CliOption respectivamente, o que nos permite expor vários comandos e argumentos que se comportam da mesma forma .

Agora, vamos verificar se tudo está funcionando conforme o esperado:

spring-shell>web-get --url https://www.google.com
web-save --url https://www.google.com --out contents.txt
Done.

4.2. Disponibilidade de comandos

Podemos usar a anotação@CliAvailabilityIndicator em um método retornando umboolean para alterar, em tempo de execução, se um comando deve ser exposto no shell.

Primeiro, vamos criar um método para modificar a disponibilidade do comandoweb-save:

private boolean adminEnableExecuted = false;

@CliAvailabilityIndicator(value = "web-save")
public boolean isAdminEnabled() {
    return adminEnableExecuted;
}

Agora, vamos criar um comando para alterar a variáveladminEnableExecuted:

@CliCommand(value = "admin-enable")
public String adminEnable() {
    adminEnableExecuted = true;
    return "Admin commands enabled.";
}

Finalmente, vamos verificar:

spring-shell>web-save --url https://www.google.com --out contents.txt
Command 'web-save --url https://www.google.com --out contents.txt'
  was found but is not currently available
  (type 'help' then ENTER to learn about this command)
spring-shell>admin-enable
Admin commands enabled.
spring-shell>web-save --url https://www.google.com --out contents.txt
Done.

4.3. Argumentos necessários

Por padrão, todos os argumentos de comando são opcionais. No entanto, podemos torná-los obrigatórios com o atributomandatory da anotação@CliOption:

@CliOption(key = { "out", "file" }, mandatory = true)

Agora, podemos testar que, se não o introduzirmos, resultará em um erro:

spring-shell>web-save --url https://www.google.com
You should specify option (--out) for this command

4.4. Argumentos padrão

Um valorkey vazio para@CliOption torna esse argumento o padrão. Lá, receberemos os valores introduzidos no shell que não fazem parte de nenhum argumento nomeado:

@CliOption(key = { "", "url" })

Agora, vamos verificar se funciona conforme o esperado:

spring-shell>web-get https://www.google.com

4.5. Ajudando Usuários

As anotações@CliCommande@CliOption fornecem um atributohelp que nos permite orientar nossos usuários ao usar o comandohelp integrado ou ao tabular para obter o preenchimento automático.

Vamos modificar nossoweb-get para adicionar mensagens de ajuda personalizadas:

@CliCommand(
  // ...
  help = "Displays the contents of an URL")
public String webGet(
  @CliOption(
    // ...
    help = "URL whose contents will be displayed."
  ) String url) {
    // ...
}

Agora, o usuário pode saber exatamente o que nosso comando faz:

spring-shell>help web-get
Keyword:                    web-get
Keyword:                    wg
Description:                Displays the contents of a URL.
  Keyword:                  ** default **
  Keyword:                  url
    Help:                   URL whose contents will be displayed.
    Mandatory:              false
    Default if specified:   '__NULL__'
    Default if unspecified: '__NULL__'

* web-get - Displays the contents of a URL.
* wg - Displays the contents of a URL.

5. Costumização

Existem três maneiras de personalizar o shell implementando as interfacesBannerProvider,PromptProvidereHistoryFileNameProvider, todas com implementações padrão já fornecidas.

Além disso, precisamos usar a anotação@Order para permitir que nossos provedores tenham precedência sobre essas implementações.

Vamos criar um novo banner para começar nossa personalização:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleBannerProvider extends DefaultBannerProvider {

    public String getBanner() {
        StringBuffer buf = new StringBuffer();
        buf.append("=======================================")
            .append(OsUtils.LINE_SEPARATOR);
        buf.append("*          example Shell             *")
            .append(OsUtils.LINE_SEPARATOR);
        buf.append("=======================================")
            .append(OsUtils.LINE_SEPARATOR);
        buf.append("Version:")
            .append(this.getVersion());
        return buf.toString();
    }

    public String getVersion() {
        return "1.0.1";
    }

    public String getWelcomeMessage() {
        return "Welcome to example CLI";
    }

    public String getProviderName() {
        return "example Banner";
    }
}

Observe que também podemos alterar o número da versão e a mensagem de boas-vindas.

Agora, vamos mudar o prompt:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimplePromptProvider extends DefaultPromptProvider {

    public String getPrompt() {
        return "example-shell";
    }

    public String getProviderName() {
        return "example Prompt";
    }
}

Finalmente, vamos modificar o nome do arquivo de histórico:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleHistoryFileNameProvider
  extends DefaultHistoryFileNameProvider {

    public String getHistoryFileName() {
        return "example-shell.log";
    }

    public String getProviderName() {
        return "example History";
    }

}

O arquivo histórico registrará todos os comandos executados no shell e será colocado ao lado de nosso aplicativo.

Com tudo instalado, podemos chamar nosso shell e vê-lo em ação:

=======================================
*          example Shell             *
=======================================
Version:1.0.1
Welcome to example CLI
example-shell>

6. Conversores

Até agora, usamos apenas tipos simples como argumentos para nossos comandos. Tipos comuns, comoInteger,Date,Enum,File, etc., têm um conversor padrão já registrado.

Ao implementar a interfaceConverter, também podemos adicionar nossos conversores para receber objetos personalizados.

Vamos criar um conversor que pode transformar umString em umURL:

@Component
public class SimpleURLConverter implements Converter {

    public URL convertFromText(
      String value, Class requiredType, String optionContext) {
        return new URL(value);
    }

    public boolean getAllPossibleValues(
      List completions,
      Class requiredType,
      String existingData,
      String optionContext,
      MethodTarget target) {
        return false;
    }

    public boolean supports(Class requiredType, String optionContext) {
        return URL.class.isAssignableFrom(requiredType);
    }
}

Finalmente, vamos modificar nossos comandosweb-get eweb-save:

public String webSave(... URL url) {
    // ...
}

public String webSave(... URL url) {
    // ...
}

Como você deve ter adivinhado, os comandos se comportam da mesma forma.

7. Conclusão

Neste artigo, vimos brevemente os principais recursos do projeto Spring Shell. Conseguimos contribuir com nossos comandos e personalizar o shell com nossos fornecedores, alteramos a disponibilidade dos comandos de acordo com as diferentes condições de tempo de execução e criamos um conversor de tipos simples.

O código-fonte completo para este artigo pode ser encontradoover on GitHub.