p-ranav / argparse

Argument Parser for Modern C++
MIT License
2.72k stars 250 forks source link

Ability to support defining K/V dictionary via arguments #311

Closed JohnnyMorganz closed 1 year ago

JohnnyMorganz commented 1 year ago

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!

p-ranav commented 1 year ago

Hi,

How about this? Does this solve your problem?

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)
JohnnyMorganz commented 1 year ago

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

p-ranav commented 1 year ago

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
JohnnyMorganz commented 1 year ago

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