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

Парсинг параметров командной строки попадает в кошмарную зону сложности. Это не настолько тривиально, что существует один очевидный подход, и не настолько сложно, чтобы быть исключительно областью научных статей, публикуемых экспертами в предметной области. Скорее, это доступная проблема, которую каждый хочет попробовать решить, а затем поскорее написать пост в блоге о своем умном маленьком решении, из-за чего результаты поиска просто затоплены бесконечным посредственным кодом.

И это достаточно распространенная проблема. Мой младший брат недавно пытался найти решения для задачи разбиения множества чисел, лучшие алгоритмы для которой хорошо известны в академических кругах. К своему ужасу, он напоролся на множество постов на StackOverflow, описывающих совершенно недееспособные решения, которые даже не реализуют известные алгоритмы.

С учетом вышеизложенного, вот мое умное маленькое решение.

Немного смышлености

Требования к моему обработчику argv будут следующими:

  • Достаточно декларативный

  • Генерирует POD-структуру

  • Никаких внешних зависимостей

  • Простое добавление и удаление настроек

Так как я хочу, чтобы на выходе получалась POD-структура, это послужит отличной отправной точкой:

Листинг 1

// Это заголовки, которые мы будем использовать во всем этом посте
#include <functional>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
#include <unordered_map>

// Только для примера! В реальном коде так делают только всякие безбожники
using namespace std;
struct MySettings {
 bool help {false};
 bool verbose {false};
 optional<string> infile;
 optional<string> outfile;
 optional<int> value;
};

Несколько замечаний по коду в Листинге 1:

  • help и verbose являются логическими значениями, которые можно устанавливать с помощью простых флагов.

  • outfile требует параметр и не может быть установлен простым флагом.

  • infile не предполагает никаких флагов, устанавливается, когда параметр не является распознанным флагом.

  • value не является ни строкой, ни логическим значением, поэтому необходимо будет выполнять какое-то преобразование.

Мой излюбленный способ ветвления на основе строк — использовать map над std::function:

Листинг 2

typedef function<void(MySettings&)> NoArgHandle;
/**
* Мы также можем использовать здесь указатель на функцию, например:
* typedef void (NoArgHandle)(MySettings&);
* Если мы собираемся использовать в качестве дескрипторов только простые функции или лямбда-выражения без захвата параметров, простой указатель на функцию будет немного более эффективен.
**/

const unordered_map<string, NoArgHandle> NoArgs {
 {"--help", [](MySettings& s) { s.help = true; }},
 {"-h", [](MySettings& s) { s.help = true; }},
 {"--verbose", [](MySettings& s) { s.verbose = true; }},
 {"-v", [](MySettings& s) { s.verbose = true; }},
 {"--quiet", [](MySettings& s) { s.verbose = false; }},
};

В Листинге 2 мы видим неупорядоченную map, при поиске по строке в которой будет выдаваться лямбда (или другой std::function-прибамбас), которая сможет выполнить соответствующую модификацию MySettings. Если это показалось вам чрезвычайно очевидным и простым, то это потому, что это так и есть.

С точки зрения стиля здесь может быть уместно использование макроса с целью свести к минимуму повторение и сделать код более декларативным:

Листинг 3

typedef function<void(MySettings&)> NoArgHandle;

#define S(str, f, v) {str, [](MySettings& s) {s.f = v;}}
const unordered_map<string, NoArgHandle> NoArgs {
  S("--help", help, true),
  S("-h", help, true),

  S("--verbose", verbose, true),
  S("-v", verbose, true),

  S("--quiet", verbose, false),
};
#undef S

Конечно, существуют религиозные течения, которые учат, что препроцессор C — от лукавого. Но лично для меня моя бессмертная душа значит все меньше и меньше, поэтому макросом я не побрезгую.

Для параметров с одним аргументом мы просто повторяем этот подход. Наши лямбда-выражения теперь будут принимать два аргумента, первый из которых структура MySettings, а второй — строка:

