Рубрики
Без рубрики

Анализ параметров командной строки с помощью Commander

Commander берет на себя всю сложность анализа и проверки параметров для наших приложений командной строки. Мы исследуем все его основные функции.

Автор оригинала: Priyank Srivastava.

1. Обзор

В этом уроке мы узнаем, как использовать Commander для анализа параметров командной строки. Мы рассмотрим некоторые из его функций при создании простого приложения командной строки.

2. Почему Командир?

“Потому что жизнь слишком коротка, чтобы анализировать параметры командной строки” – Cédric Beust

Commander, созданный Седриком Бестом, представляет собой библиотеку на основе аннотаций для синтаксического анализа параметров командной строки . Это может снизить затраты на создание приложений командной строки и помочь нам обеспечить для них хороший пользовательский интерфейс.

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

3. Настройка JCommander

3.1. Конфигурация Maven

Давайте начнем с добавления зависимости jcommander в ваш pom.xml :


    com.beust
    jcommander
    1.78

3.2. Привет, Мир

Давайте создадим простое приложение Hello World , которое принимает один ввод с именем name и печатает приветствие “Hello “ .

Поскольку JCommander связывает аргументы командной строки с полями в классе Java , мы сначала определим класс Hello World Args с полем name с аннотацией @Parameter :

class HelloWorldArgs {

    @Parameter(
      names = "--name",
      description = "User name",
      required = true
    )
    private String name;
}

Теперь давайте используем класс JCommander для анализа аргументов командной строки и назначения полей в нашем объекте Hello World Args :

HelloWorldArgs jArgs = new HelloWorldArgs();
JCommander helloCmd = JCommander.newBuilder()
  .addObject(jArgs)
  .build();
helloCmd.parse(args);
System.out.println("Hello " + jArgs.getName());

Наконец, давайте вызовем основной класс с теми же аргументами из консоли:

$ java HelloWorldApp --name JavaWorld
Hello JavaWorld

4. Создание реального приложения в Commander

Теперь, когда мы запущены и запущены , давайте рассмотрим более сложный вариант использования — клиент API командной строки, который взаимодействует с приложением для выставления счетов, таким как Stripe , в частности со сценарием выставления счетов Metered (или на основе использования). Эта сторонняя служба выставления счетов управляет нашими подписками и выставлением счетов.

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

  • отправить : Отправить количество и цену за единицу использования для клиента по данной подписке
  • fetch : Сборы за выборку для клиента на основе потребления некоторых или всех их подписок в текущем месяце — мы можем получить эти сборы, агрегированные по всем подпискам или детализированные по каждой подписке

Мы создадим клиент API по мере прохождения функций библиотеки.

Давайте начнем!

5. Определение параметра

Давайте начнем с определения параметров, которые может использовать наше приложение.

5.1. Аннотация @Parameter

Аннотирование поля с помощью @Parameter указывает JCommander привязать к нему соответствующий аргумент командной строки . @Parameter имеет атрибуты для описания основного параметра, такие как:

  • имена – одно или несколько имен опции, например “–name” или ” – n”
  • описание – значение опции, чтобы помочь конечному пользователю
  • требуется – является ли опция обязательной, по умолчанию false
  • arity – количество дополнительных параметров, которые использует опция

Давайте настроим параметр CustomerID в нашем сценарии дозированного выставления счетов:

@Parameter(
  names = { "--customer", "-C" },
  description = "Id of the Customer who's using the services",
  arity = 1,
  required = true
)
String customerId;

Теперь давайте выполним нашу команду с новым параметром “–customer”:

$ java App --customer cust0000001A
Read CustomerId: cust0000001A.

Аналогично, мы можем использовать более короткий параметр “-C” для достижения того же эффекта:

$ java App -C cust0000001A
Read CustomerId: cust0000001A.

5.2. Требуемые параметры

Если параметр является обязательным, приложение завершает работу, вызывая исключение ParameterException , если пользователь не указывает его:

$ java App
Exception in thread "main" com.beust.jcommander.ParameterException:
  The following option is required: [--customer | -C]

Следует отметить, что, как правило, любая ошибка при разборе параметров приводит к ParameterException в JCommander.

6. Встроенные Типы

6.1. Интерфейс преобразователя строк

Commander выполняет преобразование типов из командной строки String ввода в типы Java в наших классах параметров. Интерфейс Stringconverter обрабатывает преобразование типа параметра из String в любой произвольный тип. Итак, все встроенные преобразователи Commander реализуют этот интерфейс.

Из коробки Commander поставляется с поддержкой общих типов данных, таких как String , Integer , Boolean , BigDecimal и Enum .

6.2. Типы Одинарности

Arity относится к числу дополнительных параметров, используемых опционом. Встроенные типы параметров Commander имеют значение по умолчанию один , за исключением Boolean и List. Таким образом, общие типы, такие как String , Integer , BigDecimal , Long, и Enum , являются одиночными типами.

6.3. Логический тип

Поля типа boolean или Boolean не нуждаются в дополнительном параметре – эти параметры имеют arity равное нулю.

