
Вступление (можно пропустить)
Всем привет!
C++ всегда был моим основным языком программирования, начиная со стандарта C++11, который стал важным этапом в развитии языка. Несмотря на достаточно динамичные обновления в новых стандартах, в C++ остается много пробелов, которые стандарт не закрывает. Например, у нас до сих пор нет стандартного пакетного менеджера или системы сборки, как в современных языках программирования. Пока в других языках есть полноценные системы сборки, в C++ мы лишены такой роскоши.
Каждый проект должен для себя решить, нужно ли использовать менеджер пакетов: чистые репозитории, vcpkg или conan. Дальше необходимо выбрать систему сборки: нативные, cmake, waf или что-то ещё более экзотическое.
В дополнение к отсутствию нормальной системы сборки, прицепом идёт малое количество стандартных прикладных библиотек. Нет стандартной библиотеки для сетей, графики. Да хотя бы обычный JSON распарсить нельзя из коробки. Проблемы решаемые, но там целый зоопарк решений за пределами stl.
Для командной строки существует не так много хороших библиотек: CLI11, cxxopts, argparse и всемогущий Boost.ProgramOptions. Почти у всех них есть одна большая проблема — «первородный грех» многих C/C++ библиотек: мудрёный синтаксис. К счастью, за исключением Boost, все они являются single-header библиотеками, поэтому вам не нужно линковать их с чем-либо ещё, хоть где-то повезло.
Имея опыт работы с Python, где ситуация с библиотеками из коробки гораздо лучше, я решил создать свою собственную библиотеку для парсинга аргументов командной строки, вдохновленную Python-библиотекой argparse. Я считаю, что именно он должен быть ориентиром, как надо делать такие библиотеки. Но у нас, конечно же, так красиво не получится — язык пока так не позволяет сделать.
Но я попытался как можно ближе.
Встречайте Fancy Argument Parser
Вы можете её установить получить сорцы через ссылку Github или командную строчку
git clone https://github.com/simfeo/FancyArgumentParser# илиvcpkg install fancyargumentparser
Лицензия MIT позволяет очень вольное использование библиотеки
Основные возможности
-
Pure Single-Header: Библиотека состоит из одного файла (
argparse.h). Никаких компиляций исходных файлов или сложных настроек CMake. -
Python-подобный синтаксис (C++20): Если вы используете компилятор C++20, можно использовать инициализаторы, что дает чистый и лаконичный синтаксис.
-
Гибкий API: Вы можете выбрать стиль: fluent interface (цепочка вызовов), классический конструктор или современный стиль C++20.
-
Автоматические сокращения: Парсер понимает сокращенные флаги (например,
--verbпревращается в--verbose), если они уникальны. -
Контроль аргументов: Поддержка строгой проверки количества аргументов (точное количество, от нуля до бесконечности и т.д.).
-
Валидация значений (Choices): Можно ограничить список допустимых значений для флага.
-
Типобезопасность: Получение данных через
GetAsInt(),GetAsBool()и т.д.
Так же библиотека опирается на философию: выкинуть исключение для разработчика (для раннего отлова невозможных комбинаций), не выкидывать исключение для пользователя. Но хватит слов.
Фишки библиотеки
Можно поменять пространство имен через макрос:
#define ARGPARSE_NAMESPACE_NAME cli#include "argparse.h"int main(int argc, char** argv){ auto parser = cli::ArgumentParser("greet") .SetDescription("Custom namespace demo"); ...
Вы можете поменять префикс аргументов с «-» и «—» на любой другой символ, например «*»:
auto parser = argparse::ArgumentParser("sum") .SetDescription("Sum any amount of numbers") .SetPrefixChars('*'); //будет выглядить странно, но работает :-)
Поддержка булевых значений. Можно использовать ArgTypeCast::e_bool fдля логических значений. Поддерживаемые форматы true/True/TRUE и false/False/FALSE.
Можно назначить свой текст для примера использования описания и эпилога:
auto parser = argparse::ArgumentParser("server") .SetDescription("Start a web server") .SetUsage("serve --port PORT [--host HOST]") .SetEpilogue("This is example of epilogue. Will be placed in the end of help");
Вы можете сделать список разрешенных значений для конкретного аргумента:
// в этом примере пропущены многие перегрузки// по умолчанию у аргумента может быть только одно значение// сам аргумент по умолчанию формата std::stringparser.AddArgument( argparse::CreateNamedArgument("o", "operation") .SetRequired(true) .SetChoices({"+","-","*","/"}));
По умолчанию парсинг, для неизвестных аргументов, приведет к тому, что парсинг будет считаться невалидным, но можно это обойти флагом, для игнора неизвестных аргументов:
auto parser = argparse::ArgumentParser("sum") .SetDescription("Sum any amount of numbers") .SetPrefixChars('+') .SetIgnoreUnknownArgs(true);
Примеры использования
Хватит текста, время показать как реально пользоваться библиотекой. Для большего количества примеров, вы можете перейти на страничку вики библиотеки.
Если вы используете C++20, то вам доступен самый компактный синтаксис, который поддерживает библиотека. Просто добавьте хэдер куда-нибудь, где он может быть обнаружен:
#include <iostream>#include "argparse.h"int main(int argc, char** argv) { // инициализация объекта аргументов auto parser = argparse::ArgumentParser(__FILE__) .SetDescription("Description of program"); // Именованный параметр parser.AddArgument(argparse::CreateNamedArgument({ .longName = "numbers", .nargs = argparse::kFromOneToInfiniteArgCount, .type = argparse::ArgTypeCast::e_int })); // попытка распарсить auto obj = parser.ParseArgs(argc, argv); if (obj.IsArgValid() && !obj.GetArg("help").GetArgumentExists()) { auto arg = obj.GetArg("numbers"); for (auto& el : arg.GetAsVecInt()) { std::cout << el << std::endl; } } else { std::cout << parser.GetHelp(80) << std::endl; } return 0;}
Вывод для флага —help:
main.cpp -n,--numbers [n ...] [-s,--some_boring_long_name [s ...]] [-h,--help]Description of programnamed arguments:-n,--numbers Type: INT. Args count: at least one.-s,--some_boring_long_name some_boring_long_name description with some important information for user. Type: INT. Args count: at least one.-h,--help Show help!
Примечание: по умолчанию библиотека попытается сгенерировать короткий алиас, для аргумента, если это возможно. Что бы это выключить, есть отдельная опция:
auto parser = argparse::ArgumentParser("your program name") .SetDescription("Description of program") .SetAllowAbbrev(false);
Если у вас чуть более старый стандарт, то есть 2 других способа как можно написать тот же самый код.
Более короткая версия, но менее понятная версия:
#include <iostream>#include "argparse.h"int main(int argc, char** argv){ auto parser = argparse::ArgumentParser(__FILE__).SetDescription("Description of program"); parser.AddArgument(argparse::CreateNamedArgument("n", "numbers", argparse::kFromOneToInfiniteArgCount, argparse::ArgTypeCast::e_int, false, "some numbers description with some important information for user.")); parser.AddArgument(argparse::CreateNamedArgument("s", "some_boring_long_name", argparse::kFromOneToInfiniteArgCount, argparse::ArgTypeCast::e_int, false, "some_boring_long_name description with some important information for user.")); auto obj = parser.ParseArgs(argc, argv); if (obj.IsArgValid() && !obj.GetArg("help").GetArgumentExists()) { auto arg = obj.GetArg("numbers"); if (arg.GetArgumentExists()) { for (auto& el : arg.GetAsVecInt()) { std::cout << el << std::endl; } } } else { std::string help = parser.GetHelp(80); std::cout << obj.GetErrorString() << std::endl; std::cout << help << std::endl; } return 0;}
И куда более понятная (самодокументированная), но более многословная версия:
#include <iostream>#include "argparse.h"int main(int argc, char** argv){ auto parser = argparse::ArgumentParser("Program name") .SetDescription("Description of program"); parser.AddArgument(argparse::CreateNamedArgument() .SetLongName("numbers") .SetAnyNumberOfArgumentsButAtLeastOne() .SetType(argparse::ArgTypeCast::e_int)); parser.AddArgument(argparse::CreateNamedArgument() .SetLongName("some_boring_long_name") .SetAnyNumberOfArgumentsButAtLeastOne() .SetType(argparse::ArgTypeCast::e_int) .SetHelp("some_boring_long_name description with some important information for user.") .SetRequired(false)); auto obj = parser.ParseArgs(argc, argv); if (obj.IsArgValid() && !obj.GetArg("help").GetArgumentExists()) { auto arg = obj.GetArg("numbers"); if (arg.GetArgumentExists()) { for (auto& el : arg.GetAsVecInt()) { std::cout << el << std::endl; } } } else { std::string help = parser.GetHelp(80); std::cout << obj.GetErrorString() << std::endl; std::cout << help << std::endl; } return 0;}
Ялично бы предпочел более многословную версию, так как она куда понятнее, и не требует запоминать где какой аргумент надо передать в конструкторе для аргумента.
Другой пример. Только один обязательный аргумент, с только одним возможным количеством аргументов:
#include <iostream>#include "ArgParse/argparse.h"int main(int argc, char** argv){ auto parser = argparse::ArgumentParser("main") .SetDescription("ArgParse example"); parser.AddArgument(argparse::CreateNamedArgument({ .shortName = "bk", .longName = "b_key", .nargs = 1, .type = argparse::ArgTypeCast::e_int, .required = true, .help = R"=(some "b_key" description with some important information for user)="})); parser.SetAddHelp(false); auto obj = parser.ParseArgs(argc, argv); if (obj.IsArgValid() && !obj.GetArg("help").GetArgumentExists()) { auto arg = obj.GetArg("b_key"); std::cout << arg.Get().type().name() << ": " << std::any_cast<int>(arg.Get()) << std::endl; } else { std::string help = parser.GetHelp(80); std::cout << obj.GetErrorString() << std::endl; std::cout << help << std::endl; } return 0;}
Аргументы могут быть двух типов: именованные и позиционные. Позиционные аргументы должны быть переданы в командную строчку, до первого именованного. Это обязательно. Вот пример, как этим пользоваться:
#include <iostream>#include "ArgParse/argparse.h"int main(int argc, char** argv){ auto parser = argparse::ArgumentParser("main").SetDescription("ArgParse example"); parser.AddArgument(argparse::CreatePositionalArgument({ .name = "int1", .type = argparse::ArgTypeCast::e_int, .required = false})); // тоже самое в стиле C++11 // parser.AddArgument( // argparse::CreatePositionalArgument("int1") // .SetType(argparse::ArgTypeCast::e_int) // .SetRequired(false)); auto obj = parser.ParseArgs(argc, argv); if (obj.IsArgValid() && !obj.GetArg("help").GetArgumentExists()) { auto arg = obj.GetArg("int1"); if (arg.GetArgumentExists()) { std::cout << arg.Get().type().name() << ": " << std::any_cast<int>(arg.Get()) << std::endl; } } else { std::string help = parser.GetHelp(80); std::cout << obj.GetErrorString() << std::endl; std::cout << help << std::endl; } return 0;}
P.S.
Я старался создать библиотеку с простым и дружелюбным синтаксисом. Она гибкая, но при этом не перегружена лишней функциональностью, что делает ее идеальной для небольших проектов.
Библиотека распространяется под лицензией MIT. Вы можете найти её на GitHub под ником simfeo/FancyArgumentParser или установить через vcpkg.
ссылка на оригинал статьи https://habr.com/ru/articles/1060714/