Вступление (можно пропустить)

Всем привет!

C++ всегда был моим основным языком программирования, начиная со стандарта C++11, который стал важным этапом в развитии языка. Несмотря на достаточно динамичные обновления в новых стандартах, в C++ остается много пробелов, которые стандарт не закрывает. Например, у нас до сих пор нет стандартного пакетного менеджера или системы сборки, как в современных языках программирования. Пока в других языках есть полноценные системы сборки, в C++ мы лишены такой роскоши.

Каждый проект должен для себя решить, нужно ли использовать менеджер пакетов: чистые репозитории, vcpkg или conan. Дальше необходимо выбрать систему сборки: нативные, cmake, waf или что-то ещё более экзотическое.

В дополнение к отсутствию нормальной системы сборки, прицепом идёт малое количество стандартных прикладных библиотек. Нет стандартной библиотеки для сетей, графики. Да хотя бы обычный JSON распарсить нельзя из коробки. Проблемы решаемые, но там целый зоопарк решений за пределами stl.

Для командной строки существует не так много хороших библиотек: CLI11cxxoptsargparse и всемогущий 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::string
parser.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 program

named 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;
}

Аргументы могут быть двух типов: именованные и позиционные. Позиционные аргументы должны быть переданы в командную строчку, до первого именованного. Это обязательно. Вот пример, как этим пользоваться:

Краткий гид по альтернативам: что выбрать?

Давайте честно: велосипеды пишутся не от хорошей жизни. Для C++ есть проверенные временем инструменты, но у каждого из них свой «порог вхождения» и эстетические проблемы.

Чтобы понять, почему существующие библиотеки обладают тем самым «первородным грехом» перегруженного синтаксиса, о котором я говорил выше, давайте сравним их в боевых условиях.

Задача простейшая: объявить обязательный параметр --file (-f) и булевый флаг --verbose (-v).

1. CLI11 - Мощно, но многословно

Популярный, надежный инструмент, который часто пихают во все крупные проекты. Но здесь правит бал концепция binding. Вам нужно сначала объявить переменные в локальной видимости, а затем передать их по адресу в парсер.

CLI::App app{"App description"};
std::string file;
bool verbose = false;

app.add_option("-f,--file", file, "Path to file")->required();
app.add_flag("-v,--verbose", verbose, "Verbose mode");

CLI11_PARSE(app, argc, argv);

Что в нем не так: Код размазывается. Вы не можете просто объявить аргумент в одном месте - вам приходится плодить локальные переменные еще до того, как парсер вообще начал работу.

2. cxxopts - Минимализм с налётом макросов

Легковесная библиотека, которая пытается быть простой, но синтаксис добавления опций через перегруженный оператор () выглядит довольно специфично.

cxxopts::Options options("App", "Description");

options.add_options()
    ("f,file", "Path to file", cxxopts::value<std::string>())
    ("v,verbose", "Verbose mode", cxxopts::value<bool>()->default_value("false"));

auto result = options.parse(argc, argv);
auto file = result["file"].as<std::string>();

Что в нем не так: Получение значений через .as<T>() - это оверхед на ровном месте. К тому же автоматизации тут минимум, а кастомизация вывода справки навевает тоску.

3. argparse (by p-ranav) - Попытка в Python на минималках

Автор этой библиотеки явно вдохновлялся Python (как и я), но споткнулся о суровую реальность C++. Из-за отсутствия в языке именованных параметров автору пришлось строить бесконечные цепочки методов (Fluent API).

argparse::ArgumentParser program("App");

program.add_argument("-f", "--file").required().help("Path to file");
program.add_argument("-v", "--verbose").implicit_value(true).default_value(false);

program.parse_args(argc, argv);
auto file = program.get<std::string>("--file");

Что в нем не так: Код неминуемо растягивается по вертикали. Стоит добавить 5–6 аргументов со своими правилами, и ваш main превращается в бесконечную простыню из точек и вызовов методов. Требует строго C++17. Впрочем, у меня на старых версиях стандарта тоже придется городить такой огород.

4. Boost.ProgramOptions - Монстр из прошлого

Классический enterprise-стандарт. Синтаксис монументален, как советский брутализм.

po::options_description desc("Allowed options");
desc.add_options()
    ("file,f", po::value<std::string>()->required(), "Path to file")
    ("verbose,v", "Verbose mode");

po::variables_map vm;
po::store(po::parse_command_line(argc, argv, desc), vm);
po::notify(vm); // Без этого магия валидации required-полей не заработает

