No Description

Sam Jaffe 299d84cb6e chore: remove shared scheme 2 months ago
external 299d84cb6e chore: remove shared scheme 2 months ago
include 11479d1552 chore: do a little re-arrangement 2 years ago
program_args-test f36d481b18 Perform some cleanup, add some test skeletons. 4 years ago
program_args.xcodeproj 299d84cb6e chore: remove shared scheme 2 months ago
test e6c3418b64 test/docs: custom parser definitions 2 years ago
.clang-format 8839aa262d Clang-Format. 4 years ago
.gitmodules b8eb3f2714 refactor: implement program args string parse in terms of string-utils 3 years ago
README.md e6c3418b64 test/docs: custom parser definitions 2 years ago

README.md

Fluent Argument Parser

A tool for the processing of command-line arguments to a C++ program using a fluent interface. No need for writing a constructor or having to create a long list of bindings in main.

Installation

Usage

Arguments

Positional arguments are provided with their zero-based indices. Arguments can be declared as optional, but you cannot include required arguments with a higher index than any optional positional arg.

Arguments always have an arity of 1, and so cannot store information into a container. Arguments past the end requested will be accessible in a public member function called args().

Options

Options are provided as a long-form string name, and input into the command line in the form --name value. Options can map to either singular values (e.g. std::string, int) or containers (e.g. std::vector). Options mapped to containers are repeatable, so you can provide the flag more than one time. Those mapped to singular values will generate an error if repeated.
In either case, options with an arity greater than one are not allowed.

Abbreviated options support key-value concatenation, such as how you can do -I/path/to/my/include/dir in gcc/clang.

Singular option storage cannot be repeated

std::string directory = option("logdir");

$ ./a.out --logdir /var/log --logdir /usr/local/var/log
Error in program argument handling: Repeated option not allowed for argument logdir

Abbreviations are not automatically generated

std::string directory = option("logdir");

$ ./a.out -l /var/log
Error in program argument handling: Unknown argument provided: -l

Pairs/Tuples don't get to use increased arity

std::pair<int, int> bounds = options("bounds");
std::cout << args() << std::endl;

$ ./a.out --bounds 1920 1080
[ "1080" ]

Custom Parsers

You can, however, include custom types in your program arguments.

struct dim2 { int x, y; };

template <> struct string_utils::cast_helper<dim2> {
  bool operator()(std::string_view str, dim2 & to) const noexcept {
    return sscanf(str.data(), "%dx%d", &to.x, &to.y) == 2;
  }
};

dim2 bounds = options("bounds");
std::cout << bounds.x << " by " << bounds.y << std::endl;

$ ./a.out --bounds 1920x1080
1920 by 1080

Flags

Flags are a sub-class of options who do not have a follow-on value, but instead change the state of the given object through being called.

Flags are supported for the types of bool and int only.
With boolean flags, in addition to the --name argument created and the abbreviated form, a --no-name argument will be registered that sets the option value to false (in case the default is set to true).
Integer flags cannot have default values, and do not have inverted forms. Instead, it is possible to repeat an integer flag endlessly, incrementing it by one with each appearance.

Abbreviated flags have the additional feature that they can be concatenated, and integer flags can be number-repeated.
For example, suppose that both -v and -k are valid flags, representing int verbosity and bool dry_run respectively. Then the following are all valid input tokens: -vk, -vv, -vvvk, and -v5. The argument -v5k will still generate a parse error, however.

Examples

Git

Let's take a look at a partial example of how we might design the interfact for git using this tool.

For conveniece sake, we'll start with a helper object.

struct GitBranch {
  GitBranch(Git const *context); // Must be implemented later

  std::string name() const;
  std::string remote() const;
  // ...more inspectors
};

template <typename CRTP> struct GitAction : program::Arguments<CRTP> {
protected:
  using Arguments::Arguments;
  GitBranch branch() const { return GitBranch(parent<Git>()); }
};

Now, we start do define some actions. For the sake of this example, we'll take a look only at push and pull. The commands git-push and git-pull have the interesting characteristic that if the current branch is a remote-tracking branch, then we can automatically pick out the the remote and push/pull it without specifying which remote OR which branch is selected.

struct Push : GitAction<Push> {
  using GitAction::GitAction;

  bool atomic = flag("atomic");
  bool dry_run = flag("dry-run", 'n') = false;
  bool no_verify = flag("no-verify", "skip push hooks") = false;
  bool quiet = flag("quiet", 'q') = false;
  bool verbose = flag("verbose", 'v') = false;
  // ...more options

  std::string repository = argument(0, "repository") = PROGRAM_DEFER(branch().remote());
  std::vector<std::string> refspec = rest("refspec") = PROGRAM_DEFER(branch().name());
};

struct Pull : GitAction<Pull> {
  using GitAction::GitAction;

  bool quiet = flag("quiet", 'q') = false;
  bool verbose = flag("verbose", 'v') = false;
  bool commit = flag("commit", "immediately commit the merged result") = true;
  bool edit = flag("edit", "edit the commit message before merging") = true;
  bool fastforward = flag("ff", "resolve the merge with a fast-forward if possible") = true;
  bool only_ff = flag("ff-only", "fail if fast-forwarding will not succeed");
  // ...more options

  std::string repository = argument(0, "repository") = PROGRAM_DEFER(branch().remote());
  std::vector<std::string> refspec = rest("refspec") = PROGRAM_DEFER(branch().name());
};

And finally we can define the actual Git command. The macro PROGRAM_ARGS_INVOKE creates a method invoke(), which iterates through all of the actions, and checks to see if they were set. If an action is set, then we will automagically recurse into its own invoke function, creating a chain of actions that will be passed into a TYPED_MAIN function.

struct Git : program::Arguments<Git> {
public:
  // Automatically fails if no action was input
  PROGRAM_ARGS_INVOKE(commit, push, pull);

  std::string pwd = option('C');
  std::string git_dir = option("git-dir") = PROGRAM_DEFER(locate_git_dir(pwd));
  std::map<std::string, std::string> config_env = option("config-env");
  // ...more options

private: // PROGRAM_ARGS_INVOKE is the only one who needs to access these
  Push push = action("push");
  Pull pull = action("pull");
};

For each series of "actions" that we support - we instantiate a typed_main function. A combination of the PROGRAM_ARGS_MAIN macro and the PROGRAM_ARGS_INVOKE macro will automatically dispatch the correct action selected to the correct TYPED_MAIN body.

TYPED_MAIN(Git const &git, Push const &push) {
  ...
}

TYPED_MAIN(Git const &git, Pull const &pull) {
  ...
}

PROGRAM_ARGS_MAIN(Git)