Рубрика «Без рубрики»

Boost Program Options: разбор командной строки в стиле ООП

Определения

Рассмотрим команду:

prog -ab -n 10 --style=awesome file1 file2

Здесь:

  • prog — имя программы;
  • a, b, n, style — опции;
  • style — длинная опция;
  • -ab — сгруппированные опции;
  • 10 и awesome — аргументы опций;
  • file1 и file2 — параметры.

Неявным значением аргумента будем называть значение, которое использует программа вместо аргумента опции, если этот аргумент не задан (то есть, указана опция без аргумента).

Значением аргумента по умолчанию будем называть значение, которое использует программа вместо аргумента опции, если опция не задана в командной строке.

Возможно, вы видели и другие варианты названий, неважно — я буду пользоваться этими.

Требования

Мы хотим от парсера командной строки поддержки следующего:

  • коротких (-f) и длинных (--input-file) опций;
  • опций с аргументами и без;
  • опций с обязательными и необязательными аргументами;
  • возможности задания значения аргумента по умолчанию и/или неявного значения аргумента;
  • распознавания сгруппированных опций: -abc должно быть эквивалентно -a -b -c;
  • взаимоисключающих опций;
  • параметров;
  • распознавания лексемы «--», разделяющей опции и параметры;
  • возможности выполнить произвольную функцию, если задана некоторая опция;
  • строгой типизации аргументов опций;
  • автоматического формирования справки по опциям.

Также мы хотим, чтобы парсер уведомлял о

  • неизвестной опции;
  • отсутствии обязательного аргумента;
  • задании нескольких взаимоисключающих опций;
  • прочих ошибках.

Реализация

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#include <iostream>
#include <vector>
#include <string>
#include <iterator>
#include <boost/program_options.hpp>
namespace po = boost::program_options; // чтобы было короче
using namespace std;
 
// Вспомогательная функция - выводит содержимое вектора строк.
void print_vector(const vector<string> &v, const string &name)
{
    cout << name << ": ";
    for (vector<string>::const_iterator i = v.begin(); i != v.end(); ++i)
        cout << *i << "/";
    cout << "\n";
}
 
// Функция уведомления, автоматически вызываемая при задании опции -v.
// В аргументе функции передается аргумент опции.
void value_notifier(int v)
{
    cout << "Value notifier, argument is " << v << "\n";
}
 