auto file = vm["file"].as<std::string>();

Что в нем не так: Всё. Начиная с необходимости тянуть и линковать тяжеловесный Boost (забудьте про single-header), заканчивая ручным вызовом notify() и монструозной картой переменных variables_map.

5. FancyArgumentParser - Как это сделано у меня

А теперь посмотрим, как этот же сценарий выглядит с использованием назначенных инициализаторов C++20. Код становится чисто декларативным, параметры упакованы в понятную структуру, а чтение аргументов интуитивно.

auto parser = FancyArgumentParser();

parser.Add({.long_name="file",    .short_name='f', .required=true, .help="Path to file"});
parser.Add({.long_name="verbose", .short_name='v', .action=Action::SetTrue});

parser.Parse(argc, argv);
auto file = parser.GetAsString("file");

Сводная таблица: где билиотека выигрывает, а где уступаю

Чтобы окончательно разложить всё по полочкам, я собрал краткую матрицу компромиссов. Идеальных библиотек не бывает, и у каждого инструмента своя ниша.

Библиотека

Философия синтаксиса

Главная «киллер-фича»

Главная головная боль

Как поставляется

FancyArgumentParser

Modern C++20 structures / Fluent

Декларативность, авто-сокращения флагов из коробки

Проект совсем свежий, пока мало встроенных валидаторов

Single-Header

CLI11

Variable Binding (указатели)

Мощнейшая валидация (проверка путей, IP, диапазонов чисел)

Избыточный синтаксический оверхед для простых утилит

Single-Header

cxxopts

Функциональный (операторы)

Быстрая компиляция, проверен годами

Спартанский help, слабая автоматизация

Single-Header

argparse

Fluent API (цепочки методов)

Знакомый синтаксис для тех, кто часто пишет на Python

Код сильно раздувается в длину, требует строго C++17

Single-Header

Boost.ProgramOptions

Архаичный (макросы и variables_map)

Умеет «из коробки» парсить файлы конфигурации (.ini, .cfg)

Главный кошмар: требует полноценной сборки и линковки Boost

Нужна линковка

Короткий вердикт: Если вам нужна тяжелая промышленная валидация путей или интеграция с конфигами - CLI11 и Boost всё еще вне конкуренции. Но если вы хотите писать аккуратный, лаконичный код на современном C++20, не отвлекаясь на бойлерплейт - FancyArgumentParser создавался именно для этого.

P.S.

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

Библиотека распространяется под лицензией MIT. Вы можете найти её на GitHub под ником simfeo/FancyArgumentParser или установить через vcpkg.

Комментарии (7)


  1. iWowik
    20.07.2026 08:41

    Ура! У нас теперь есть еще один набор "самописных костылей"!


  1. monah_tuk
    20.07.2026 08:41

    Имхо, cxxopts на фоне выглядит куда более естественным в плане синтаксиса


  1. eao197
    20.07.2026 08:41

    В копилку хороших парсеров аргументов командной строки надо бы добавить еще и args с Lyra

    А статье, имхо, не хватает перечня недостатков в существующих библиотеках и компенсирующих их достоинств в FancyArgumentParser. Типа “вот в XX и в YY вот это не делается никак, а в ZZ – только через тридцать три лишних приседания, тогда как в FancyArgumentParser – двумя с половиной строчками”.

    Без такого перечня складывается ощущение, что у всех отвергнутых автором библиотек был всего лишь один фатальный недостаток…


  1. SilverTrouse
    20.07.2026 08:41

    И среди всех этих бибилотек только argparse и cli11 умeют в модули. Вы ввели 20 стандарт в своей либе, но не сделали это без уважения.


    1. idimus Автор
      20.07.2026 08:41

      Оличное замечание, возожмно стоит добавить)

      Этим и полезно выкладывать вещи на всеобщее обозрение.

      Единственное замечание, что модули пока что нишевая штука, хоть на дворе 2026-ой. Вон XCode до сих пор их не поддерживает.


      1. SilverTrouse
        20.07.2026 08:41

        напомню что еще стоит учитывать, что может подключатся import std из 23 стандарта


      1. SilverTrouse
        20.07.2026 08:41

        Единственное замечание, что модули пока что нишевая штука, хоть на дворе 2026-ой. Вон XCode до сих пор их не поддерживает.

        Ну буст часть своих либ первел на модули, а где-то еще вносится поддержка. Да переход очень тяжелый на с++ моудли, но после опыта написания на модулях, обратно очень неприятно переходить