Давайте рассмотрим пример. Возможно, мы хотим получить плату за клиента, детализированную по подписке. Мы можем добавить логическое поле детализированное , которое по умолчанию false :

@Parameter(
  names = { "--itemized" }
)
private boolean itemized;

Наше приложение будет возвращать агрегированные сборы с детализированным значением false . Когда мы вызываем командную строку с параметром itemized , мы устанавливаем в поле значение true :

$ java App --itemized
Read flag itemized: true.

Это хорошо работает, если у нас нет прецедента, когда мы всегда хотим детализировать сборы , , если не указано иное. Мы могли бы изменить параметр, чтобы он был не детализирован, но было бы яснее, если бы можно было указать false в качестве значения детализирован .

Давайте представим это поведение, используя значение по умолчанию true для поля и установив его arity как один:

@Parameter(
  names = { "--itemized" },
  arity = 1
)
private boolean itemized = true;

Теперь, когда мы указываем опцию, значение будет установлено в false :

$ java App --itemized false
Read flag itemized: false.

7. Типы списков

Commander предоставляет несколько способов привязки аргументов к полям List .

7.1. Многократное указание параметра

Предположим, мы хотим получить плату только за подмножество подписок клиента:

@Parameter(
  names = { "--subscription", "-S" }
)
private List subscriptionIds;

Это поле не является обязательным, и приложение будет получать сборы по всем подпискам, если параметр не указан. Однако мы можем указать несколько подписок , используя имя параметра несколько раз :

$ java App -S subscriptionA001 -S subscriptionA002 -S subscriptionA003
Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003].

7.2. Привязка списков с помощью разделителя

Вместо того, чтобы указывать параметр несколько раз, давайте попробуем связать список, передав разделенную запятыми строку |:

$ java App -S subscriptionA001,subscriptionA002,subscriptionA003
Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003].

При этом используется одно значение параметра) для представления списка. Commander будет использовать разделитель параметров класса Command для привязки разделенной запятыми Строки к нашему Списку .

7.3. Привязка списков с помощью пользовательского разделителя

Мы можем переопределить разделитель по умолчанию, реализовав I Разделитель параметров интерфейс:

class ColonParameterSplitter implements IParameterSplitter {

    @Override
    public List split(String value) {
        return asList(value.split(":"));
    }
}

А затем сопоставление реализации с атрибутом splitter в @Parameter :

@Parameter(
  names = { "--subscription", "-S" },
  splitter = ColonParameterSplitter.class
)
private List subscriptionIds;

Давайте попробуем:

$ java App -S "subscriptionA001:subscriptionA002:subscriptionA003"
Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003].

7.4. Списки переменной Арности

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

Давайте попробуем это для анализа подписок:

@Parameter(
  names = { "--subscription", "-S" },
  variableArity = true
)
private List subscriptionIds;

И когда мы выполняем нашу команду:

$ java App -S subscriptionA001 subscriptionA002 subscriptionA003 --itemized
Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003].

Командир связывает все входные аргументы после параметра “-S” с полем списка до следующего параметра или окончания команды.

7.5. Фиксированные списки Arity

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

@Parameter(
  names = { "--subscription", "-S" },
  arity = 2
)
private List subscriptionIds;

Исправлено, что arity заставляет проверять количество параметров, переданных параметру List , и в случае нарушения выдает исключение ParameterException :

$ java App -S subscriptionA001 subscriptionA002 subscriptionA003
Was passed main parameter 'subscriptionA003' but no main parameter was defined in your arg class

Сообщение об ошибке предполагает, что, поскольку JCommander ожидал только два аргумента, он попытался проанализировать дополнительный входной параметр “subscriptionA003” в качестве следующего параметра.

8. Пользовательские типы

Мы также можем привязать параметры, написав пользовательские конвертеры. Как и встроенные преобразователи, пользовательские преобразователи должны реализовывать интерфейс Stringconverter .

Давайте напишем конвертер для разбора временной метки ISO8601 :

class ISO8601TimestampConverter implements IStringConverter {

    private static final DateTimeFormatter TS_FORMATTER = 
      DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss");

    @Override
    public Instant convert(String value) {
        try {
            return LocalDateTime
              .parse(value, TS_FORMATTER)
              .atOffset(ZoneOffset.UTC)
              .toInstant();
        } catch (DateTimeParseException e) {
            throw new ParameterException("Invalid timestamp");
        }
    }
}

Этот код будет анализировать ввод String и возвращать Instant , вызывая ParameterException , если есть ошибка преобразования. Мы можем использовать этот конвертер, привязав его к полю типа Instant с помощью атрибута converter в @Parameter :

@Parameter(
  names = { "--timestamp" },
  converter = ISO8601TimestampConverter.class
)
private Instant timestamp;

Давайте посмотрим на это в действии:

$ java App --timestamp 2019-10-03T10:58:00
Read timestamp: 2019-10-03T10:58:00Z.

9. Проверка Параметров

Commander предоставляет несколько проверок по умолчанию:

  • указаны ли необходимые параметры
  • если количество заданных параметров совпадает с арностью поля
  • можно ли преобразовать каждый параметр String в тип соответствующего поля

Кроме того, мы можем пожелать добавить пользовательские проверки . Например, предположим, что идентификаторы клиентов должны быть UUID s.

