Pull to refresh

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

C++ *
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)
  • только однобуквенные короткие имена аргументов
Tags:
Hubs:
Total votes 35: ↑33 and ↓2 +31
Views 35K
Comments Comments 22

Please pay attention