int main(int argc, char *argv[])
{
    // Группа опций под названием "Visible". Эти опции будем включать в справку.
    po::options_description desc_visible("Visible options");
    // Добавляем опции в группу. Обратите внимание на удобный синтаксис. Функция add_options()
    // возвращает объект-функтор, создающий объекты опций.
    desc_visible.add_options()
        // Обычная опция без аргумента. Длинное и короткое имя указываются через запятую.
        ("help,h", "produce help message")
 
        // Опция с обязательным аргументом типа int. При указании опции в командной строке
        // вызывается функция value_notifier(), которой передается значение аргумента.
        ("value,v", po::value<int>()->notifier(value_notifier), "A value. Argument is mandatory.")
 
        // Опция-флаг. С ней автоматически связывается аргумент булевского типа, который
        // равен true, если опция указана в командной строке, и false в противном случае.
        ("aflag,a", po::bool_switch(), "Flag A")
 
        // Полностью эквивалентно предыдущей записи, но выражено более многословно.
        // zero_tokens() не дает задать аргумент опции, default_value() задает значение аргумента
        // по умолчанию (когда опция не задана), а implicit_value() задает неявное значение аргумента
        // (когда опция задана без аргумента).
        ("bflag,b", po::value<bool>()->zero_tokens()->default_value(false)->implicit_value(true), "Flag B")
 
        // Для закрепления: если опция указана, у нее может быть целый аргумент. Если аргумент
        // опции не указан, считается, что он равен 10. Вызов implicit_value() делает указание
        // аргумента необязательным.
        ("implicit,i", po::value<int>()->implicit_value(10), "Implicit value option")
 
        // Задание опции, аргументом которой является список. Такое нечасто встретишь.
        // Функция multitoken() позволяет комбинировать аргументы несколько раз заданной опции.
        // zero_tokens() позволяет задать список нулевой длины. Тип аргумента vector<string> позволяет
        // включать в значение аргумента несколько следующих за именем опции лексем, не
        // являющихся именем опции.
        // Опция может быть задана так:
        // 1. --list // пустой список
        // 2. --list a b c // список содержит a, b, c
        // 3. --list a -i 10 --list b c // список содержит a, b, c
        ("list", po::value< vector<string> >()->multitoken()->zero_tokens(), "list of some values");
 
    // Группа опций, которую мы не будем показывать пользователю. Зачем эта группа, описано ниже.
    po::options_description desc_hidden("Hidden options");
    desc_hidden.add_options()
        // Опция с аргументом-списком. Обратите внимание на задание значения по умолчанию. Если
        // для типа значения аргумента не определен оператор << для ostream, должно быть явно задано
        // текстовое представление указываемого значения (в данном случае - пустого вектора).
        ("pos2",
         po::value< vector<string> >()->default_value(vector<string>(), ""),
         "First two positional arguments")
 
        // Такая же опция.
        ("posrem",
         po::value< vector<string> >()->default_value(vector<string>(), ""),
         "Remaining positional arguments");
 
    // Группа опций, объединяющая две ранее определенные группы.
    po::options_description desc;
    desc.add(desc_visible).add(desc_hidden);
 
    // Описание параметров ("позиционных опций"). Теперь внимание. В значение аргумента опции
    // pos2 попадают первые два параметра, остальные параметры попадают в значение аргумента
    // опции posrem (на это указывает -1).
    po::positional_options_description p;
    p.add("pos2", 2).add("posrem", -1);
 
    // Здесь будет сохранен результат разбора.
    po::variables_map vm;
    try {
        // Предписываем использовать desc как множество распознаваемых опций и p как
        // множество "псевдоопций", предназначенных для "усыновления" параметров.
        po::store(po::command_line_parser(argc, argv).options(desc).positional(p).run(), vm);
        // Вызвать функции уведомления (value_notifier() в данном случае).
        po::notify(vm);
    }
    // Обработаем исключения, генерируемые при некорректном синтаксисе.
    catch (const po::invalid_command_line_syntax &inv_syntax) {
        switch (inv_syntax.kind()) {
        case po::invalid_syntax::missing_parameter:
            cout << "Missing argument for option '" << inv_syntax.tokens() << "'.\n";
            break;
        default:
            cout << "Syntax error, kind " << int(inv_syntax.kind()) << "\n";
            break;
        };
        return 1;
    }
    // Обработаем исключение, генерируемое при обнаружении неизвестной опции. Есть еще
    // несколько типов исключений, но в подавляющем большинстве случаев нужны только эти.
    catch (const po::unknown_option &unkn_opt) {
        cout << "Unknown option '" << unkn_opt.get_option_name() << "'\n";
        return 1;
    }
 
    // Задана опция help - выводим справку по опциям группы "Visible". Справка по опциям pos2 и
    // posrem не выводится (зачем пользователю знать, как мы обрабатываем параметры?).
    if (vm.count("help")) {
        cout << desc_visible << "\n";
        return 1;
    }
 
    // Примеры типизации значения аргумента.
    if (vm["aflag"].as<bool>() == true)
        cout << "Flag A is set\n";
    if (vm["bflag"].as<bool>() == true)
        cout << "Flag B is set\n";
 
    // Типизация векторов.
    if (vm.count("list")) {
        print_vector(vm["list"].as< vector<string> >(), "List");
    }
 
    // Здесь не надо проверять vm.count, как у list, потому что мы задали значения по умолчанию.
    print_vector(vm["pos2"].as< vector<string> >(), "pos2");
    print_vector(vm["posrem"].as< vector<string> >(), "posrem");
 
    return 0;
}

