Краткое введение в boost::program_options

  • Tutorial
Занимаясь разработкой алгоритмов, постоянно одергиваю себя, а вдруг изменения, которые работают на небольшом примере, привнесут разброд и шатание в результаты на других, больших данных. Тогда мне на помощь приходит командная строка. Самое ужасное, что каждый раз реализовывать парсер аргументов уже надоело, а значит, не последним средством для C++ программиста оказывается пакет program_options из библиотеки boost.

Начнём с примера. Допустим я занимаюсь разработкой алгоритма распознавания чего-нибудь с обучением и у нас есть следующие данные. Файлы с какими-то данными и расширением .dat (data); файлы с обучающей информацией и расширением .trn (train) и файлы параметров с расширением .prs (parameters). Файлы параметров получаются в результате обучения и используются для распознавания. Итак, у нас есть 3 действия: train (обучить), recognize (распознать), score (оценить качество распознавания). В таком случае скрипт вызова цепочки обучение, распознавание, оценка выглядит, например, так:

  recognizer --type=train --input=train.dat --info=train.trn --output=best.prs
  recognizer --type=recognize --input=test1.dat --input=test2.dat --params=best.prs --output=./
  recognizer --type=score --ethanol=test1_expected.trn --test=test1.trn --output=scores.txt
  recognizer --type=score --ethanol=test2_expected.trn --test=test2.trn --output=scores.txt

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

  po::options_description desc("General options");
  std::string task_type;
  desc.add_options()
    ("help,h", "Show help")
    ("type,t", po::value<std::string>(&task_type), "Select task: train, recognize, score")
    ;
  po::options_description train_desc("Train options");
  train_desc.add_options()
    ("input,I", po::value<std::string>(), "Input .dat file")
    ("info,i", po::value<std::string>(), "Input .trn file")
    ("output,O", po::value<std::string>(), "Output parameters file .prs")
    ;
  po::options_description recognize_desc("Recognize options");
  recognize_desc.add_options()
    ("input,I",  po::value<std::vector<std::string> >(), "Input .dat file")
    ("params,p", po::value<std::string>(), "Input .prs file")
    ("output,O", po::value<std::string>(), "Output directory")
    ;
  po::options_description score_desc("Score options");
  score_desc.add_options()
    ("ethanol,e",  po::value<std::string>(), "Etalon .trn file")
    ("test,t", po::value<std::string>(), "Testing .trn file")
    ("output,O", po::value<std::string>(), "Output comparison file")
    ;

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

  ("input,I", po::value<std::string>(), "Input .dat file")

Первый аргумент input,I на самом деле это два варианта аргумента: input — длинное имя аргумента, I — короткое (регистр имеет значение). Особенностью boost::program_options является то, что короткое имя всегда должно быть однобуквенным (его, правда, можно и не задавать). Обращение к длинному имени в командной строке будет выглядеть следующим образом:

  --input=train.dat

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

  -Itrain.dat

Второй параметр po::value<std::string>() определяет формат значения аргумента (часть после знака равно) и может отсутствовать, если никакое значение передавать не нужно. Например, следующие вызовы эквивалентны:

  recognizer --help
  recognizer -h

Если присмотреться еще пристальнее, то можно заметить что в группе recognize, аргумент input имеет тип:

 po::value<std::vector<std::string> >()

std::vector<std::string> означает, что input может встречаться в аргументах командной строки более одного раза, то есть в нашем случае можно провести распознавание более одного файла одновременно. Например:

 recognizer --type=recognize -itest1.dat -itest2.dat -pbest.prs -O./

Третий и последний параметр — описание. Очень полезный пункт, особенно когда нужно что-нибудь еще посчитать через полгода после того, как написал в recognizer последнюю строчку. В нашем случае вывод хелпа будет выглядеть приблизительно следующим образом:

me@my: ./recognizer -h
General options:
  -h [ --help ]         Show help
  -t [ --type ] arg     Select task: train, recognize, score

Train options:
  -I [ --input ] arg    Input .dat file
  -i [ --info ] arg     Input .trn file
  -O [ --output ] arg   Output parameters file .prs

Recognize options:
  -I [ --input ] arg    Input .dat file
  -p [ --params ] arg   Input .prs file
  -O [ --output ] arg   Output directory

Score options:
  -e [ --ethanol ] arg  Etalon .trn file
  -t [ --test ] arg     Testing .trn file
  -O [ --output ] arg   Output comparison file

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

  namespace po = boost::program_options;
  po::variables_map vm;
  po::parsed_options parsed = po::command_line_parser(ac, av).options(desc).allow_unregistered().run();
  po::store(parsed, vm);
  po::notify(vm);