Мы можем написать валидатор для поля customer, который реализует интерфейс IParameterValidator :

class UUIDValidator implements IParameterValidator {

    private static final String UUID_REGEX = 
      "[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}";

    @Override
    public void validate(String name, String value) throws ParameterException {
        if (!isValidUUID(value)) {
            throw new ParameterException(
              "String parameter " + value + " is not a valid UUID.");
        }
    }

    private boolean isValidUUID(String value) {
        return Pattern.compile(UUID_REGEX)
          .matcher(value)
          .matches();
    }
}

Затем мы можем подключить его с помощью атрибута validate With параметра:

@Parameter(
  names = { "--customer", "-C" },
  validateWith = UUIDValidator.class
)
private String customerId;

Если мы вызовем команду с идентификатором клиента, отличным от UUID, приложение завершит работу с сообщением об ошибке проверки:

$ java App --C customer001
String parameter customer001 is not a valid UUID.

10. Субкоманды

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

В Commander мы можем поддерживать несколько команд, называемых подкомандами, каждая из которых имеет отдельный набор параметров.

10.1. Аннотация @Parameters

Мы можем использовать @Parameters для определения подкоманд. @@Parameters содержит атрибут Имена команд для идентификации команды.

Давайте смоделируем submit и fetch в качестве подкоманд:

@Parameters(
  commandNames = { "submit" },
  commandDescription = "Submit usage for a given customer and subscription, " +
    "accepts one usage item"
)
class SubmitUsageCommand {
    //...
}

@Parameters(
  commandNames = { "fetch" },
  commandDescription = "Fetch charges for a customer in the current month, " +
    "can be itemized or aggregated"
)
class FetchCurrentChargesCommand {
    //...
}

JCommander использует атрибуты в @Parameters для настройки подкоманд, таких как:

  • Имена команд – имя подкоманды; связывает аргументы командной строки с классом, аннотированным @Parameters
  • commandDescription – документирует назначение подкоманды

10.2. Добавление подкоманд в JCommander

Мы добавляем подкоманды в Commander с помощью метода add Command :

SubmitUsageCommand submitUsageCmd = new SubmitUsageCommand();
FetchCurrentChargesCommand fetchChargesCmd = new FetchCurrentChargesCommand();

JCommander jc = JCommander.newBuilder()
  .addCommand(submitUsageCmd)
  .addCommand(fetchChargesCmd)
  .build();

Метод addCommand регистрирует подкоманды с их соответствующими именами, указанными в атрибуте commandNames аннотации @Parameters|/.

10.3. Разбор Подкоманд

Чтобы получить доступ к выбранной пользователем команде, мы должны сначала проанализировать аргументы:

jc.parse(args);

Затем мы можем извлечь подкоманду с помощью getParsedCommand :

String parsedCmdStr = jc.getParsedCommand();

В дополнение к идентификации команды, Commander связывает остальные параметры командной строки с их полями в подкоманде. Теперь нам просто нужно вызвать команду, которую мы хотим использовать:

switch (parsedCmdStr) {
    case "submit":
        submitUsageCmd.submit();
        break;

    case "fetch":
        fetchChargesCmd.fetch();
        break;

    default:
        System.err.println("Invalid command: " + parsedCmdStr);
}

11. Справка по использованию командира

Мы можем вызвать usage для отображения руководства по использованию. Это краткое изложение всех опций, которые использует наше приложение. В нашем приложении мы можем вызвать использование основной команды или, в качестве альтернативы, каждой из двух команд “отправить” и “извлечь” отдельно.

Отображение использования может помочь нам несколькими способами: отображение параметров справки и во время обработки ошибок.

11.1. Отображение параметров Справки

Мы можем привязать параметр справки в наших командах, используя параметр boolean вместе с атрибутом help , установленным в true :

@Parameter(names = "--help", help = true)
private boolean help;

Затем мы можем определить, был ли передан “–help” в аргументах, и вызвать usage :

if (cmd.help) {
  jc.usage();
}

Давайте посмотрим вывод справки для нашей подкоманды “отправить”:

$ java App submit --help
Usage: submit [options]
  Options:
  * --customer, -C     Id of the Customer who's using the services
  * --subscription, -S Id of the Subscription that was purchased
  * --quantity         Used quantity; reported quantity is added over the 
                       billing period
  * --pricing-type, -P Pricing type of the usage reported (values: [PRE_RATED, 
                       UNRATED]) 
  * --timestamp        Timestamp of the usage event, must lie in the current 
                       billing period
    --price            If PRE_RATED, unit price to be applied per unit of 
                       usage quantity reported

Метод usage использует атрибуты @Parameter , такие как description , для отображения полезной сводки. Параметры, отмеченные звездочкой ( * ), являются обязательными.

11.2. Обработка ошибок

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

try {
  jc.parse(args);

} catch (ParameterException e) {
  System.err.println(e.getLocalizedMessage());
  jc.usage();
}

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

В этом уроке мы использовали Commander для создания приложения командной строки. Хотя мы рассмотрели многие основные функции, в официальной документации есть еще больше .

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