При сборке нужно подключить библиотеку libboost_program_options.

Чего мне не хватает в Program Options:

  • возможности задавать аргумент-список, перечисляя его члены через запятую (как в команде mount);
  • встоенной поддержки взаимоисключающих аргументов (сейчас — только вручную);
  • более широких возможностей по форматированию автоматически формируемой справки.

Конечно, в одном посте невозможно описать все возможности библиотеки — их гораздо больше, чем я описал. Но, во-первых, приведенный мной код покрывает 95% всех случаев, и, во-вторых, всегда можно обратиться к документации.

Также обязан заметить, что я представлял в качестве читателя человека, для которого синтаксис C++ не является сюрпризом.

УжасноПлохоНормальноХорошоОтлично (4 голосов, средний: 5,00 из 5)
Loading ... Loading ...

Немного рефлексии

Давненько не брал я в руки шашек, домино, да и прочие настольные игры тоже. Рождение сына в сочетании с неудержимым желанием как можно быстрее написать и защитить дисер заставило меня несколько пересмотреть жизненные приоритеты.

Блог — это ведь тоже вроде игрушки, этакий тамагочи, за которым надо ухаживать и кормить свежими постами, иначе он начинает пахнуть нафталином и перестает вызывать интерес у общественности. Впрочем, в отличие от многих блогеров, я пишу в основном на «вечные» темы, и большинство моих постов, смею надеяться, не теряют в цене от того, что им год, а не день. В этом смысле я могу себе позволить делать полугодовые перерывы. Но, увы, чем меньше пишешь, тем меньше желание писать: посещаемость падает до неприличных объемов, в комментариях начинают заводиться спамеры, движок уже который месяц требует обновиться. Смотришь на это все — и всегда находится более важное дело.

Самым простым выходом было бы просто не заплатить в очередной раз за домен — и все дела. В самом деле, что мне такого особенного дает блог? Тренировка писательских навыков? Этого у меня и на работе, и в аспирантуре предостаточно. Сохранение сведений на будущее? Для этого у меня есть своя Wiki, записи в которой отличаются сверхлаконичностью и, к тому же, обычно написаны на английском (так короче). К публикации всяких незначительных событий собственной жизни я отношусь скептически. Вот и выходит, что остается единственный мотивирующий фактор — сознание того, что мои посты могут кому-то оказаться полезными или хотя бы интересными.

Вам интересно? Вам пригодилось что-нибудь? Хотите, чтобы я о чем-то написал подробнее? Напишите коммент — вам несложно, а меня смотивирует на новые посты.

P.S. Завтра будет пост про Boost Program Options.

P.P.S. Посты про make & Autotools таки тоже будут. Про Python не будет, забросил пока.

At last!

int pid = fork();
if (pid) {
    printf("Yahoo!\n");
    drink();
}
else if (!pid) {
    execl("Boris", "");
}
else {
    // never executed
}

Перешел на Arch Linux

Fedora перестала меня удовлетворять, и главным образом по двум причинам.

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

Во-вторых, концепция релизов меня окончательно разочаровала. Релизы хороши для корпоративного использования, когда развитие проекта должно двигаться вперед, но в то же время для распространяемых дистрибутивов должна гарантироваться поддержка и стабильность. Для домашнего использования в этом мало смысла: почему я должен ждать следующего релиза чтобы моя любимая программа обновилась до свежей версии? В принципе, учитывая мою консервативность, проблема свежего софта меня не очень напрягала, но сама мысль мешала спокойно спать.

