Перейти к основному содержимому

Создайте программу командной строки Java с помощью Picocli

· 11 мин. чтения

1. Введение

В этом руководстве мы рассмотрим библиотеку picocli , которая позволяет нам легко создавать программы командной строки на Java.

Сначала мы начнем с создания команды Hello World. Затем мы углубимся в ключевые функции библиотеки, частично воспроизведя команду git .

2. Привет, мировая команда

Начнем с чего-то простого: команды Hello World!

Прежде всего, нам нужно добавить зависимость к проекту picocli :

<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>3.9.6</version>
</dependency>

Как мы видим, мы будем использовать версию библиотеки 3.9.6 , хотя версия 4.0.0 находится в стадии разработки (в настоящее время доступна в альфа-тестировании).

Теперь, когда зависимость настроена, давайте создадим нашу команду Hello World. Для этого воспользуемся аннотацией @Command из библиотеки :

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

Как мы видим, аннотация может принимать параметры. Здесь мы используем только два из них. Их цель — предоставить информацию о текущей команде и тексте автоматического справочного сообщения.

На данный момент мы мало что можем сделать с этой командой. Чтобы заставить его что-то делать, нам нужно добавить основной метод, вызывающий удобный метод CommandLine.run(Runnable, String[]) . Он принимает два параметра: экземпляр нашей команды, которая, таким образом, должна реализовать интерфейс Runnable , и массив строк , представляющий аргументы команды (опции, параметры и подкоманды):

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

Теперь, когда мы запустим метод main , мы увидим, что консоль выводит «Hello World!»

После упаковки в банку мы можем запустить нашу команду Hello World с помощью команды java :

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

Неудивительно, что это также выводит «Hello World!» строку в консоль.

3. Конкретный вариант использования

Теперь, когда мы ознакомились с основами, мы углубимся в библиотеку picocli . Для этого мы частично воспроизведем популярную команду: git .

Конечно, цель будет заключаться не в том, чтобы реализовать поведение команды git , а в том, чтобы воспроизвести возможности команды git — какие подкоманды существуют и какие параметры доступны для конкретной подкоманды.

Во-первых, мы должны создать класс GitCommand , как мы сделали для нашей команды 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. Добавление подкоманд

Команда git предлагает множество подкомандadd, commit, remote и многие другие. Здесь мы сосредоточимся на добавлении и фиксации .

Итак, наша цель здесь будет состоять в том, чтобы объявить эти две подкоманды основной команде. Picocli предлагает три способа добиться этого.

4.1. Использование аннотации @Command для классов

Аннотация @Command предлагает возможность регистрировать подкоманды через параметр subcommands `` :

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

В нашем случае мы добавляем два новых класса: GitAddCommand и GitCommitCommand . Оба аннотированы @Command и реализуют Runnable . Важно дать им имя, так как имена будут использоваться picocli для распознавания, какие подкоманды выполнять:

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

Таким образом, если мы запустим нашу основную команду с аргументом add , консоль выдаст «Добавление некоторых файлов в staging area» .

4.2. Использование аннотации @Command для методов

Другой способ объявить подкоманды — создать @Command-аннотированные методы , представляющие эти команды в классе GitCommand `` :

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

Таким образом, мы можем напрямую реализовать нашу бизнес-логику в методах, а не создавать отдельные классы для ее обработки.

4.3. Программное добавление подкоманд

Наконец, picocli предлагает нам возможность программно зарегистрировать наши подкоманды. Это немного сложнее, так как мы должны создать объект CommandLine , обертывающий нашу команду, а затем добавить к нему подкоманды:

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

После этого нам все еще нужно запустить нашу команду, но мы больше не можем использовать метод CommandLine.run () . Теперь нам нужно вызвать метод parseWithHandler() для нашего только что созданного объекта CommandLine :

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

Следует отметить использование класса RunLast , который указывает picocli запускать наиболее конкретную подкоманду. Есть два других обработчика команд, предоставляемых picocli : RunFirst и RunAll . Первый запускает самую верхнюю команду, а второй запускает их все.

При использовании удобного метода CommandLine.run() обработчик RunLast используется по умолчанию.

5. Управление параметрами с помощью аннотации @Option

5.1. Вариант без аргументов

Давайте теперь посмотрим, как добавить некоторые параметры к нашим командам. Действительно, мы хотели бы сказать нашей команде добавления , что она должна добавить все измененные файлы. Для этого мы добавим поле с аннотацией @Option в наш класс GitAddCommand :

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

Как мы видим, аннотация принимает параметр имен , который дает разные имена опции. Следовательно, вызов команды добавления с параметром -A или –all установит для поля allFiles значение true . Итак, если мы запустим команду с опцией, в консоли отобразится «Добавление всех файлов в промежуточную область» .

5.2. Вариант с аргументом

Как мы только что видели, для опций без аргументов их наличие или отсутствие всегда оценивается как логическое значение.

Однако можно зарегистрировать параметры, которые принимают аргументы. Мы можем сделать это, просто объявив наше поле другого типа. Давайте добавим опцию сообщения в нашу команду фиксации :

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

Неудивительно, что при задании параметра сообщения команда покажет сообщение фиксации на консоли. Позже в этой статье мы рассмотрим, какие типы обрабатываются библиотекой и как обрабатывать другие типы.

5.3. Вариант с несколькими аргументами

Но что, если мы хотим, чтобы наша команда принимала несколько сообщений, как это делается с настоящей командой git commit ? Не беспокойтесь, давайте сделаем наше поле массивом или коллекцией , и мы почти закончили:

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

Теперь мы можем использовать опцию сообщения несколько раз:

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

