Разбор аргументов командной строки в Java


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

Небольшое введение

Прежде всего необходимо определиться с терминами. Для этого воспользуемся простым примером:
hg log -l 3
это команда для вывода последних трех сообщений из истории системы контроля версий Mercurial.
  • hg - имя приложения
  • log - команда, в данном случае, на вывод сообщений из истории
  • l - параметр (или опция), в данном примере, уточняющий количество выводимых сообщений
Обратите внимание, что параметром я называю именно l, а не -l, тк есть некоторые расхождения по поводу префикса перед параметром. Вообще, существует несколько подходов к форматированию параметров, например
POSIX (tar -zxvf foo.tar.gz), GNU (du --human-readable --max-depth=1) или Java (java -Djava.awt.headless=true Foo). Каким пользоваться, выбор за Вами.

Commons CLI

Первая библиотека, Apache Commons CLI, предлагает следующий подход:
Каждый параметр описывается как объект Option:
Option count = new Option("l", "limit", true, "ограничивает количество отображаемых изменений");
Для параметра указывается краткое  и полное имя, логический ключ и описание. Логический ключ говорит о том, имеет ли параметр аргументы или нет. Объекты, описывающие параметры, объединяются в набор параметров Options:
Options options = new Options();
options.addOption(count);
Сама строка запуска представляется классом CommandLine. У объекта этого класса можно узнать был ли указан параметр и если был, то с каким значением:
if( line.hasOption( "l" ) ) {
    String count = line.getOptionValue( "l" );
}
А вот процесс получения объекта CommandLine представляет отдельный интерес. Для получения CommandLine применяются парсеры - объекты, реализующие интерфейс CommandLineParser:
CommandLine line = parser.parse( options, args );
Реализаций данного интерфейса библиотека предлагает два: PosixParser и GnuParser.
CommandLineParser parser = new GnuParser();
CommandLineParser parser = new PosixParser(); 
Каждый парсер реализует подход к форматированию параметров из одноименного стандарта. Такое решение позволяет без труда менять подход к форматированию параметров.

Помимо разбора аргументов строки запуска, Commons CLI позволяет генерировать справку, опираясь на описанные параметры:
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp( "hg", options );
Результат:
usage: hg
 -l,--limit <arg>   ограничивает количество отображаемых изменений
HelpFormatter достаточно гибко настраивается. Более полный пример его использования можно посмотреть на официальном сайте.

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


JCommander

В библиотеке JCommander предлагается иной подход к решению задачи разбора командной строки. Автор предлагает сопоставлять каждому параметру поле класса:
import com.beust.jcommander.Parameter;
 
public class HgLogExample {
 
  @Parameter(names = { "-l", "--limit" }, arity = 1,
    description = "ограничивает количество отображаемых изменений")
  private Integer limit = 1;
}

Обратите внимание, что в данном решении префиксы в имени параметра указываются в ручную и вам не удастся так просто сменить подход к форматированию параметров. Но на самом деле, кому это надо? :)

Зато, библиотека поддерживает различные типы параметров. Из коробки идут как минимум стандартные типы, вроде Integer, String и Boolean. И предусмотрена гибкая система для расширения этого списка через свои конверторы:
public class FileConverter implements IStringConverter<File> {
  @Override
  private File convert(String value) {
    return new File(value);
  }
}
/* Конвертор указывается в аннотации к параметру */
@Parameter(names = "-file", converter = FileConverter.class)
File file;

Помимо этого, есть механизм предварительной проверки параметров:
public class PositiveInteger implements IParameterValidator {
 public void validate(String name, String value)
      throws ParameterException {
    int n = Integer.parseInt(value);
    if (n < 0) {
      throw new ParameterException("Parameter " + name 
        + " should be positive (found " + value +")");
    }
  }
}

/* Тем временем в классе HgLogExample...*/
@Parameter(names = { "-l", "--limit" }, arity = 1, validateWith = PositiveInteger.class,
    description = "ограничивает количество отображаемых изменений")
private Integer limit = 1;

После того, как необходимые параметры описаны, создается объект JCommander и ему указывается массив с аргументами командной строки:
HgLogExample hgLog = new HgLogExample();
JCommander jc = new JCommander(hgLog);
jc.parse(args);

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

Самое главное, что в этой библиотеке аннотации предусмотрены не только для полей класса, но и для самого класса тоже. Аннотация Parameters позволяет объявить класс, как реализацию команды, не двусмысленно призывая следовать паттерну Команда:
@Parameters(commandNames = { "log" }, 
commandDescription = "показывает историю ревизий всего репозитория или файлов")
public class HgLogExample implements Runnable {

    public void run() {
        System.out.println("Hello world!");        
    } 

    @Parameter(names = { "-l", "--limit" }, arity = 1, 
        description = "ограничивает количество отображаемых изменений")
    private Integer limit;
}
Пример использования:
public static void main(String[] args) {
   /* Создается карта имя-команда */
   Map<String, Runnable> map = new HashMap<String, Runnable>();
   HgLogExample hgLog = new HgLogExample();
   map.put("log", hgLog);
   /* Разбираются аргументы запуска */
   JCommander jc = new JCommander();
   jc.addCommand(hgLog);
   args = new String[] { "log", "-l", "3" };
   jc.parse(args);
   /* Выполняется указанная в аргументах команда */
   String name = jc.getParsedCommand();
   map.get(name).run();
}
Результат:
Hello world!


JCommander так же позволяет выводить справку, но в кастомизации форматирования эта библиотека уступает предыдущей:
jc.usage();
Usage: <main class> [options] [command] [command options]  Commands:
    log      показывает историю ревизий всего репозитория или файлов
      Usage: log [options]      
        Options:
          -l, --limit   ограничивает количество отображаемых изменений

Зато, есть возможность выводить справку только по конкретной команде:
jc.usage("log");
показывает историю ревизий всего репозитория или файлов
Usage: log [options]
  Options:
    -l, --limit   ограничивает количество отображаемых изменений

ИМХО:

Если в вашем приложении нет того, что в начале статьи было определено как команда, то скорее всего вам стоит выбрать common-cli. Но тот факт, что jcommander берет на себя вопрос инициализации полей(присвоение им значения из командной строки) и проверку значений аргументов, скорее всего даже в простом случае заставит вас сделать выбор в пользу JCommander, 



Что еще почитать:

  1. Commons CLI
  2. Работа с commons-cli 1.2
  3. JCommander

Комментариев нет: