Обработка аргументов с помощью JCommander

Обработка аргументов с помощью JCommander #

Васильев Андрей Михайлович, 2025

Версии презентации


Аргументы приложения #

  • Приложение при старте может получить некоторый набор строк, аргументов, которые были указаны при запуске приложения
  • Приложение может использовать их для настройки собственного поведения
  • Разработчик приложения волен самостоятельно определить формат обработки строк
  • Разумно использовать подходы, которые известны пользователю в результате работы с другими приложениями, сделать приложение «интуитивно понятным»
  • Существует множество библиотек для JVM, позволяющих выполнить обработку передаваемых строк. Рассмотрим JCommander

Официальная документация: https://jcommander.org/

JavaDoc-документация: https://javadoc.io/doc/com.beust/jcommander/latest/index.html

Альтернаитвное Java-решение: https://picocli.info/


Подключение библиотеки #

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

Gradle-описание зависимости для подключения: "org.jcommander:jcommander:2.0"

При использовании Gradle без дополнений её необходимо добавить в файл build.gradle.kts в раздел dependencies:

dependencies {
    implementation("org.jcommander:jcommander:2.0")
}

При использовании шаблона лабораторной добавьте строку в файл build.properties.json в список dependencies


Концептуальный подход JCommander #

diagram

  • Необходимо описать и создать объекты-хранители аргументов
  • Необходимо создать ключевой объект библиотеки JCommander
    • Объект JCommander создаётся путём взаимодействия с объектом-строителем
    • Объекту-строителю передаётся список объектов-хранителей аргументов
  • Созданному объекту передаются аргументы командной строки
    • При работе JCommander опирается на мета-информацию, расположенную в аннотациях к свойствам объектам-хранителям аргументов
    • Объект-хранитель вместе с хранением описывает поддерживаемые аргументы
    • В случае ошибки обработки аргументов выбрасывается исключение ParameterException

Описание объекта-хранителя аргументов #

@Parameters(separators = "=")
class Args {
    @Parameter
    var targets: List<String> = arrayListOf()
    @Parameter(names = ["-bf", "--buildFile"],
               description = "The build file")
    var buildFile: String? = null
    @Parameter(names = ["--checkVersions"],
               description = "Check if there are any newer versions")
    var checkVersions = false
}
  • Аннотация Parameters определяет общие правила обработки строк для данного объекта, не обязательна для использования
    • С помощью аргументов separators="=" включается обработка аргументов в формате ключ=значение помимо формата ключ значение
  • Конкретные свойства обозначаются для записи с помощью аннотации @Parameter
  • Свойства должны быть изменяемыми
  • Свойства могут иметь значения по умолчанию
  • Названия ключей для свойств объектов определяется с помощью names
  • Рекомендуется добавлять описание параметра, чтобы предоставить пользователю содержательную информацию по назначению аргумента

Создание и вызов JCommander #

val commander: JCommander = 
    JCommander
        .newBuilder()
        .addObject(commonParameters)
        .build()
  • Создаём объект-строитель путём вызова статического метода newBuilder()
  • Добавляем объекты-хранители аргументов с помощью addObject()
  • Завершаем создание путём вызова метода build()
  • Передаём аргументы путём вызова метода parse(), принимающего произвольное количество строк
commander.parse("--enable", "--size", "15")
val arguments: Array<String> = ...
commander.parse(*arguments)
  • Обработанные аргументы будут сохранены в объект commonParameters
  • Если строки не смогут быть обработаны, будет выброшено исключение ParameterException

Преобразование в сложные типы данных #

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

Для других сложных типов данных, классов можно предоставить класс-преобразователь, который должен реализовывать следующий Java-интерфейс:

public interface IStringConverter<T> {
  T convert(String value);
}

Например для UUID на Kotlin такой класс может выглядеть следующим образом:

class UUIDConverter : IStringConverter<UUID> {
    override fun convert(parameter: String): UUID = 
        UUID.fromString(parameter)
}


В целевом объекте-хранителе аргументов необходимо сослаться на данный преобразователь следующим образом:

@Parameter(names=["--id"], converter = UUIDConverter::class)
var someId: UUID? = null
  • Если во время преобразования строки к UUID возникнет ошибка, то будет выброшено исключение IllegalArgumentException
  • Конвертор необходимо явно указывать для каждого поля в параметрах аннотации Parameter, для которого необходимо выполнить преобразование