В результате я пришел к непростому выбору из Gentoo, Arch и FreeBSD. Бесполезно меня спрашивать, почему именно эти три, и почему только они. Не знаю. Возможно, Gentoo просто на слуху, Arch мне советовал коллега (который сам при этом поклонник Gentoo), а FreeBSD — это просто старая платоническая любовь к неведомому миру, который «тоже опенсорс, но другой». Уважение к опыту коллеги перевесило все остальные доводы, и я поставил Arch Linux.

Как человек, развращенный автоматическими установщиками, графическими утилитами настройки и прочими bells and whistles, я готовился к худшему. Отчетливо представлялся процесс установки в виде загрузки с live-диска в голую консоль с приглашением «Вот вам консоль. Пожалуйста, установите Linux на свой вкус и цвет. Спасибо». После этого я должен был погрузиться в чтение мануалов, провести за этим занятием пару недель, отрастить бороду до груди и приобрести интенсивный красный цвет глаз. Эти атрибуты позволили бы мне достигнуть просветления, и я, пятнадцать раз пересобрав ядро и шестнадцать раз — все остальное, — получил бы, наконец, вожделенную системищу. Вот такую жуть напредставлял.

И конечно же, все оказалось совсем не так. Новичковый мануал написан в духе «делай раз, делай два», и точное следование указаниям позволяет уже через пару часов получить работающую систему с настроенным иксовым окружением. На самом деле, я уже где-то с середины мануала плюнул на их скучные инструкции, и стал все делать по-своему. Все оказалось настолько просто и логично, что я оказался даже немножко разочарован. Хотелось почувствовать себя суровым админом, но нет, не судьба.

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

  1. Философия «чем проще, тем лучше». Что характерно, «проще» здесь означает «оценишь простоту, когда научишься», а не «любой дурак поймет». Arch — однозначно не для новичков и сторонников подхода, чтобы все из коробки автоматически поставилось и подушечку подложило. Это как раз тот случай, когда «вы можете настроить все… и вы будете настраивать все».
  2. Простое и прозрачное конфигурирование загрузки (все практически в двух файлах — /etc/rc.conf и ~/.xinitrc). Скрипты из /etc/rc.d никуда не делись, но все демоны вызываются централизованно, а не исходя из первой буквы соответствующей символической ссылки.
  3. Менеджер пакетов pacman определенно хорош. В первую очередь тем, что про него сложно сказать что-то плохое.
  4. Arch Build System позволяет при желании превратить Arch в этакий Gentoo. Формат файлов PKGBUILD, управляющих сборкой пакетов, нормально пишется и читается.
  5. AUR — гигантская база пользовательских пакетов в исходных кодах на случай, если в арчевских репозитариях чего-то нет.
  6. Концепция rolling release: отсутствуют релизы. Система эволюционирует, нет нужды переустанавливать ее или делать «апгрейд». Кроме того, всегда доступны самые свежие версии пакетов.
  7. Обширная Wiki, в которой можно найти инструкции почти на все случаи жизни. В некоторой степени переведена на русский.

Короче говоря, друзья, категорически рекомендую Arch тем, для кого Ubuntu — это «для чайников», а Gentoo — «для красноглазых». Очень разумный компромисс.

УжасноПлохоНормальноХорошоОтлично (Еще не оценили)
Loading ... Loading ...

Shave: чистим вывод make при использовании Autotools

Не люблю, когда make выплевывает километры всякого мусора. Зачем мне для каждого файла полный вызов gcc со всеми флагами? Спасибо, я эти флаги сам задавал, знаю что к чему. Зато среди всей этой ерунды пропустить что-то важное — проще простого. Значит, надо как-то выделить важное, а неважное вовсе не показывать. Вот какой я мудрый!

В обычных Makefile’ах все просто:

chset.c: chset.lex
        @echo "  FLEX  $(@:.c=)"
        @$(LEX) --outfile=$@ $<

Символ ‘@’, указанный перед командой, подавляет вывод на stdout, оставляя возможность увидеть ошибки, выводимые в stderr. Строчка-подсказка выводится как раз для того, чтобы увидеть, на каком этапе сборки произошла ошибка, если таковая будет.

