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

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

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

1. Обзор

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

2. Почему JCommander?

«Потому что жизнь слишком коротка, чтобы анализировать параметры командной строки» — Седрик Беуст

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

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

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

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

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

<dependency>
<groupId>com.beust</groupId>
<artifactId>jcommander</artifactId>
<version>1.78</version>
</dependency>

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

Давайте создадим простое приложение HelloWorldApp , которое принимает один вход с именем name и печатает приветствие «Hello <name>» .

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

class HelloWorldArgs {

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

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

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. Создание реального приложения в JCommander

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

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

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

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

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

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

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

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

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

  • имена — одно или несколько имен опции, например «—name» или «-n»
  • описание — значение параметра, чтобы помочь конечному пользователю
  • required — является ли опция обязательной, по умолчанию 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. Интерфейс IStringConverter

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

По умолчанию JCommander поставляется с поддержкой распространенных типов данных, таких как String , Integer , Boolean , BigDecimal и Enum .

6.2. Одноместные типы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

7.1. Указание параметра несколько раз

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

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

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

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

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

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

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

Это использует одно значение параметра (арность = 1) для представления списка. JCommander будет использовать класс CommaParameterSplitter для привязки строки , разделенной запятыми, к нашему списку .

7.3. Связывание списков с использованием пользовательского разделителя

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

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<String> subscriptionIds;

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

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

7.4. Переменные списки Arity ``

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

Давайте попробуем разобрать подписки:

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

И когда мы запускаем нашу команду:

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

JCommander привязывает все входные аргументы после опции «-S» к полю списка до следующей опции или конца команды.

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

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

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

Фиксированная арность принудительно проверяет количество параметров, переданных в параметр списка , и выдает исключение 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. Пользовательские типы

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

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

class ISO8601TimestampConverter implements IStringConverter<Instant> {

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

Этот код будет анализировать входную строку и возвращать Instant , вызывая исключение ParameterException в случае ошибки преобразования. Мы можем использовать этот преобразователь, привязав его к полю типа Instant , используя атрибут преобразователя в @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. Проверка параметров

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

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

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

Мы можем написать валидатор для поля клиента, реализующий интерфейс 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();
}
}

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

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

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

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

10. Подкоманды

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

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

10.1. @Параметры аннотации

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

Давайте смоделируем 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 для настройки подкоманд, таких как:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

try {
jc.parse(args);

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

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

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

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