«1. Введение

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

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

2. Команда Hello World

Давайте начнем с чего-то простого: команды 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!");
    }
}

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

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

java -cp "pathToPicocliJar;pathToCommandJar" com.baeldung.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?");
    }
}

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

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

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

Как мы видим, аннотация принимает параметр name, который задает разные имена опции. Следовательно, вызов команды добавления с параметром -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. Вариант с несколькими аргументами

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

add file1 file2

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

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

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

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

Теперь наша команда из ранее напечатал бы:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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>

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

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

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

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

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

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

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

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

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

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

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

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

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

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