Мы передаем только General options в качестве шаблона аргументов. Без вызова allow_unregistered boost::program_options будет ругаться на лишние аргументы, не описанные в шаблоне, в котором только тип операции и help. После выполнения этого кода заполнена переменная task_type и можно писать «switch»:

  if(task_type == "train") {
    desc.add(train_desc);
    po::store(po::parse_command_line(ac,av,desc), vm);
    train(vm);
  }
  else if(task_type == "recognize") {
  //...
  else {
    desc.add(train_desc).add(recognize_desc).add(score_desc);
    std::cout << desc << std::endl;
  }

В шаблон добавляется соответствующая группа и аргументы командной строки разбираются полностью без исключений. Переменная vm представляет собой словарь со строковым ключом и boost::any в качестве значений. help, как можно видеть, получается практически даром.

Рассмотрим процедуру train(vm) пристальнее, чтобы понять как доставать значения из полученного словаря.

  void train(const po::variables_map& vm)
  {
    std::string input_path, info_path, output_path;
    if (vm.count("input")) {
      input_path = vm["input"].as<std::string>();
    }
    if(vm.count("info")) {
      info_path = vm["info"].as<std::string>();
    }
    if(vm.count("output")) {
      output_path = vm["output"].as<std::string>();
    }
    //...
  }

Как видно, все просто, однако, заметьте, что к аргументам нужно обращаться по их полному имени, а не по строке переданной в описании. Сравните «info,i» и просто «info».

Заключение


Полную версию примера можно найти на pastebin. Это далеко не все возможности библиотеки, но тем кому интересно уже на середине ушли читать официальную документацию.

Преимущества:
  • интуитивность (по крайней мере для меня)
  • самодостаточность (комментарии, типы, имена и группы из коробки)
  • работа с аргументами и конфигурационными файлами (хотя это и не освещалось)

Недостатки:
  • скудная документация
  • требует линковки бинарников (по сравнению со многими другими пакетами boost)
  • только однобуквенные короткие имена аргументов
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 21
  • 0
    В целом уже ничего, но иногда мне хочется наконец взять и написать нормальный парсер, но видимо на Qt, на голых плюсах локализацию неудобно делать.
    • +3
      Есть слишком много вещей, которые хочется взять и переписать с нуля, но времени на всё не хватит и значит с чем-то приходётся мириться. Если говорить о Qt, то мне гараздо больше не хватает вменяемых chart-ов.
    • +2
      Зря не упомянули о прикручивании response-файлов, а их не так-то уж и сложно прикручивать с помощью этой либы. Зато это сэкономило бы время читателю пытающегося прикрутить столь полезную фичу к тулзе

      >>скудная документация
      Нет. Очень даже на высоте! Даже такой ленивый как я и то за 1 час чтения разобрался

      Добавлю альтернативную библиотеку: tclap.sourceforge.net/
      • 0
        Я долго думал, что включить: custom parsers, config files. Решил остановится на том, что качует из проекта в проект и только это. Библиотека не очень большая, и самое тяжёлая задача убедить себя. что её использовать проще, чем написать очередной парсер самому.

        Мне было проще было читать код, чем доки, поэтому не очень хорошего о них мнения.
        • 0
          Там по сути нужно открыть только одну страницу и 2 примера, а дальше становится понятно и можно писать свое.
          Также не упомянули «скрытие» опции. Т.е. когда вы решили включить фичу в продукт эксперементально, чтобы ее потестили другие, но пока об это заявлять рано и поэтому вы в '--help' не показываете
          • 0
            Я полностью согласен, что это очень полезная опция и и возможность создания скрытых опции неявно следует из следующего кода. Никто ведь не обязан передавать все группы в --help. Например, так:
            desc.add(train_desc).add(recognize_desc);//.add(score_desc); std::cout << desc << std::endl;
      • 0
        А чем их не устраивал стандартный getopt_long()?
        • +2
          Видимо, тем что его на виндах нету.
          Но застав однажды такое вольное решение как svn.boost.org/trac/boost/ticket/850, сам предпочитаю getopt_long на *nix. (поясню — в версии 1.41 они _по-умолчанию_ стали вырезать кавычки из аргументов).
          • 0
            С boost таких косяков много, но мне он не критичен.
          • +2
            Ручная обработка ошибок на типы аргументов заставляет меня грустить.
          • +1
            Для информации, разбор параметров командной строки в библиотеке Poco: pocoproject.org/slides/190-Applications.pdf
            • +1
              Функция train нарушает принцип DRY.
              Что будет, если вздумается переименовать/добавить/удалить параметр?
              Разве нельзя автоматически привязать все параметры к переменным и использовать статическую типизацию для контроля на этапе компиляции?
              • 0
                Поддерживаю! Смысл в функции train
                ...
                if (vm.count("input")) {
                      input_path = vm["input"].as<std::string>();
                    }
                ...

                если все равно все параметры скорее всего предполагаются обязательными.
                • 0
                  Как раз vm.count(var_name) проверяет встречается ли такой параметр во входных данных, так что не все параметры обязательные.
                  • 0
                    >>vm.count(var_name)
                    А меня эта ф-ция бесит! ;( Противоречивая. В командной строке нельзя написать больше чем 1 аргумент, а название функции 'count' намекает на возможность нескольких. Мне больше нравится 'is_set', поэтому у меня возникают функции-врапперы. Они конечно лучше, т.к. код становится понятней, но не хочется писать того что лучше было в родной либе
                • 0
                  Отвечу в обратном порядке:

                  Про статическую типизацию не понял, все равно аргументы все строковые, а значит lexical_cast будет плеваться динамически. функция as статически типизирована

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

                  Я воспринимаю аргументы командной строки как внешний интерфейс: переименовывание/удаление/добавление аргументов означает изменение контракта на интерфейс со всеми вытекающими.

                  Насчёт, как это сделать DRY и можно ли это — надо подумать.
                • 0
                       ("ethanol,e",  po::value<std::string>(), "Etalon .trn file")
                  

                  Так этанол или эталон? :)
                  • 0
                    Я вот до сих пор не уверен насколько корректно использовать слово etalon. Более правильное pattern, standard. но pattern уже занято, а использовать standard мне очень не удобно. Ethanol хорошая альтернатива =).
                    • +1
                      Да нет же!
                      У Вас в первом случае написано «ethanol», что с английского переводится как "этанол" — одноатомный спирт.
                      А в другом случае у Вас написано «etanol», что переводится как "'эталон", т.е. средство измерения.
                      • 0
                        Запоздало замечу, что эта опечатка, судя по вашему комментарию, весьма распространена.
                        • 0
                          Ого, 4 года прошло!

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