p-ranav/argparse

Ability to support defining K/V dictionary via arguments

JohnnyMorganz opened this issue · 4 comments

Hey, awesome project!

I have a CLI tool where I allow users to customize FFlags on the command line. It is essentially a "dictionary-like" structure, where a user can do --flag:NAME=VALUE:

./tool --flag:FOO=True --flag:BAR=True

I could not figure out if there was a way to do this via this library.
I could get part the way there by iterating through all the FFlags statically defined, and registering them individually as hidden arguments in the parser. However, I do not want unknown FFlags to error, and I want to allow it to be dynamic. parse_known_args stops it from erroring, but I do want any other unknown option to error.

Essentially, any argument prefixed with --flag: should not error, and be handled as a K/V dictionary.

Is it possible to do this right now?

Thank you!

Hi,

How about this? Does this solve your problem?

  • Use append() on the flag argument to specify that you want to use --flag multiple times
  • Use assign_chars and set the assignment character to ':'

Once parsing is done, you'll have a vector of strings, for each flag that was used. Now parse each one and separate out the KEY and VALUE using std::string::substr.

#include "argparse.hpp"

std::pair<std::string, std::string> get_kvpair(const std::string &str) {
  size_t pos = str.find("=");
  if (pos != std::string::npos) {
    std::string key = str.substr(0, pos);
    std::string value = str.substr(pos + 1);

    return std::make_pair(key, value);
  } else {
    throw std::invalid_argument(
        "Invalid key-value pair, expected KEY=VALUE, instead got " + str);
  }
}

int main(int argc, char *argv[]) {

  argparse::ArgumentParser program("kv");

  program.add_argument("--flag")
      .default_value<std::vector<std::string>>({})
      .append();

  program.set_assign_chars(":");

  try {
    program.parse_args(argc, argv);
  } catch (const std::exception &err) {
    std::cerr << err.what() << std::endl;
    std::cerr << program;
    return 1;
  }

  const auto &flags = program.get<std::vector<std::string>>("--flag");

  for (auto f : flags) {

    /// Parse each "KEY=VALUE" string into KEY and VALUE pair
    auto pair = get_kvpair(f);

    /// Print it
    std::cout << "Key(" << pair.first << ") = Value(" << pair.second << ")\n";
  }
}

Example Output:

foo:bar $ ./main --flag:FOO=True --flag:BAR=False --flag:CMAKE_BUILD_TYPE=Release
Key(FOO) = Value(True)
Key(BAR) = Value(False)
Key(CMAKE_BUILD_TYPE) = Value(Release)

Hey, thanks for the quick response!

This looks promising. I haven't tried it yet, but would it still be able to handle standard flags with = assign char?

I'm guessing I would need to also include = as an assign char in the set

End result is the ability to parse something like this:

./tool --flag:FOO=BAR --config=path file.txt

I'm trying to repurpose some existing logic to try and use this library, but understandable if it's not possible to fit it all together.

Thanks again! I'll try it out in the next day or so

Correct. That should work.

You can add = to the assign chars spec.

Here's the updated example:

#include "argparse.hpp"

std::pair<std::string, std::string> get_kvpair(const std::string &str) {
  size_t pos = str.find("=");
  if (pos != std::string::npos) {
    std::string key = str.substr(0, pos);
    std::string value = str.substr(pos + 1);

    return std::make_pair(key, value);
  } else {
    throw std::invalid_argument(
        "Invalid key-value pair, expected KEY=VALUE, instead got " + str);
  }
}

int main(int argc, char *argv[]) {

  argparse::ArgumentParser program("kv");

  // Typical usage: --flag:FOO=VALUE
  program.add_argument("--flag")
      .default_value<std::vector<std::string>>({})
      .append();

  // Path to config file
  program.add_argument("--config");

  // Input file
  program.add_argument("input_file");

  // Use ':' or '=' as the assignment character for arguments
  program.set_assign_chars(":=");

  try {
    program.parse_args(argc, argv);
  } catch (const std::exception &err) {
    std::cerr << err.what() << std::endl;
    std::cerr << program;
    return 1;
  }

  const auto &flags = program.get<std::vector<std::string>>("--flag");

  for (auto f : flags) {

    /// Parse each "KEY=VALUE" string into KEY and VALUE pair
    auto pair = get_kvpair(f);

    /// Print it
    std::cout << "Key(" << pair.first << ") = Value(" << pair.second << ")\n";
  }

  if (program.is_used("--config")) {
    std::cout << "Config: " << program.get<std::string>("--config") << "\n";
  }

  std::cout << "Input File: " << program.get<std::string>("input_file") << "\n";
}
foo:bar $ ./main --flag:FOO=True --flag:BAR=False --flag:CMAKE_BUILD_TYPE=Release file.txt
Key(FOO) = Value(True)
Key(BAR) = Value(False)
Key(CMAKE_BUILD_TYPE) = Value(Release)
Input File: file.txt

foo:bar $~/dev/argparse/include/argparse$ ./main --flag:FOO=True --flag:BAR=False --flag:CMAKE_BUILD_TYPE=Release --config=config_file_path file.txt
Key(FOO) = Value(True)
Key(BAR) = Value(False)
Key(CMAKE_BUILD_TYPE) = Value(Release)
Config: config_file_path
Input File: file.txt

Yes, this seems to be working perfectly. Thank you very much!