Простая в использовании, мощная и выразительная обработка аргументов командной строки для C++11/14/17, содержащаяся в одном заголовочном файле .
параметры, параметры+значения, позиционные значения, позиционные команды, вложенные альтернативы, деревья решений, присоединяемые флаги, фильтры настраиваемых значений, ...
создание документации (строки использования, страницы руководства); обработка ошибок
множество примеров; большой набор тестов
Рассмотрим этот интерфейс командной строки:
СИНОПСИС Convert <входной файл> [-r] [-o <выходной формат>] [-utf16] ПАРАМЕТРЫ -r, --recursive рекурсивно конвертировать файлы -utf16 использовать кодировку UTF-16
Вот код, который определяет input file
позиционного значения и три параметра -r
, -o
и -utf16
. Если синтаксический анализ не удался, приведенный выше фрагмент, похожий на справочную страницу по умолчанию, будет выведен на стандартный вывод.
#include <iostream>#include "clipp.h" с использованием пространства имен clipp; используя std::cout; используя std::string;int main(int argc, char* argv[]) { bool Rec = false, utf16 = false; string infile = "", fmt = "csv";auto cli = (value("входной файл", infile),option("-r", "--recursive").set(rec).doc("конвертировать файлы рекурсивно"),option("-o") & value("формат вывода", fmt),option("-utf16").set(utf16).doc("использовать кодировку UTF-16") );if(!parse(argc, argv, cli)) cout << make_man_page(cli, argv[0]);// ...}
СИНОПСИС finder make <файл_слова> -dict <словарь> [--progress] [-v] finder find <входной файл>... -dict <словарь> [-o <выходной файл>] [-split|-nosplit] [-v] помощь в поиске [-v] ПАРАМЕТРЫ --progress, -p показать прогресс -o, --output <выходной файл> писать в файл вместо стандартного вывода -split, -nosplit (не разделять) вывод -v, --version показать версию
Этот CLI имеет три альтернативные команды ( make
, find
, help
), несколько позиционных аргументов-значений ( <wordfile>
, <infile>
), один из которых является повторяемым, обязательный флаг с аргументом-значением ( -dict <dictionary>
), опция со значением-аргументом ( -o <outfile>
), одна опция с двумя альтернативами ( -split
, -nosplit
) и две обычные опции ( -v
, --progress
).
Вот код, который определяет интерфейс, генерирует приведенный выше фрагмент справочной страницы и обрабатывает результат анализа:
использование пространства имен clipp; используя std::cout; использование std::string;//переменные, хранящие результат анализа; инициализируются значениями по умолчанию, режим класса enum {make, find, help}; выбранный режим = режим::help; std::vector<string> input; string dict, out;bool Split = false, progr = false;auto словарь = требуется("-dict") & value("словарь", dict);auto makeMode = (команда("make").set(selected,mode ::делать), значения («файл_слова», ввод), словарь, option("--progress", "-p").set(progr) % "показать прогресс" );auto findMode = (command("find").set(selected,mode::find), значения («входной файл», ввод), словарь, (option("-o", "--output") & value("outfile", out)) % "записывать в файл вместо стандартного вывода", (option("-split").set(split,true) | option("-nosplit").set(split,false)) % "(не) разделять вывод" );auto cli = ( (makeMode | findMode | команда("help").set(selected,mode::help) ),option("-v", "--version").call([]{cout << "версия 1.0nn" ;}).doc("показать версию") );if(parse(argc, argv, cli)) {switch(selected) {case mode::make: /* ... */break;case mode::find: /* ... */break;case mode::help: cout << make_man_page(cli, "finder"); перерыв; } } еще { cout << use_lines(cli, "finder") << 'n'; }
Ниже приведены несколько примеров, которые должны дать вам представление о том, как работает clipp. Рассмотрим эту базовую настройку с несколькими переменными, которые мы хотим установить с помощью аргументов командной строки:
int main(int argc, char* argv[]) { using namespace clipp;// определяем некоторые переменныеbool a = false, b = false;int n = 0, k = 0;double x = 0.0, y = 0.0; std::vector<int> ids;auto cli = ( /* ЗДЕСЬ НАХОДИТСЯ КОД, ОПРЕДЕЛЯЮЩИЙ ИНТЕРФЕЙС КОМАНДНОЙ СТРОКИ */ );parse(argc, argv, cli); //исключаем argv[0]std::cout << use_lines(cli, "exe") << 'n'; }
Интерфейс ( usage_lines ) | Код (содержимое круглых скобок cli ) |
---|---|
exe [-a] | option("-a", "--all").set(a) |
exe [--all] | option("--all", "-a", "--ALL").set(a) |
exe [-a] [-b] | option("-a").set(a), option("-b").set(b) |
exe -a | required("-a").set(a) |
exe [-a] -b | option("-a").set(a), required("-b").set(b) |
exe [-n <times>] | option("-n", "--iter") & value("times", n) |
exe [-n [<times>]] | option("-n", "--iter") & opt_value("times", n) |
exe -n <times> | required("-n", "--iter") & value("times", n) |
exe -n [<times>] | required("-n", "--iter") & opt_value("times", n) |
exe [-c <x> <y>] | option("-c") & value("x", x) & value("y", y) |
exe -c <x> <y> | required("-c") & value("x", x) & value("y", y) |
exe -c <x> [<y>] | required("-c") & value("x", x) & opt_value("y", y) |
exe [-l <lines>...] | option("-l") & values("lines", ids) |
exe [-l [<lines>...]] | option("-l") & opt_values("lines", ids) |
exe [-l <lines>]... | repeatable( option("-l") & value("lines", ids) ) |
exe -l <lines>... | required("-l") & values("lines", ids) |
exe -l [<lines>...] | required("-l") & opt_values("lines", ids) |
exe (-l <lines>)... | repeatable( required("-l") & value("lines", ids) ) |
exe fetch [-a] | command("fetch").set(k,1), option("-a").set(a) |
exe init | fetch [-a] | command("init").set(k,0) | (command("fetch").set(k,1), option("-a").set(a)) |
exe [-a|-b] | option("-a").set(a) | option("-b").set(b) |
exe [-ma|b] | option("-m") & (required("a").set(a) | required("b").set(b)) |
Подробные объяснения каждой темы см. в разделе примеров.
Квалификаторы пространства имен опущены во всех примерах для лучшей читаемости. Все сущности определены в namespace clipp
.
int main(int argc, char* argv[]) { using namespace clipp;auto cli = ( /* ЗДЕСЬ НАХОДИТСЯ КОД, ОПРЕДЕЛЯЮЩИЙ ИНТЕРФЕЙС КОМАНДНОЙ СТРОКИ */ );parse(argc, argv, cli); //исключает argv[0]//если вы хотите включить argv[0]//parse(argv, argv+argc, cli);}
Существует два типа строительных блоков для интерфейсов командной строки: параметры и группы. Заводские функции с удобными названиями создают параметры или группы с примененными желаемыми настройками.
bool a = ложь, f = ложь; строка с; вектор<string> vs;auto cli = ( // соответствует требуемой позиционной повторяемой команде("push"), // точно да да norequired("-f", "--file").set(f), // точно да нет norequired("-a", "--all", "-A").set(a), // точно нет нет нет value("file", s), // любой аргумент да да novalues("file", vs), // любой аргумент да да yesopt_value("file", s), // любой аргумент нет да noopt_values("file" , vs), // любой аргумент нет да да // параметр «catch all» — полезен для обработки ошибокany_other(vs), // любой аргумент нет нет да // перехватывает аргументы, которые соответствуют предикату и не совпадают с другими optionsany(predicate, vs) // предикат нет нет да);
Вышеуказанные функции являются удобными фабриками:
bool f = правда; string s;auto v1 = Values("file", s);// эквивалентно:auto v2 = параметр{match::nonempty}.label("file").blocking(true).repeatable(true).set (s);auto r1 = требуется("-f", "--file").set(f);// эквивалентно:auto r2 = параметр{"-f", "--file"}.required (истина).set(f);
обязательный параметр должен соответствовать хотя бы одному аргументу командной строки
повторяемый параметр может соответствовать любому количеству аргументов
непозиционные (=неблокирующие) параметры могут соответствовать аргументам в любом порядке
позиционный (блокирующий) параметр определяет «точку остановки», т.е. до тех пор, пока он не совпадет со всеми параметрами, следующими за ним, совпадение не допускается; как только он совпадет, все предшествующие ему параметры (в текущей группе) станут недоступными
Если вы хотите, чтобы параметры сопоставлялись последовательно, вы можете связать их вместе с помощью operator &
или функции группировки in_sequence
:
интервал п = 1; строка с; вектор<int> ls;auto cli = (//опция с обязательным значениемoption("-n", "--repeat") & value("times", n),//обязательный флаг с необязательным значениемrequired("--file ") & opt_value("name", s), //опция ровно с двумя значениямиoption("-p", "--pos") & value("x") & value("y"),//то же, что и до v vin_sequence( option("-p", "--pos"), value("x"), value("y")), //опция хотя бы с одним значением (и необязательно больше)option("-l" ) & значения("линии", ls) );
Параметры-значения используют функцию фильтра, чтобы проверить, разрешено ли им соответствовать строке аргумента. Фильтр match::nonempty
по умолчанию, используемый value
, values
, opt_value
и opt_values
будет соответствовать любой непустой строке аргумента. Вы можете либо предоставить другие функции фильтра/функциональные объекты в качестве первого аргумента value
, values
и т. д., либо использовать одну из этих встроенных сокращенных фабричных функций, охватывающих наиболее распространенные случаи:
имя строки; двойной г = 0,0; int n = 0;auto cli = (value("user", name), // соответствует любому непустому строковому слову("user", name), // соответствует любому непустому буквенно-цифровому stringnumber("ratio", r) , // соответствует строковому представлению чисел integer("times", n) // соответствует строковому представлению целых чисел);
Аналогично value
, opt_value
и т. д. существуют также функции для words
, opt_word
и т. д.
auto is_char = [](const string& arg) { return arg.size() == 1 && std::isalpha(arg[0]); };char c = ''; // соответствует требуемому позиционному повторяемому значению(is_char, "c", c); // один символ да да нет
группируйте взаимно совместимые параметры с помощью круглых скобок и запятых:
auto cli = (option("-a"), option("-b"), option("-c") );
группировать взаимоисключающие параметры как альтернативы с помощью operator |
или one_of
:
auto cli1 = ( value("input_file") | команда("список") | команда("flush") );auto cli2 = one_of( value("input_file") , команда("список") , команда("flush" ) );
группируйте параметры так, чтобы они сопоставлялись последовательно с помощью operator &
или in_sequence
:
double x = 0, y = 0, z = 0;auto cli1 = ( option("-pos") & value("X",x) & value("Y",y) & value("Z",z ) );auto cli2 = in_sequence( option("-pos"), value("X",x) , value("Y",y) , value("Z",z) );
Обратите внимание, что это не влияет на окружающие группы, поэтому -a
и -b
могут сопоставляться в любом порядке, тогда как -b
и значение X
должны совпадать последовательно:
bool a = ложь, b = ложь; int x = 0;auto cli = ( option("-a").set(a), option("-b").set(b) & value("X",x) );
группы могут быть вложены и объединены для формирования сколь угодно сложных интерфейсов (см. здесь и здесь):
auto cli = (команда("push") | (команда("тянуть"), option("-f", "--force") ) );
группы также могут повторяться:
auto cli1 = повторяемый (команда («флип») | команда («флоп»));
принудительно использовать общие префиксы для группы флагов:
int x = 0;auto cli1 = with_prefix("-", option("a"), option("b") & value("x",x), ... ); // => -a -b ^unaffected^auto cli2 = with_prefix_short_long("-", "--", option("a", "all"), option("b"), ... ); // => -a --all -b
принудительно использовать общие суффиксы для группы флагов:
int x = 0;auto cli1 = with_suffix("=", option("a") & value("x",x), ... ); // => a= ^unaffected^auto cli2 = with_suffix_short_long(":", ":=", option("a", "all"), option("b"), ... ); // => a: все:= b:
сделать группу флагов объединяемой:
auto cli1 = joinable(option("-a"), option("-b")); //будет соответствовать "-a", "-b", "-ab", "-ba"//работает также с произвольными общими префиксами:auto cli2 = joinable( option("--xA0"), option("- -xB1")); //также будет соответствовать "--xA0B1" или "--xB1A0"
Самый простой способ подключить интерфейс командной строки к остальной части вашего кода — это привязать значения объектов или вызовы функций (объектов) к параметрам (см. также здесь):
логический б = ложь; интервал я = 5; интервал м = 0; строка х; ifstream fs;auto cli = ( option("-b").set(b), // обнаружен "-b" -> установите b в trueoption("-m").set(m,2), // " -m" обнаружено -> установить m в 2option("-x") & value("X", x), // установить значение x из строки аргумента option("-i") & opt_value("i", i) , // устанавливаем значение i из строки аргумента option("-v").call( []{ cout << "v"; } ), // вызов функции (объекта) /lamdaoption("-v")( []{ cout << "v"; } ), // то же, что и предыдущее lineoption("-f") & value("file").call([&](string f){ fs.open(f); }) );
В производственном коде, вероятно, можно было бы использовать класс настроек:
настройки структуры {bool x = false; /* ... */ }; settings cmdline_settings(int argc, char* argv[]) { settings s;auto cli = ( option("-x").set(sx), /* ... */ );parse(argc, argv, cli);return s; }
Обратите внимание, что цель должна быть либо:
фундаментальный тип ( int, long int, float, double, ...
)
тип, который можно преобразовать из const char*
вызываемая сущность: функция, функциональный объект/лямбда, которая имеет либо пустой список параметров, либо ровно один параметр, который можно преобразовать из const char*
Строки документации для групп и параметров можно задать либо с помощью функции-члена doc
, либо с помощью operator %
:
автокли = ( ( option("x").set(x).doc("устанавливает X"),option("y").set(y) % "устанавливает Y" ), "документированная группа 1:" % ( option("-g").set(g).doc("активирует G"), option("-h").set(h) % "активирует H" ), ( option("-i").set(i) % "активирует I", option("-j").set(j) % "активирует J" ).doc("документированная группа 2:") );
Линии использования:
cout << use_lines(cli, "progname") << 'n';//с параметрами форматированияauto fmt = doc_formatting{} .first_column(3) .last_column(79); cout << use_lines(cli, "progname", fmt) << 'n';
Подробная документация:
cout << документация(cli) << 'n';//с параметрами форматированияauto fmt = doc_formatting{} .first_column(7) .doc_column(15) .last_column(99); cout << документация (cli, fmt) << 'n';
Страницы руководства:
auto cli = ( /*КОД, ОПРЕДЕЛЯЮЩИЙ ИНТЕРФЕЙС КОМАНДНОЙ СТРОКИ, НАХОДИТСЯ ЗДЕСЬ*/ ); cout << make_man_page(cli, "progname") << 'n';//с параметрами форматированияauto fmt = doc_formatting{} .first_column(7) .doc_column(15) .last_column(99); cout << make_man_page(cli, "progname", fmt) << 'n';
К каждому параметру могут быть прикреплены функции обработчика событий. Они вызываются один раз для каждого аргумента, сопоставленного с параметром (или один раз для каждого отсутствующего события):
строковый файл = "default.txt";авто параметр = требуется("-nof").set(file,"") | требуется («-f») и значение («файл», файл) // при 2-м, 3-м, 4-м,... совпадении (в этом случае будет ошибкой) .if_repeated( [] { /* ... */ } ) // если требуемый параметр-значение отсутствует .if_missing( [] { /* ... */ } ) // если недоступен, например, нет флага "-f" перед именем файла .if_blocked( [] { /* ... */ } ) // если совпадение конфликтует с другой альтернативой "-nof" .if_conflicted( [] { /* ... */ } );
Функции-обработчики также могут принимать целое число, которому присвоен индекс аргумента, в котором событие произошло первым:
строковый файл = "default.txt";авто параметр = требуется("-nof").set(file,"") | требуется («-f») и значение («файл», файл) .if_repeated ( [] (int argIdx) { /* ... */ } ) .if_missing ( [] (int argIdx) { /* ... */ } ) .if_blocked ( [] (int argIdx) { /* ... */ } ) .if_conflicted( [] (int argIdx) { /* ... */ } );
Если мы укажем -f -b
или -b -f -a
в качестве аргументов командной строки для следующего CLI, будет сообщено об ошибке, поскольку значение после -f
не является необязательным:
auto cli = ( option("-a"), option("-f") & value("filename"), option("-b") );
Такое поведение подходит для большинства случаев использования. Но что, если мы хотим, чтобы наша программа принимала в качестве имени файла любую строку, поскольку имена наших файлов также могут конфликтовать с именами флагов? Мы можем сделать параметр значения жадным с помощью operator !
. Таким образом, следующая строка после -f
всегда будет соответствовать наивысшему приоритету, как только была указана -f
:
auto cli = ( option("-a"), option("-f") & !value("filename"), option("-b") );// ^~~~~~
Будьте очень осторожны с жадными параметрами!
auto cli = ( /* здесь ваш интерфейс */ );auto res = parse(argc, argv, cli);if(res.any_error()) { /* ... */ }//агрегированные ошибкиif(res.unmapped_args_count ()) { /* ... */ }if(res.any_bad_repeat()) { /* ... */ }if(res.any_blocked()) { /* ... */ }if(res.any_conflict()) { /* ... */ }