Листинг 4

typedef function<void(MySettings&, const string&)> OneArgHandle;

#define S(str, f, v) \
  {str, [](MySettings& s, const string& arg) { s.f = v; }}

const unordered_map<string, OneArgHandle> OneArgs {
  // Записываем всю лямбду
  {"-o", [](MySettings& s, const string& arg) {
    s.outfile = arg;
  }},

  // Используем макрос
  S("--output", outfile, arg),

  // Выполняем преобразования string -> int
  S("--value", value, stoi(arg)),
};
#undef S

С этого момента должно быть очевидно, что этот подход можно масштабировать на поддержку флагов с произвольным количеством аргументов и сложностью парсинга. Все, что нам осталось сделать, это собрать все кусочки воедино и наконец-таки распарсить argv.

Собираем все воедино

Нам понадобится функция, которая делает четыре вещи в следующем порядке:

  1. Проверяет, является ли строка из argv NoArgs-опцией, и если это так, вызывает соответствующий обработчик

  2. Проверяет, является ли строка OneArg-опцией, и, если это так, извлекает строковый аргумент (пробрасывая исключения, если такого аргумента не существует), затем вызывает соответствующий обработчик.

  3. Проверяет, был установлен infile, и если нет, то устанавливает в него строку.

  4. Предупреждает, если infile уже установлен и флаг не распознан

Давай напишем ее:

Листинг 5

MySettings parse_settings(int argc, const char* argv[]) {
  MySettings settings;

  // argv[0] — традиционно имя программы, поэтому начинайте с 1
  for(int i {1}; i < argc; i++) {
    string opt {argv[i]};

    // Это NoArgs?
    if(auto j {NoArgs.find(opt)}; j != NoArgs.end())
      j->second(settings); // Да, справляйся!

    // Нет, а что насчет OneArg?
    else if(auto k {OneArgs.find(opt)}; k != OneArgs.end())
      // Да. А у нас есть параметр?
      if(++i < argc)
        // Да, обрабатываем!
        k->second(settings, {argv[i]});
      else
        // Нет, и мы не можем продолжать, выкидываем исключение.
        throw std::runtime_error {"missing param after " + opt};

    // Нет, а infile уже установлен?
    else if(!settings.infile)
      // Нет, используем это в качестве входного файла
      settings.infile = argv[i];

    // Да, возможно пробрасываем исключение или просто выводим ошибку
    else
      cerr << "unrecognized command-line option " << opt << endl;
  }

  return settings;
}

Это минус целых 50 dkp, если вы не справитесь с этим!

Пара слов в заключение

Парсинг argv не является сложной проблемой, и я не претендую на то, что представленное здесь решение является инновационным или новаторским. Тем не менее, если верить интернету, для обработки этих скромных флагов требуется как минимум один платформоспецифичный фреймворк или один заголовочный файл, скопированный у доктора Доббса (Dr. Dobb) где-то в 2003 году.

Зависимости — это не обязательно плохо, но C++ — это не Javascript. Нет необходимости задействовать сторонние фреймворки для решения проблемы, с которой мы можем разобраться сами в несколько десятков строк кода.


Завтра вечером пройдет открытое занятие на тему «Объектно-ориентированное программирование средствами C++».

Что будет на занятии: инкапсуляция, наследование и полиморфизма глазами C++ программиста; преимущества и недостатки активного использования полиморфизма в программах.

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

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


  1. Cheater
    00.00.0000 00:00
    +10

    Тем не менее, если верить интернету, для обработки этих скромных флагов требуется как минимум один платформоспецифичный фреймворк или один заголовочный файл, скопированный у доктора Доббса (Dr. Dobb) где-то в 2003 году.

    В glibc есть getopt


  1. alexei_ovsyannikov
    00.00.0000 00:00

    Мне интересно, а есть ли еще какие-нибудь инстурменты по парсингу аргументов командной строки кроме argv?