Erstellen Sie mit Picocli ein Java-Befehlszeilenprogramm

1. Einleitung

In diesem Tutorial nähern wir uns der Picocli- Bibliothek, mit der wir auf einfache Weise Befehlszeilenprogramme in Java erstellen können.

Zunächst erstellen wir einen Hello World-Befehl. Wir werden uns dann eingehend mit den wichtigsten Funktionen der Bibliothek befassen, indem wir teilweise den Befehl git reproduzieren .

2. Hallo Weltkommando

Beginnen wir mit etwas Einfachem: einem Hello World-Befehl!

Zuerst müssen wir die Abhängigkeit zum Picocli- Projekt hinzufügen :

 info.picocli picocli 3.9.6 

Wie wir sehen können, werden wir die Version 3.9.6 der Bibliothek verwenden, obwohl sich eine Version 4.0.0 im Aufbau befindet (derzeit im Alpha-Test verfügbar).

Nachdem die Abhängigkeit eingerichtet wurde, erstellen wir unseren Befehl Hello World. Dazu verwenden wir die Annotation @Command aus der Bibliothek :

@Command( name = "hello", description = "Says hello" ) public class HelloWorldCommand { }

Wie wir sehen können, kann die Annotation Parameter annehmen. Wir verwenden hier nur zwei davon. Sie dienen dazu, Informationen über den aktuellen Befehl und Text für die automatische Hilfemeldung bereitzustellen.

Im Moment können wir mit diesem Befehl nicht viel anfangen. Um es etwas zu tun, müssen wir hinzufügen Haupt Methode aufrufen die Bequemlichkeit CommandLine.run (Runnable, String []) Methode . Dies erfordert zwei Parameter: eine Instanz unseres Befehls, der daher die Runnable- Schnittstelle implementieren muss , und ein String- Array, das die Befehlsargumente (Optionen, Parameter und Unterbefehle) darstellt:

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

Wenn wir nun die Hauptmethode ausführen , werden wir sehen, dass die Konsole "Hello World!"

Wenn wir in ein Glas gepackt sind, können wir unseren Hello World-Befehl mit dem Java- Befehl ausführen :

java -cp "pathToPicocliJar;pathToCommandJar" com.baeldung.picoli.helloworld.HelloWorldCommand

Kein Wunder, dass dies auch die "Hallo Welt!" Zeichenfolge an die Konsole.

3. Ein konkreter Anwendungsfall

Nachdem wir die Grundlagen gesehen haben, werden wir tief in die Picocli- Bibliothek eintauchen . Zu diesem Zweck werden wir teilweise einen beliebten Befehl reproduzieren: git .

Der Zweck besteht natürlich nicht darin, das Verhalten des Befehls git zu implementieren, sondern die Möglichkeiten des Befehls git zu reproduzieren - welche Unterbefehle existieren und welche Optionen für einen bestimmten Unterbefehl verfügbar sind.

Zuerst müssen wir eine GitCommand- Klasse erstellen, wie wir es für unseren Hello World-Befehl getan haben:

@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. Hinzufügen von Unterbefehlen

Der Befehl git bietet viele Unterbefehle - Hinzufügen, Festschreiben, Remote und viele mehr. Wir werden uns hier auf das Hinzufügen und Festschreiben konzentrieren .

Unser Ziel hier wird es also sein, diese beiden Unterbefehle dem Hauptbefehl zu deklarieren. Picocli bietet drei Möglichkeiten, um dies zu erreichen.

4.1. Verwenden der @ Befehlsanmerkung für Klassen

Die @Command Annotation bietet die Möglichkeit Subbefehle durch die Registrierung Subbefehle Parameter :

@Command( subcommands = { GitAddCommand.class, GitCommitCommand.class } )

In unserem Fall fügen wir zwei neue Klassen hinzu: GitAddCommand und GitCommitCommand . Beide sind mit @Command versehen und implementieren Runnable . Es ist wichtig, ihnen einen Namen zu geben, da die Namen von picocli verwendet werden, um zu erkennen, welche Unterbefehle ausgeführt werden sollen:

@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?"); } }

Wenn wir also unseren Hauptbefehl mit add als Argument ausführen , gibt die Konsole "Hinzufügen einiger Dateien zum Staging-Bereich" aus .

4.2. Verwenden der @ Command Annotation zu Methoden

Eine andere Möglichkeit, Unterbefehle zu deklarieren, besteht darin , mit @Command annotierte Methoden zu erstellen , die diese Befehle in der GitCommand- Klasse darstellen :

@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?"); }

Auf diese Weise können wir unsere Geschäftslogik direkt in die Methoden implementieren und keine separaten Klassen erstellen, um damit umzugehen.

4.3. Programmgesteuertes Hinzufügen von Unterbefehlen

Schließlich bietet uns picocli die Möglichkeit, unsere Unterbefehle programmgesteuert zu registrieren. Dies ist etwas kniffliger, da wir ein CommandLine- Objekt erstellen müssen, das unseren Befehl umschließt, und dann die Unterbefehle hinzufügen müssen:

CommandLine commandLine = new CommandLine(new GitCommand()); commandLine.addSubcommand("add", new GitAddCommand()); commandLine.addSubcommand("commit", new GitCommitCommand());

Danach müssen wir unseren Befehl noch ausführen, aber wir können die CommandLine.run () -Methode nicht mehr verwenden . Jetzt müssen wir die parseWithHandler () -Methode für unser neu erstelltes C ommandLine- Objekt aufrufen :

commandLine.parseWithHandler(new RunLast(), args);