Однако мы также можем указать параметр только один раз и разделить различные параметры разделителем регулярных выражений. Следовательно, мы можем использовать параметр разделения аннотации @Option :

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

Теперь мы можем передать -m «Моя фиксация великолепна», «Моя фиксация прекрасна» , чтобы получить тот же результат, что и выше.

5.4. Обязательный параметр

Иногда у нас может быть опция, которая требуется. Обязательный аргумент , который по умолчанию равен false , позволяет нам сделать это:

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

Теперь невозможно вызвать команду фиксации без указания опции сообщения . Если мы попытаемся это сделать, picocli выдаст ошибку:

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

6. Управление позиционными параметрами

6.1. Захват позиционных параметров

Теперь давайте сосредоточимся на нашей команде добавления , потому что она еще не очень мощная. Мы можем решить добавить только все файлы, но что, если мы хотим добавить определенные файлы?

Для этого мы могли бы использовать другой вариант, но здесь лучше использовать позиционные параметры. Действительно, позиционные параметры предназначены для захвата аргументов команды, которые занимают определенные позиции и не являются ни подкомандами, ни опциями.

В нашем примере это позволит нам сделать что-то вроде:

add file1 file2

Чтобы получить позиционные параметры, мы будем использовать аннотацию @Parameters :

@Parameters
private List<Path> 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"));
}
}

Теперь наша предыдущая команда напечатает:

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

6.2. Захват подмножества позиционных параметров

Благодаря индексному параметру аннотации можно более точно определить, какие позиционные параметры следует захватывать. Индекс отсчитывается от нуля. Таким образом, если мы определим:

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

Это зафиксирует аргументы, которые не соответствуют параметрам или подкомандам, от третьего до конца.

Индекс может быть либо диапазоном, либо одним числом, представляющим одну позицию.

7. Несколько слов о преобразовании типов

Как мы видели ранее в этом руководстве, picocli сам выполняет некоторое преобразование типов. Например, он сопоставляет несколько значений с массивами или коллекциями , но также может сопоставлять аргументы с определенными типами, например, когда мы используем класс Path для команды добавления .

На самом деле, Picocli поставляется с кучей предварительно обработанных типов . Это означает, что мы можем использовать эти типы напрямую, не задумываясь об их преобразовании самостоятельно.

Однако нам может потребоваться сопоставить аргументы нашей команды с типами, отличными от тех, которые уже обработаны. К счастью для нас, это возможно благодаря интерфейсу ITypeConverter и методу CommandLine#registerConverter , который связывает тип с преобразователем .

Давайте представим, что мы хотим добавить подкоманду config в нашу команду git , но мы не хотим, чтобы пользователи изменяли несуществующий элемент конфигурации. Итак, мы решили сопоставить эти элементы с перечислением:

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

Кроме того, в наш только что созданный класс GitConfigCommand добавим два позиционных параметра:

@Parameters(index = "0")
private ConfigElement element;

@Parameters(index = "1")
private String value;

@Override
public void run() {
System.out.println("Setting " + element.value() + " to " + value);
}

Таким образом мы гарантируем, что пользователи не смогут изменить несуществующие элементы конфигурации.

Наконец, мы должны зарегистрировать наш преобразователь. Что прекрасно, так это то, что при использовании Java 8 или выше нам даже не нужно создавать класс, реализующий интерфейс ITypeConverter . Мы можем просто передать лямбда-выражение или ссылку на метод в метод registerConverter() :

CommandLine commandLine = new CommandLine(new GitCommand());
commandLine.registerConverter(ConfigElement.class, ConfigElement::from);

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

Это происходит в методе main() GitCommand . Обратите внимание, что нам пришлось отказаться от удобного метода CommandLine.run() .

При использовании с необработанным элементом конфигурации команда будет отображать справочное сообщение и часть информации о том, что невозможно преобразовать параметр в ConfigElement :

Invalid value for positional parameter at index 0 (<element>): 
cannot convert 'user.phone' to ConfigElement
(java.lang.IllegalArgumentException: The argument user.phone doesn't match any ConfigElement)
Usage: git config <element> <value>
<element>
<value>

8. Интеграция с Spring Boot

Наконец, давайте посмотрим, как все это Springify!

Действительно, мы можем работать в среде Spring Boot и хотим извлечь из нее пользу в нашей программе командной строки. Для этого мы должны создать SpringBootApplication ` , реализующий интерфейс CommandLineRunner` :

@SpringBootApplication
public class Application implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Override
public void run(String... args) {
}
}

Кроме того, давайте аннотируем все наши команды и подкоманды аннотацией Spring @Component и автоматически подключаем все это в нашем приложении :

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

Обратите внимание, что нам пришлось автоматически связывать каждую подкоманду. К сожалению, это связано с тем, что на данный момент picocli еще не может извлекать подкоманды из контекста Spring при декларативном объявлении (с аннотациями). Таким образом, нам придется сделать эту проводку самостоятельно программным способом:

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

И теперь наша программа командной строки прекрасно работает с компонентами Spring. Поэтому мы могли бы создать несколько классов обслуживания и использовать их в наших командах, а Spring позаботится об внедрении зависимостей.

9. Заключение

В этой статье мы рассмотрели некоторые ключевые особенности библиотеки picocli . Мы узнали, как создать новую команду и добавить к ней несколько подкоманд. Мы видели много способов работы с опциями и позиционными параметрами. Кроме того, мы научились реализовывать собственные преобразователи типов, чтобы сделать наши команды строго типизированными. Наконец, мы увидели, как включить Spring Boot в наши команды.

Конечно, есть еще много вещей, которые можно узнать об этом. Библиотека предоставляет полную документацию .

Что касается полного кода этой статьи, то его можно найти на нашем GitHub .