Занимаясь разработкой алгоритмов, постоянно одергиваю себя, а вдруг изменения, которые работают на небольшом примере, привнесут разброд и шатание в результаты на других, больших данных. Тогда мне на помощь приходит командная строка. Самое ужасное, что каждый раз реализовывать парсер аргументов уже надоело, а значит, не последним средством для 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)
- только однобуквенные короткие имена аргументов
Автор: kokorins