При использовании Autotools все становится сложнее, тут уже одними собачками не обойдешься. Интересное решение предлагает Shave: он подменяет вызовы компилятора и libtool на вызовы своих сценариев, которые молча выполняют все что нужно, а выводят только краткую диагностику, как в предыдущем примере. Вот так, например:

[nkalex@stables build]$ make
Making all in src
CC    main.o
CC    msg.o
CC    ctype.o
CC    collate.o
CC    charset.o
CC    ldefpars.o
CC    locopts.o
CC    chset.o
CC    ldeflex.o
LINK  localedef

Как настраивать Shave, написано в его кратком руководстве, но там есть пара граблей, на которые я не преминул наступить. Поэтому лучше уж я свою инструкцию напишу, чтоб люди не мучались.

  1. Делаем нормально собирающийся проект, использующий Automake и Autoconf.
  2. Скачиваем Shave по адресу http://download.lespiau.name/shave.
  3. Сценарии shave.in и shave-libtool.in кладем в корневой каталог проекта.
  4. Файл shave.m4 копируем в файл acinclude.m4 в корневом каталоге проекта.
  5. Вносим изменения в configure.ac:
    1. даже если не используем libtool, добавляем строку
      LT_INIT
    2. добавляем сценарии Shave в AC_CONFIG_FILES, чтобы получилось как-то так:
      AC_CONFIG_FILES([
      shave
      shave-libtool
      Makefile
      src/Makefile
      ])
    3. сразу перед AC_CONFIG_FILES добавляем строчку
      SHAVE_INIT(.,enable)
  6. Если не пользовались до этого libtool, в корневом каталоге нужно вызвать
    $ libtoolize
    $ automake --add-missing
  7. Profit!

Теперь по умолчанию Shave включен, и вывод при сборке будет кратким. Чтобы все-таки посмотреть подробный вывод, нужно добавить ключ в вызов configure:

$ configure --disable-shave

Грабли, на которые я наступил, заключались в необходимости включить поддержку libtool. Без этого вызов SHAVE_INIT выдает крайне невнятную диагностику в духе syntax error, и пришлось читать shave.m4, чтобы разобраться, в чем, собственно, дело.
УжасноПлохоНормальноХорошоОтлично (2 голосов, средний: 5,00 из 5)
Loading ... Loading ...

Получить полный путь к файлу/каталогу в bash

Иногда в аргументах сценария приходят пути к файлам и каталогам в разнообразных форматах, ну например:

./file1
~/cat1/cat2/
../../cat1/file2
justfile
cat1/file3

А хочется в каждом случае получить полный путь, скажем, «/home/user/cat1/file». И если второй вариант раскроется оболочкой в полный путь еще до передачи сценарию, то остальные так и останутся с точками и относительными путями.

Есть простое и изящное решение, подсмотренное на каких-то форумах:

absPath=$(readlink -f "$(dirname "$relPath")")/$(basename "$relPath")

Эта штука замечательно работает, но ровно до тех пор, пока мы не передадим ей путь «.», «./», «..» или «../». В таком случае basename "$relPath" выдаст «.» или «..», и мы в результате получим путь, который выглядит, соответственно, как-то вроде «/home/user/cat1/cat2/.» или «/home/user/cat1/cat2/..», который работать-то будет, но выглядит не очень.

Можно, конечно, почистить путь от стоящих в конце точек, но в случае с двумя точками придется еще и удалить предпоследнюю компоненту. Но зачем делать работу самому, если ее за тебя могут сделать другие? Вот такой получился окончательный вариант:

absPath=$(readlink -f $(readlink -f "$(dirname "$relPath")")/$(basename "$relPath"))
УжасноПлохоНормальноХорошоОтлично (1 голосов, средний: 5,00 из 5)
Loading ... Loading ...