Conar 6: Parser — окончание

Начало

Предыдущая часть

Последнее, что осталось «дописать» — собственно организация процесса разбора, а именно: поиск ключей и выборка параметров. При поиске ключей проверяем четыре варианта: «ключ» «…» «другой ключ», «ключ=значение», «-ключключ…ключ» и «ключзначение» (именно в таком порядке). Третий вариант применяется только для однобуквенных ключей.

Все нижеприведённые функции являются членами класса Parser.

Функция, проверяющая параметр на удовлетворение первому варианту («ключ»):

bool is_direct_key(const std::string &param) const
{
  return mapper.count(param) != 0;
}

Функция, проверяющая параметр на удовлетворение второму варианту («ключ=значение»), возвращает позицию знака «=»:

size_t is_key_equals(const std::string &param) const
{
  const auto eq_pos = param.find('=');
  if (eq_pos == std::string::npos)
    return 0;

  return is_direct_key(param.substr(0, eq_pos))? eq_pos: 0;
}

Функция, проверяющая параметр на удовлетворение третьему варианту («-ключключ…ключ»):

bool is_key_sequence(const std::string &param) const
{
  const auto psz = param.size();
  if (psz < 3 || param[0] != '-')
    return false;

  char key[3] {'-'};
  for (size_t i = 1; i < psz; ++i)
  {
    key[1] = param[i];
    if (mapper.count(key) == 0)
      return false;
  }

  return true;
}

Наконец, функция, проверяющая параметр на удовлетворение четвёртому варианту («ключзначение»), возвращает позицию первого символа значения:

size_t is_key_value(std::string param) const
{
  while (!param.empty())
  {
    param.pop_back();
    if (is_direct_key(param))
      return param.size();
  }

  return 0;
}

Данная функция проверяет «с конца», пытаясь подобрать самый длинный ключ, что может быть нежелаемым поведением (например, мы хотим использовать в такой манере только однобуквенные ключи — такую опцию я хочу добавить позже).

Используя определённые выше функции можно набросать скелет цикла обработки последовательности параметров:

  Unparsed_parameters unparsed;
  Parameters params(from, to);
  for (auto key = begin(params), params_end = end(params),
            prev_key = params_end;
    key != params_end;)
  {
    // Check all possible variants one by one.
    if (is_direct_key(*key))
    {
      // The only situation in which we may have a sequence
      // of argument parameters after the key.

      prev_key = key;
    }
    else if (const auto eq_pos = is_key_equals(*key))
    {    }
    else if (is_key_sequence(*key))
    {    }
    else if (const auto val_pos = is_key_value(*key))
    {    }
    else
    {
      // Nope, this is not a valid parameter of any form.
      unparsed.emplace_back(move(*key));
      ++key; // next...
    }
  }
  return unparsed;

Если сработало условие is_direct_key(*key), то надо найти следующий ключ, а всё что между передать диапазоном обработчику опции, соответствующей *key. Первая мысль: запоминать последнюю такую позицию (итератор prev_key) и вызывать обработчик, когда был найден правый конец. Недостаток: это надо делать под каждым if’ом, хотелось бы избежать такого дублирования кода…

Подумав, как можно разобраться с данной ситуацией, я остановился на варианте «умного» кода (здесь «умный» — не комплимент), а именно, использовать приём, известный как «хэширование»:

    size_t pos;
    if
    (
      int situation =
        is_direct_key(*key)?          1
      : (pos = is_key_equals(*key))?  2
      : is_key_sequence(*key)?        3
      : (pos = is_key_value(*key))?   4
      : 0 // --> else
    )
    {   }
    else
    {
      // Nope, this is not a valid parameter of any form.
      unparsed.emplace_back(move(*key));
      ++key; // next...
    }

Теперь я могу избежать дублирования кода, а необходимая информация о случае хранится в situation и pos. Хотя нет, есть ещё случай, когда ключ — последний, дальше до конца идут только его аргументы… Продублировать вызов обработчика после цикла? Думаю, лучше ситуацию «конец параметров» тоже занести в situation:

  for (auto key = begin(params), params_end = end(params),
            prev_key = params_end;;)
  {
    size_t pos;
    if
    (
      int situation =
        key == params_end?            1
      : is_direct_key(*key)?          2
      : (pos = is_key_equals(*key))?  3
      : is_key_sequence(*key)?        4
      : (pos = is_key_value(*key))?   5
      : 0 // --> else
    )

Интересно, как можно было бы записать то же самое чище и без дублирования кода…

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

template <class Key>
Handler& handler_of(const Key &key)
{
  return std::get<0>(options[mapper.at(key)]);
}

Теперь я могу, наконец, записать, что делается под if’ом:

  if (prev_key != params_end)
  {
    for (auto last = handler_of(*prev_key)
                     (prev_key + 1, key);
         last != key; ++last)
    { unparsed.emplace_back(move(*last)); }
    prev_key = params_end;
  }

  switch (situation)
  {
  case 1:
    return unparsed;

  case 2:
    prev_key = key;
    break;

  case 3: case 5:
    {
      const int has_equals = situation == 3;
      auto k = key->substr(0, pos);
      key->erase(0, pos + has_equals);
      if (handler_of(k)(key, key + 1) == key)
        unparsed.emplace_back(move(*key));
    }
    break;

  case 4:
    for (size_t i = 1, sz = key->size(); i < sz; ++i)
    {
      char k[3] { '-', (*key)[i] };
      handler_of(k)(params_end, params_end);
    }
  }

Красотой этот код не отличается, увы. И надо написать для него тесты.

В результате тестирования была исправлена ветка else:

    else
    {
      // Nope, this is not a valid parameter of any form.
      if (prev_key == params_end)
        unparsed.emplace_back(move(*key));
    }

При тестировании со строковыми параметрами обнаружилась старая ошибка: оператор >> читает «словами», однако в случае строковых параметров их надо не «читать» (с пробельными символами-разделителями, да), а просто «отдавать» целиком. Для этого я заменю явное чтение с помощью >> на вызов новой функции impl::read_value:

/// Tries to read a generic val from string str.
template <class S, class T>
inline bool read_value
  (std::istringstream &reader, S &&str, T &val)
{
  reader.str(std::forward<S>(str));
  return reader >> val;
}

/// Forwards a string instead of reading it.
inline bool read_value
  (std::istringstream&, std::string &&str, std::string &val)
{
  val = std::move(str);
  return true;
}

Забавно, что после этой замены сработал тест value_out (код ошибки 7) — из-за того, что теперь «чтение» забирает строки с помощью move, а не копирует их.

Вместе с тестами таки вылезло за пределы 1000 строк. В итоге, я не решился использовать C++17 (только static_assert без сообщения, а можно было декомпозиционное определение и string_view, например).

Код данного варианта целиком.

Реклама

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: