jarro2783 / cxxopts

Lightweight C++ command line option parser
MIT License
4.16k stars 582 forks source link

A proposition to implement any complex rule on the arguments #401

Open AdelKS opened 1 year ago

AdelKS commented 1 year ago

After skimming through #73 #44 and #35, where the main issues from the devs seems to be this one

The main reasoning is that usually options are not just mandatory or optional., but something more complicated

I have a suggestion that should please both parties (the developers and the users)

A simple and elegant way to handle "required", "optional" and actually any complex rule is to implement template expressions

Here's a small working example that handles constraints of type "option 'foo' must appear exactly once and option 'bar' exactly twice" :

#include <cxxopts.hpp>
#include <cstddef>
#include <iostream>

namespace cxxopts {
namespace rules {

// =============================================================
class Option
{
public:
  Option(std::string option_name) : name(option_name) {}

  std::string name;
};
// =============================================================

// =============================================================
class Count
{
public:
  Count(Option option) : option(std::move(option)) {}

  inline size_t count(const ParseResult &parsing) const
  {
    return parsing.count(option.name);
  }

  Option option;
};
// =============================================================

// =============================================================
class ExactCount
{
public:
  ExactCount(Option option, size_t target_count)
    : option(std::move(option)), target_count(target_count) {};

  inline bool operator () (const ParseResult& parsing) const
  {
    return option.count(parsing) == target_count;
  }

  inline explicit operator std::string () const
  {
    if (target_count == 1)
      return "option '--" + option.name + "' must be provided exactly once";
    else return "option '--" + option.name + "' must be provided exactly " + std::to_string(target_count) + " times";
  }

protected:
  Option option;
  size_t target_count;
};

ExactCount operator == (Count option_count, size_t target_count)
{
  return ExactCount(std::move(option_count.option), target_count);
}
// =============================================================

// =============================================================
template <class SubExpression1, class SubExpression2>
class AND
{
public:
  AND(SubExpression1 expr1, SubExpression2 expr2)
    : expr1(std::move(expr1)), expr2(std::move(expr2)) {}

  inline bool operator () (const ParseResult& parsing) const
  {
    return expr1(parsing) && expr2(parsing);
  }

  inline explicit operator std::string () const
  {
    return "(" + std::string(expr1) + ") and (" + std::string(expr2) + ")";
  }

protected:
  SubExpression1 expr1;
  SubExpression2 expr2;

};

template <class SubExpression1, class SubExpression2>
auto operator && (SubExpression1 expr1, SubExpression2 expr2)
{
  return AND(std::move(expr1), std::move(expr2));
}
// =============================================================

template <class Rule>
void enforce(const Rule& rule, const ParseResult& parsing)
{
  if (not rule(parsing))
    throw std::runtime_error("rule not respected: " + std::string(rule));
}

}
}

using namespace cxxopts::rules;

int main(int argc, char *argv[])
{
  cxxopts::Options options(
    "hash-id",
    "Advertising-ID hasher");

  options.add_options()
    ("i,input", "input", cxxopts::value<std::string>())
    ("o,output", "output", cxxopts::value<std::string>())
    ("h,help", "Print usage")
    ;

  auto parsing = options.parse(argc, argv);

  auto rule = (Count(Option("input")) == 1) and (Count(Option("output")) == 2);

  cxxopts::rules::enforce(rule, parsing);

  std::cout << parsing["output"].as<std::string>() << std::endl;

  return 0;
}

After implementing the or, not, xor, nand boolean operations, and all the comparison operations >, >=... on the Count class. We get a great deal of flexibility: rules like "this option can only be used this other one is set ..." can be implemented just with this.

Ordering rules e.g. "this option can only be set after this option" can also be implemented in the same way I think.

One needs to think a little bit more to be able to display in a human understandable way what's wrong when the check rules don't evaluate to true, in the code snippet above we just display the whole rule in text. The user can already subdivide the rules that are not related to make it better: in my example above, I could write this instead:

  auto rule1 = (Count(Option("input")) == 1);
  auto rule2 = (Count(Option("output")) == 2);

  cxxopts::rules::enforce({rule1, rule2}, parsing);

Just leaving this here maybe you guys find it interesting.