Обязательные аргументы #

Аннотация Parameter позволяет указать аргумент как обязательный:

@Parameter(names=["--data-one"], required = true)
var dataOne: Int = 0

Согласно документации в случае отсутствия аргумента будет выброшено соответствующее исключение. Однако в данном случае оно не будет выброшено

  1. JCommander получает уже инициализированный объект
  2. JCommander обрабатывает строки аргументов, записывая результаты в свойства объекта
  3. Если после обработки всех строк какие-то поля остались равными null, то будет выброшено исключение

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

@Parameter(names=["--data-one"], required = true)
var dataOne: Int? = null

Факт отсутствия исключения не убеждает компилятор Kotlin в наличие не-null-значения в свойстве объекта.


Команды #

Приложения с командным интерфейсом зачастую предоставляют ряд связных между собой действий, каждое из которых имеет свой собственный набор аргументов

  • Склонировать репозиторий: git clone <REPOSITORY>
  • Создать новую ветку в репозитории git switch --create <BRANCH>

Названием команды обычно выступает первый позиционный аргумент приложения

Согласно концепции JCommander

  • Список аргументов команды описывается в отдельном объекте
  • Объекты регистрируются как команды, указывается уникальный идентификатор команды
  • Объекты-команды описываются также как объекты-параметры, только регистрируются по-другому
  • После обработки аргументов у объекта JCommander можно узнать строковый идентификатор команды, которая была определена, через свойство parsedCommand

JCommander с командами #

diagram

  • Необходимо описать классы для сохранения аргументов для каждой команды
  • При создании объекта JCommander необходимо связать название команды с объектами-хранителями аргумента команды
  • Во время обработки аргументов первый используется для определения команды
  • Данные будут записаны в объект-хранитель соответствующей команды

Описание объекта-хранителя команды #

Описание не отличается от обычного объекта-хранителя:

  • Аннотация Parameters используется для описания поведения объекта-хранителя
  • Аннотация Parameter используется для описания конкретного аргумента
@Parameters(separators = "=",
        commandDescription = "List application entries")
class ListCommand {
    @Parameter(names = ["-s", "--size"],
            description = "Amount of items to display")
    var size: Int = 10
}

Единственным отличием является возможность использования аргумента commandDescription у аннотации Parameters, которая позволяет добавить описание назначения команды


Настройка JCommander для работы с командами #

val commander: JCommander = 
    JCommander
        .newBuilder()
        .addCommand("list", listCommand)
        .addCommand("build", buildCommand)
        .build()
  • Создаём объект-строитель путём вызова метода newBuilder()
  • Добавляем список объектов-хранителей аргументов команд
  • При добавлении команды указывается её уникальное имя
commander.parse("list", "--size")
val arguments: Array<String> = ...
commander.parse(*arguments)
if (commander.parsedCommand == "list") {
  ... // Обрабатываем данные внутри listCommand
} else if (commander.parsedCommand == "build") {
  ... // Обрабатываем данные внутри buildCommand
}

Справка по использованию #

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

Для вывода этой информации на стандартный поток вывода, вызовите метод usage() на объекте JCommander после добавления к нему объектов и команд

Usage: <main class> [command] [command options]
  Commands:
    list      List application entries
      Usage: list [options]
        Options:
          -s, --size
            Amount of items to display
            Default: 10

    update      Update application entry
      Usage: update [options]
        Options:
        * -i, --id
            Id of the entry to update

Аргументы, помеченные *, являются обязательными


Размещение аргументов в файле #

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

Помимо использования модульных тестов JCommander предоставляет возможность считывания списка аргументов из файла с помощью @-синтаксиса

commander.parse("@params.txt")

Из файла params.txt, который находится в текущем рабочем каталоге:

  • Будут считаны все строки
  • Строки, начинающиеся с символа #, будут проигнорированы
  • Оставшиеся строки разбиты на слова, будет получен список аргументов
  • Внутри данных аргументов могут находиться @-аргументы

Данный подход удобнее постоянного редактирования конфигурации запуска Gradle-задачи

© A. M. Васильев, 2025, CC BY-SA 4.0, andrey@crafted.su