Wir sollten die Verwendung der RunLast- Klasse beachten , die picocli anweist , den spezifischsten Unterbefehl auszuführen. Picocli stellt zwei weitere Befehlshandler zur Verfügung : RunFirst und RunAll . Ersteres führt den obersten Befehl aus, während letzteres alle ausführt.

Bei Verwendung der Convenience-Methode CommandLine.run () wird standardmäßig der RunLast- Handler verwendet.

5. Verwalten von Optionen mithilfe der @ Option- Anmerkung

5.1. Option ohne Argument

Let's now see how to add some options to our commands. Indeed, we would like to tell our add command that it should add all modified files. To achieve that, we'll add a field annotated with the @Option annotation to our GitAddCommand class:

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

As we can see, the annotation takes a names parameter, which gives the different names of the option. Therefore, calling the add command with either -A or –all will set the allFiles field to true. So, if we run the command with the option, the console will show “Adding all files to the staging area”.

5.2. Option with an Argument

As we just saw, for options without arguments, their presence or absence is always evaluated to a boolean value.

However, it's possible to register options that take arguments. We can do this simply by declaring our field to be of a different type. Let's add a message option to our commit command:

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

Unsurprisingly, when given the message option, the command will show the commit message on the console. Later in the article, we'll cover which types are handled by the library and how to handle other types.

5.3. Option with Multiple Arguments

But now, what if we want our command to take multiple messages, as is done with the real git commit command? No worries, let's make our field be an array or a Collection, and we're pretty much done:

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

Now, we can use the message option multiple times:

commit -m "My commit is great" -m "My commit is beautiful"

However, we might also want to give the option only once and separate the different parameters by a regex delimiter. Hence, we can use the split parameter of the @Option annotation:

@Option(names = {"-m", "--message"}, split = ",") private String[] messages;

Now, we can pass -m “My commit is great”,”My commit is beautiful” to achieve the same result as above.

5.4. Required Option

Sometimes, we might have an option that is required. The required argument, which defaults to false, allows us to do that:

@Option(names = {"-m", "--message"}, required = true) private String[] messages;

Now it's impossible to call the commit command without specifying the message option. If we try to do that, picocli will print an error:

Missing required option '--message=' Usage: git commit -m= [-m=]... -m, --message=

6. Managing Positional Parameters

6.1. Capture Positional Parameters

Now, let's focus on our add command because it's not very powerful yet. We can only decide to add all files, but what if we wanted to add specific files?

We could use another option to do that, but a better choice here would be to use positional parameters. Indeed, positional parameters are meant to capture command arguments that occupy specific positions and are neither subcommands nor options.

In our example, this would enable us to do something like:

add file1 file2

In order to capture positional parameters, we'll make use of the @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")); } }

Now, our command from earlier would print:

Adding file1 to the staging area Adding file2 to the staging area

6.2. Capture a Subset of Positional Parameters

It's possible to be more fine-grained about which positional parameters to capture, thanks to the index parameter of the annotation. The index is zero-based. Thus, if we define:

@Parameters(index="2..*")

This would capture arguments that don't match options or subcommands, from the third one to the end.

The index can be either a range or a single number, representing a single position.

7. A Word About Type Conversion

As we've seen earlier in this tutorial, picocli handles some type conversion by itself. For example, it maps multiple values to arrays or Collections, but it can also map arguments to specific types like when we use the Path class for the add command.

As a matter of fact, picocli comes with a bunch of pre-handled types. This means we can use those types directly without having to think about converting them ourselves.

However, we might need to map our command arguments to types other than those that are already handled. Fortunately for us, this is possible thanks to the ITypeConverter interface and the CommandLine#registerConverter method, which associates a type to a converter.

Let's imagine we want to add the config subcommand to our git command, but we don't want users to change a configuration element that doesn't exist. So, we decide to map those elements to an enum:

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

Plus, in our newly created GitConfigCommand class, let's add two positional parameters:

@Parameters(index = "0") private ConfigElement element; @Parameters(index = "1") private String value; @Override public void run() { System.out.println("Setting " + element.value() + " to " + value); }

This way, we make sure that users won't be able to change non-existent configuration elements.

Finally, we have to register our converter. What's beautiful is that, if using Java 8 or higher, we don't even have to create a class implementing the ITypeConverter interface. 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);

This happens in the GitCommand main() method. Note that we had to let go of the convenience CommandLine.run() method.

When used with an unhandled configuration element, the command would show the help message plus a piece of information telling us that it wasn't possible to convert the parameter to a ConfigElement:

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. Integrating with Spring Boot

Finally, let's see how to Springify all that!

Indeed, we might be working within a Spring Boot environment and want to benefit from it in our command-line program. In order to do that, we must create a SpringBootApplicationimplementing 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) { } }

Plus, let's annotate all our commands and subcommands with the Spring @Component annotation and autowire all that in our Application:

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

Note that we had to autowire every subcommand. Unfortunately, this is because, for now, picocli is not yet able to retrieve subcommands from the Spring context when declared declaratively (with annotations). Thus, we'll have to do that wiring ourselves, in a programmatic way:

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

And now, our command line program works like a charm with Spring components. Therefore, we could create some service classes and use them in our commands, and let Spring take care of the dependency injection.

9. Conclusion

In this article, we've seen some key features of the picocli library. We've learned how to create a new command and add some subcommands to it. We've seen many ways to deal with options and positional parameters. Plus, we've learned how to implement our own type converters to make our commands strongly typed. Finally, we've seen how to bring Spring Boot into our commands.

Of course, there are many things more to discover about it. The library provides complete documentation.

As for the full code of this article, it can be found on our GitHub.