Criar um programa de linha de comando Java com Picocli
1. Introdução
Neste tutorial, vamos abordar opicocli library, que nos permite criar facilmente programas de linha de comando em Java.
Começaremos criando um comando Hello World. Em seguida, mergulharemos profundamente nos principais recursos da biblioteca, reproduzindo, parcialmente, o esquemagit .
2. Hello World Command
Vamos começar com algo fácil: um comando Hello World!
Em primeiro lugar, precisamos adicionar odependency to the picocli project:
info.picocli
picocli
3.9.6
Como podemos ver, usaremos a versão3.9.6 da biblioteca, embora uma versão4.0.0 esteja em construção (atualmente disponível em teste alfa).
Agora que a dependência está configurada, vamos criar nosso comando Hello World. Para fazer isso,we’ll use the @Command annotation from the library:
@Command(
name = "hello",
description = "Says hello"
)
public class HelloWorldCommand {
}
Como podemos ver, a anotação pode assumir parâmetros. Estamos usando apenas dois deles aqui. Seu objetivo é fornecer informações sobre o comando e texto atuais para a mensagem de ajuda automática.
No momento, não há muito que possamos fazer com este comando. Para fazer algo, precisamos adicionar um métodomain chamandothe convenience CommandLine.run(Runnable, String[]) method. Isso leva dois parâmetros: uma instância de nosso comando, que deve implementar a interfaceRunnable, e uma matrizString representando os argumentos do comando (opções, parâmetros e subcomandos):
public class HelloWorldCommand implements Runnable {
public static void main(String[] args) {
CommandLine.run(new HelloWorldCommand(), args);
}
@Override
public void run() {
System.out.println("Hello World!");
}
}
Agora, quando executarmos o métodomain, veremos que a saída do console“Hello World!”
When packaged to a jar, podemos executar nosso comando Hello World usando o comandojava:
java -cp "pathToPicocliJar;pathToCommandJar" com.example.picoli.helloworld.HelloWorldCommand
Sem surpresa, isso também envia a string“Hello World!” para o console.
3. Um caso de uso concreto
Agora que vimos o básico, vamos nos aprofundar na bibliotecapicocli. Para fazer isso, vamos reproduzir, parcialmente, um comando popular:git.
Claro, o propósito não será implementar o comportamento do comandogit, mas reproduzir as possibilidades do comandogit - quais subcomandos existem e quais opções estão disponíveis para um subcomando peculiar.
Primeiro, temos que criar uma classeGitCommand como fizemos para o nosso comando Hello World:
@Command
public class GitCommand implements Runnable {
public static void main(String[] args) {
CommandLine.run(new GitCommand(), args);
}
@Override
public void run() {
System.out.println("The popular git command");
}
}
4. Adicionando subcomandos
O scommandgit oferece muitoshttps://picocli.info/#subcommands[subcommands] — _add, commit, remote, e muitos mais. Vamos nos concentrar aqui emadd ecommit.
Portanto, nosso objetivo aqui será declarar esses dois subcomandos para o comando principal. Picocli oferece três maneiras de fazer isso.
4.1. Usando a anotação@Command nas aulas
The @Command annotation offers the possibility to register subcommands through the subcommands parameter:
@Command(
subcommands = {
GitAddCommand.class,
GitCommitCommand.class
}
)
Em nosso caso, adicionamos duas novas classes:GitAddCommandeGitCommitCommand. Ambos são anotados com@Commande implementamRunnable. It’s important to give them a name, as the names will be used by picocli to recognize which subcommand(s) to execute:
@Command(
name = "add"
)
public class GitAddCommand implements Runnable {
@Override
public void run() {
System.out.println("Adding some files to the staging area");
}
}
@Command(
name = "commit"
)
public class GitCommitCommand implements Runnable {
@Override
public void run() {
System.out.println("Committing files in the staging area, how wonderful?");
}
}
Portanto, se executarmos nosso comando principal comadd como argumento, o console produzirá“Adding some files to the staging area”.
4.2. Usando a anotação@Command em métodos
Outra maneira de declarar subcomandos écreate @Command-annotated methods representing those commands in the GitCommand class:
@Command(name = "add")
public void addCommand() {
System.out.println("Adding some files to the staging area");
}
@Command(name = "commit")
public void commitCommand() {
System.out.println("Committing files in the staging area, how wonderful?");
}
Dessa forma, podemos implementar diretamente nossa lógica de negócios nos métodos e não criar classes separadas para lidar com isso.
4.3. Adicionando subcomandos programaticamente
Finally, picocli offers us the possibility to register our subcommands programmatically. Este é um pouco mais complicado, pois temos que criar um objetoCommandLine envolvendo nosso comando e, em seguida, adicionar os subcomandos a ele:
CommandLine commandLine = new CommandLine(new GitCommand());
commandLine.addSubcommand("add", new GitAddCommand());
commandLine.addSubcommand("commit", new GitCommitCommand());
Depois disso, ainda temos que executar nosso comando, maswe can’t make use of the CommandLine.run() method anymore. Agora, temos que chamar o métodoparseWithHandler() em nosso objeto CommandLine recém-criado:
commandLine.parseWithHandler(new RunLast(), args);
Devemos observar o uso da classeRunLast, que diz apicocli para executar o subcomando mais específico. Existem dois outros manipuladores de comando fornecidos porpicocli:RunFirsteRunAll. O primeiro executa o comando superior, enquanto o último executa todos eles.
Ao usar o método de conveniênciaCommandLine.run(), o manipuladorRunLast é usado por padrão.
5. Gerenciando opções usando a anotação@Option
5.1. Opção sem argumento
Vamos agora ver como adicionar algumas opções aos nossos comandos. Na verdade, gostaríamos de dizer ao nosso comandoadd que ele deve adicionar todos os arquivos modificados. Para conseguir isso,we’ll add a field annotated with the https://picocli.info/#options[@Option_] annotation para nossa classeGitAddCommand:
@Option(names = {"-A", "--all"})
private boolean allFiles;
@Override
public void run() {
if (allFiles) {
System.out.println("Adding all files to the staging area");
} else {
System.out.println("Adding some files to the staging area");
}
}
Como podemos ver, a anotação leva um parâmetronames, que dá os diferentes nomes da opção. Portanto, chamar o comandoadd com-A ou–all definirá o campoallFiles paratrue. Portanto, se executarmos o comando com a opção, o console mostrará“Adding all files to the staging area”.
5.2. Opção com um argumento
Como acabamos de ver, para opções sem argumentos, sua presença ou ausência é sempre avaliada com um valor deboolean.
No entanto, é possível registrar opções que levam argumentos. We can do this simply by declaring our field to be of a different type. Vamos adicionar uma opçãomessage ao nosso comandocommit:
@Option(names = {"-m", "--message"})
private String message;
@Override
public void run() {
System.out.println("Committing files in the staging area, how wonderful?");
if (message != null) {
System.out.println("The commit message is " + message);
}
}
Sem surpresa, quando dada a opçãomessage, o comando mostrará a mensagem de confirmação no console. Posteriormente neste artigo, abordaremos quais tipos são gerenciados pela biblioteca e como lidar com outros tipos.
5.3. Opção com vários argumentos
Mas agora, e se quisermos que nosso comando receba várias mensagens, como é feito com o comandogit commit real? Não se preocupe,let’s make our field be an array or a Collection, e estamos praticamente prontos:
@Option(names = {"-m", "--message"})
private String[] messages;
@Override
public void run() {
System.out.println("Committing files in the staging area, how wonderful?");
if (messages != null) {
System.out.println("The commit message is");
for (String message : messages) {
System.out.println(message);
}
}
}
Agora, podemos usar a opçãomessage várias vezes:
commit -m "My commit is great" -m "My commit is beautiful"
No entanto, também podemos querer dar a opção apenas uma vez e separar os diferentes parâmetros por um delimitador de expressão regular. Portanto, podemos usar o parâmetrosplit da anotação@Option:
@Option(names = {"-m", "--message"}, split = ",")
private String[] messages;
Agora, podemos passar-m “My commit is great”,”My commit is beautiful” para obter o mesmo resultado acima.
5.4. Opção necessária
Às vezes, podemos ter uma opção necessária. O argumentorequired, cujo padrão éfalse, nos permite fazer isso:
@Option(names = {"-m", "--message"}, required = true)
private String[] messages;
Agora é impossível chamar o comandocommit sem especificar a opçãomessage. Se tentarmos fazer isso,picocli exibirá um erro:
Missing required option '--message='
Usage: git commit -m= [-m=]...
-m, --message=
6. Gerenciando parâmetros posicionais
6.1. Capturar parâmetros posicionais
Agora, vamos nos concentrar em nosso comandoadd porque ele ainda não é muito poderoso. Só podemos decidir adicionar todos os arquivos, mas e se quisermos adicionar arquivos específicos?
Poderíamos usar outra opção para fazer isso, mas uma escolha melhor aqui seria usar parâmetros posicionais. Na verdade,positional parameters are meant to capture command arguments that occupy specific positions and are neither subcommands nor options.
No nosso exemplo, isso nos permitiria fazer algo como:
add file1 file2
Para capturar os parâmetros posicionais,we’ll make use of the https://picocli.info/#positional_parameters[@Parameters_] annotation:
@Parameters
private List files;
@Override
public void run() {
if (allFiles) {
System.out.println("Adding all files to the staging area");
}
if (files != null) {
files.forEach(path -> System.out.println("Adding " + path + " to the staging area"));
}
}
Agora, nosso comando da versão anterior seria impresso:
Adding file1 to the staging area
Adding file2 to the staging area
6.2. Capturar um subconjunto de parâmetros posicionais
É possível ser mais refinado sobre quais parâmetros de posição capturar, graças ao parâmetroindex da anotação. O índice é baseado em zero. Assim, se definirmos:
@Parameters(index="2..*")
Isso capturaria argumentos que não correspondem a opções ou subcomandos, do terceiro ao final.
O índice pode ser um intervalo ou um único número, representando uma única posição.
7. Uma palavra sobre conversão de tipo
Como vimos anteriormente neste tutorial,picocli lida com alguma conversão de tipo por si só. Por exemplo, ele mapeia vários valores paraarrays ouCollections, mas também pode mapear argumentos para tipos específicos, como quando usamos a classePath para o comandoadd.
Na verdade,picocli vem coma bunch of pre-handled types. Isso significa que podemos usar esses tipos diretamente sem ter que pensar em convertê-los nós mesmos.
No entanto, talvez seja necessário mapear nossos argumentos de comando para outros tipos além daqueles que já foram tratados. Felizmente para nós,this is possible thanks to the https://picocli.info/#custom_type_converters[_ITypeConverter] interface and the CommandLine#registerConverter method, which associates a type to a converter.
Vamos imaginar que queremos adicionar o subcomandoconfig ao nosso comandogit, mas não queremos que os usuários alterem um elemento de configuração que não existe. Então, decidimos mapear esses elementos para uma enumeração:
public enum ConfigElement {
USERNAME("user.name"),
EMAIL("user.email");
private final String value;
ConfigElement(String value) {
this.value = value;
}
public String value() {
return value;
}
public static ConfigElement from(String value) {
return Arrays.stream(values())
.filter(element -> element.value.equals(value))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("The argument "
+ value + " doesn't match any ConfigElement"));
}
}
Além disso, em nossa classeGitConfigCommand recém-criada, vamos adicionar dois parâmetros posicionais:
@Parameters(index = "0")
private ConfigElement element;
@Parameters(index = "1")
private String value;
@Override
public void run() {
System.out.println("Setting " + element.value() + " to " + value);
}
Dessa forma, garantimos que os usuários não possam alterar os elementos de configuração inexistentes.
Finalmente, temos que registrar nosso conversor. O que é bonito é que, se estivermos usando Java 8 ou superior, não precisamos nem criar uma classe implementando a interfaceITypeConverter. We can just pass a lambda or method reference to the registerConverter() method:
CommandLine commandLine = new CommandLine(new GitCommand());
commandLine.registerConverter(ConfigElement.class, ConfigElement::from);
commandLine.parseWithHandler(new RunLast(), args);
Isso acontece no métodoGitCommand main(). Observe que tivemos que abandonar o método de conveniênciaCommandLine.run().
Quando usado com um elemento de configuração não manipulado, o comando mostraria a mensagem de ajuda mais uma informação informando que não foi possível converter o parâmetro para umConfigElement:
Invalid value for positional parameter at index 0 ():
cannot convert 'user.phone' to ConfigElement
(java.lang.IllegalArgumentException: The argument user.phone doesn't match any ConfigElement)
Usage: git config
8. Integrando com o Spring Boot
Finalmente, vamos ver como transformar tudo isso em Springify!
De fato, podemos estar trabalhando em um ambiente Spring Boot e queremos nos beneficiar dele em nosso programa de linha de comando. Para fazer isso,we must create a SpringBootApplication implementing the CommandLineRunner interface:
@SpringBootApplication
public class Application implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Override
public void run(String... args) {
}
}
Além disso,let’s annotate all our commands and subcommands with the Spring @Component annotatione autowire tudo isso em nossoApplication:
private GitCommand gitCommand;
private GitAddCommand addCommand;
private GitCommitCommand commitCommand;
private GitConfigCommand configCommand;
public Application(GitCommand gitCommand, GitAddCommand addCommand,
GitCommitCommand commitCommand, GitConfigCommand configCommand) {
this.gitCommand = gitCommand;
this.addCommand = addCommand;
this.commitCommand = commitCommand;
this.configCommand = configCommand;
}
Observe que tivemos que conectar automaticamente todos os subcomandos. Infelizmente, isso ocorre porque, por enquanto,picocli ainda não é capaz de recuperar subcomandos do contexto Spring quando declarado declarativamente (com anotações). Portanto, teremos que fazer essa fiação nós mesmos, de forma programática:
@Override
public void run(String... args) {
CommandLine commandLine = new CommandLine(gitCommand);
commandLine.addSubcommand("add", addCommand);
commandLine.addSubcommand("commit", commitCommand);
commandLine.addSubcommand("config", configCommand);
commandLine.parseWithHandler(new CommandLine.RunLast(), args);
}
E agora, nosso programa de linha de comando funciona como um encanto com os componentes Spring. Portanto, poderíamos criar algumas classes de serviço e usá-las em nossos comandos, e deixar o Spring cuidar da injeção de dependência.
9. Conclusão
Neste artigo, vimos alguns recursos principais da bibliotecapicocli . Aprendemos como criar um novo comando e adicionar alguns subcomandos a ele. Vimos muitas maneiras de lidar com opções e parâmetros posicionais. Além disso, aprendemos como implementar nossos próprios conversores de tipo para tornar nossos comandos fortemente tipados. Finalmente, vimos como trazer Spring Boot para nossos comandos.
Claro, há muito mais a descobrir sobre isso. A biblioteca fornececomplete documentation.
Quanto ao código completo deste artigo, ele pode ser encontrado emour GitHub.