inline parser_results parser::parse()

in sdk/core/perf/inc/azure/perf/argagg.hpp [1046:1288]


inline parser_results parser::parse(int argc, const char** argv, bool posOnly) const
{
  // Inspect each definition to see if its valid. You may wonder "why don't
  // you do this validation on construction?" I had thought about it but
  // realized that since I've made the parser an aggregate type (granted it
  // just "aggregates" a single vector) I would need to track any changes to
  // the definitions vector and re-run the validity check in order to
  // maintain this expected "validity invariant" on the object. That would
  // then require hiding the definitions vector as a private entry and then
  // turning the parser into a thin interface (by re-exposing setters and
  // getters) to the vector methods just so that I can catch when the
  // definition has been modified. It seems much simpler to just enforce the
  // validity when you actually want to parse because it's at the moment of
  // parsing that you know the definitions are complete.
  parser_map map = validate_definitions(this->definitions);

  // Initialize the parser results that we'll be returning. Store the program
  // name (assumed to be the first command line argument) and initialize
  // everything else as empty.
  std::unordered_map<std::string, option_results> options{};
  std::vector<const char*> pos;
  parser_results results{argv[0], std::move(options), std::move(pos)};

  // Add an empty option result for each definition.
  for (const auto& defn : this->definitions)
  {
    option_results opt_results{{}};
    results.options.insert(std::make_pair(defn.name, opt_results));
  }

  // Don't start off ignoring flags. We only ignore flags after a -- shows up
  // in the command line arguments.
  bool ignore_flags = false;

  // Keep track of any options that are expecting arguments.
  const char* last_flag_expecting_args = nullptr;
  option_result* last_option_expecting_args = nullptr;
  unsigned int num_option_args_to_consume = 0;

  // Get pointers to pointers so we can treat the raw pointer array as an
  // iterator for standard library algorithms. This isn't used yet but can be
  // used to template this function to work on iterators over strings or
  // C-strings.
  const char** arg_i = argv + 1;
  const char** arg_end = argv + argc;

  while (arg_i != arg_end)
  {
    auto arg_i_cstr = *arg_i;
    auto arg_i_len = std::strlen(arg_i_cstr);

    // Some behavior to note: if the previous option is expecting an argument
    // then the next entry will be treated as a positional argument even if
    // it looks like a flag.
    bool treat_as_positional_argument
        = (ignore_flags || num_option_args_to_consume > 0
           || !cmd_line_arg_is_option_flag(arg_i_cstr));
    if (treat_as_positional_argument)
    {

      // If last option is expecting some specific positive number of
      // arguments then give this argument to that option, *regardless of
      // whether or not the argument looks like a flag or is the special "--"
      // argument*.
      if (num_option_args_to_consume > 0)
      {
        last_option_expecting_args->arg = arg_i_cstr;
        --num_option_args_to_consume;
        ++arg_i;
        continue;
      }

      // Now we check if this is just "--" which is a special argument that
      // causes all following arguments to be treated as non-options and is
      // itself discarded.
      if (std::strncmp(arg_i_cstr, "--", 2) == 0 && arg_i_len == 2)
      {
        ignore_flags = true;
        ++arg_i;
        continue;
      }

      // If there are no expectations for option arguments then simply use
      // this argument as a positional argument.
      results.pos.push_back(arg_i_cstr);
      ++arg_i;
      continue;
    }

    // Reset the "expecting argument" state.
    last_flag_expecting_args = nullptr;
    last_option_expecting_args = nullptr;
    num_option_args_to_consume = 0;
    if (!posOnly)
    {
      // If we're at this point then we're definitely dealing with something
      // that is flag-like and has hyphen as the first character and has a
      // length of at least two characters. How we handle this potential flag
      // depends on whether or not it is a long-option so we check that first.
      bool is_long_flag = (arg_i_cstr[1] == '-');

      if (is_long_flag)
      {

        // Long flags have a complication: their arguments can be specified
        // using an '=' character right inside the argument. That means an
        // argument like "--output=foobar.txt" is actually an option with flag
        // "--output" and argument "foobar.txt". So we look for the first
        // instance of the '=' character and keep it in long_flag_arg. If
        // long_flag_arg is nullptr then we didn't find '='. We need the
        // flag_len to construct long_flag_str below.
        auto long_flag_arg = std::strchr(arg_i_cstr, '=');
        size_t flag_len = arg_i_len;
        if (long_flag_arg != nullptr)
        {
          flag_len = long_flag_arg - arg_i_cstr;
        }
        std::string long_flag_str(arg_i_cstr, flag_len);

        if (!map.known_long_flag(long_flag_str))
        {
          std::ostringstream msg;
          msg << "found unexpected flag: " << long_flag_str;
          throw unexpected_option_error(msg.str());
        }

        const auto defn = map.get_definition_for_long_flag(long_flag_str);

        if (long_flag_arg != nullptr && defn->num_args == 0)
        {
          std::ostringstream msg;
          msg << "found argument for option not expecting an argument: " << arg_i_cstr;
          throw unexpected_argument_error(msg.str());
        }

        // We've got a legitimate, known long flag option so we add an option
        // result. This option result initially has an arg of nullptr, but that
        // might change in the following block.
        auto& opt_results = results.options[defn->name];
        option_result opt_result{nullptr};
        opt_results.all.push_back(std::move(opt_result));

        if (defn->requires_arguments())
        {
          bool there_is_an_equal_delimited_arg = (long_flag_arg != nullptr);
          if (there_is_an_equal_delimited_arg)
          {
            // long_flag_arg would be "=foo" in the "--output=foo" case so we
            // increment by 1 to get rid of the equal sign.
            opt_results.all.back().arg = long_flag_arg + 1;
          }
          else
          {
            last_flag_expecting_args = arg_i_cstr;
            last_option_expecting_args = &(opt_results.all.back());
            num_option_args_to_consume = defn->num_args;
          }
        }

        ++arg_i;
        continue;
      }

      // If we've made it here then we're looking at either a short flag or a
      // group of short flags. Short flags can be grouped together so long as
      // they don't require any arguments unless the option that does is the
      // last in the group ("-o x -v" is okay, "-vo x" is okay, "-ov x" is
      // not). So starting after the dash we're going to process each character
      // as if it were a separate flag. Note "sf_idx" stands for "short flag
      // index".
      for (size_t sf_idx = 1; sf_idx < arg_i_len; ++sf_idx)
      {
        const auto short_flag = arg_i_cstr[sf_idx];

        if (!std::isalnum(static_cast<unsigned char>(short_flag)))
        {
          std::ostringstream msg;
          msg << "found non-alphanumeric character '" << arg_i_cstr[sf_idx] << "' in flag group '"
              << arg_i_cstr << "'";
          throw std::domain_error(msg.str());
        }

        if (!map.known_short_flag(short_flag))
        {
          std::ostringstream msg;
          msg << "found unexpected flag '" << arg_i_cstr[sf_idx] << "' in flag group '"
              << arg_i_cstr << "'";
          throw unexpected_option_error(msg.str());
        }

        auto defn = map.get_definition_for_short_flag(short_flag);
        auto& opt_results = results.options[defn->name];

        // Create an option result with an empty argument (for now) and add it
        // to this option's results.
        option_result opt_result{nullptr};
        opt_results.all.push_back(std::move(opt_result));

        if (defn->requires_arguments())
        {

          // If this short flag's option requires an argument and we're the
          // last flag in the short flag group then just put the parser into
          // "expecting argument for last option" state and move onto the next
          // command line argument.
          bool is_last_short_flag_in_group = (sf_idx == arg_i_len - 1);
          if (is_last_short_flag_in_group)
          {
            last_flag_expecting_args = arg_i_cstr;
            last_option_expecting_args = &(opt_results.all.back());
            num_option_args_to_consume = defn->num_args;
            break;
          }

          // If this short flag's option requires an argument and we're NOT the
          // last flag in the short flag group then we automatically consume
          // the rest of the short flag group as the argument for this flag.
          // This is how we get the POSIX behavior of being able to specify a
          // flag's arguments without a white space delimiter (e.g.
          // "-I/usr/local/include").
          opt_results.all.back().arg = arg_i_cstr + sf_idx + 1;
          break;
        }
      }
    }
    ++arg_i;
    continue;
  }

  // If we're done with all of the arguments but are still expecting
  // arguments for a previous option then we haven't satisfied that option.
  // This is an error.
  if (num_option_args_to_consume > 0)
  {
    std::ostringstream msg;
    msg << "last option \"" << last_flag_expecting_args
        << "\" expects an argument but the parser ran out of command line "
        << "arguments to parse";
    throw option_lacks_argument_error(msg.str());
  }

  return results;
}