argparse icon indicating copy to clipboard operation
argparse copied to clipboard

Allow custom titles for arguments group

Open Temporalitas opened this issue 3 years ago • 13 comments

As far as I understand, now the library automatically distributes arguments into groups in the --help output like this:

Positional arguments:
  somearg       	do something

Optional arguments:
  -h, --help                 shows help message and exits
  -v, --version              prints version information and exits
  -i, --input-file           input file path
  -o, --output-file          output file path
  --other                    Other important parameter

However, I (with another library at the moment, but this one seems to me better in almost everything) describe the parameters for the user by custom groups, something like this:

Basic options:
  -h, --help                 shows help message and exits
  -v, --version              prints version information and exits

File options
  -i, --input-file           input file path
  -o, --output-file          output file path
  
Another purpose options:
  --other                    Other important parameter
  
Positional arguments:
   somearg                   do something

I would be grateful if something like this was added to this library.

P.S. I know, that I can define argparse::default_arguments::none and manually rewrite help message, but I'm looking for a more scalable and convenient solution

Temporalitas avatar Oct 15 '22 13:10 Temporalitas

If we were to add formatters, IMHO it would take almost as much code to make a custom formatter as to make custom help output.

Because Argument objects handle their own help output, you can order them however you prefer. With a std::map to hold groups of Arguments, there is little overhead.

#include <argparse/argparse.hpp>

#include <iostream>
#include <map>
#include <string>
#include <vector>

using OptionGroup = std::vector<argparse::Argument>;
using GroupsMap = std::map<std::string, OptionGroup>;
using KeyOGPair = std::pair<std::string, OptionGroup>;

void print_grouped_help(const GroupsMap &groups) {
  for ( auto &group : groups ) {
    std::cout << group.first << ':' << std::endl;
    for ( auto &arg : group.second ) {
      std::cout << arg;
    }
    std::cout << std::endl;
  }
}

int main(int argc, char *argv[]) {
  GroupsMap help_groups {
    KeyOGPair{"Basic Options", {}},
    KeyOGPair{"File Options", {}}
  };

  argparse::ArgumentParser program("test", "1.0",
                                   argparse::default_arguments::none);

  help_groups.at("Basic Options").push_back(
    program.add_argument("-h", "--help")
    .help("show help")
    .nargs(0));
  help_groups.at("Basic Options").push_back(
    program.add_argument("-v", "--version")
    .help("show version")
    .nargs(0));

  help_groups.at("File Options").push_back(
    program.add_argument("-i", "--input-file")
    .help("read IN file")
    .metavar("IN"));
  help_groups.at("File Options").push_back(
    program.add_argument("-o", "--output-file")
    .help("write to OUT file")
    .metavar("OUT"));

  program.parse_args(argc, argv);

  if ( program.is_used("--help") ) {
    std::cout << program.usage() << std::endl;
    print_grouped_help(help_groups);
    exit(1);
  }

  return 0;
}

skrobinson avatar Oct 18 '22 13:10 skrobinson

If we were to add formatters, IMHO it would take almost as much code to make a custom formatter as to make custom help output.

Because Argument objects handle their own help output, you can order them however you prefer. With a std::map to hold groups of Arguments, there is little overhead.

Thanks for help, I'll try your suggestion

Temporalitas avatar Oct 18 '22 15:10 Temporalitas

Because Argument objects handle their own help output, you can order them however you prefer. With a std::map to hold groups of Arguments, there is little overhead

How to add a positional argument to a group? When trying to do this, it throws an error

Temporalitas avatar Nov 11 '22 00:11 Temporalitas

@skrobinson

Temporalitas avatar Nov 16 '22 14:11 Temporalitas

@Theodikes Thank you for the bump, I missed your reply in the recent flood of Issues.

I'll assume the error you see is something like

terminate called after throwing an instance of 'std::runtime_error'
  what():  in_file: 1 argument(s) expected. 0 provided.
Aborted

For my example, I left out the try...catch block recommended to surround program.parse_args(argc, argv);. You should catch std::runtime_error and output an appropriate user message. Something like

  try {
    program.parse_args(argc, argv);
  } catch (const std::runtime_error& err) {
    std::cout << program.usage() << std::endl;
    print_grouped_help(help_groups);
    std::cerr << err.what() << std::endl;
    std::exit(1);
  }

skrobinson avatar Nov 16 '22 21:11 skrobinson

@Theodikes Thank you for the bump, I missed your reply in the recent flood of Issues.

I'll assume the error you see is something like

terminate called after throwing an instance of 'std::runtime_error'
  what():  in_file: 1 argument(s) expected. 0 provided.
Aborted

For my example, I left out the try...catch block recommended to surround program.parse_args(argc, argv);. You should catch std::runtime_error and output an appropriate user message. Something like

  try {
    program.parse_args(argc, argv);
  } catch (const std::runtime_error& err) {
    std::cout << program.usage() << std::endl;
    print_grouped_help(help_groups);
    std::cerr << err.what() << std::endl;
    std::exit(1);
  }

@skrobinson, thank you for help, it worked well. But how can I combine it with subcommands? My program uses subcommands with custom argument group titles, for example:
Screenshot_15

Temporalitas avatar Nov 17 '22 21:11 Temporalitas

And is it possible to make a "abbreviated" command name, like with an argument? For example, fullname of argument - --input, shortname - -i, same way for subcommand: fullname - deduplicate, shortname d Screenshot_16

Temporalitas avatar Nov 18 '22 10:11 Temporalitas

And is it possible to make a "abbreviated" command name, like with an argument? For example, fullname of argument - --input, shortname - -i, same way for subcommand: fullname - deduplicate, shortname d Screenshot_16

@skrobinson

Temporalitas avatar Nov 22 '22 03:11 Temporalitas

The pattern I showed for Argument groups can be used for subparsers, as well. There will be some differences in the needed syntax, but the general idea will transfer.

For command abbreviations, there is no built-in support for aliases like there is for Argument. I have not thought through all the corner cases, but I would handle this with a mapping of abbreviations to commands and pre-process the command line.

skrobinson avatar Nov 23 '22 15:11 skrobinson

For command abbreviations, there is no built-in support for aliases like there is for Argument.

Does it make sense to implement this and send you a pull request, or you do not plan to add this to code?

Temporalitas avatar Nov 23 '22 18:11 Temporalitas

I'm neutral on the concept. What about you, @p-ranav?

skrobinson avatar Nov 23 '22 20:11 skrobinson

I'm neutral on the idea as well. Feel free to create a PR adding aliases to subcommands. It would not be in my typical use cases but if it is for someone else (like yourself), I have no particular concerns supporting it.

If you do create a PR, please add test cases and update the README.

p-ranav avatar Nov 23 '22 21:11 p-ranav

For the record, I wanted to have argument groups and ended up using the solution above with in place creation of the vector instead of multiple push_backs. But it would have been more handy if it was built-in :)


    GroupsMap help_groups{
            KeyOGPair{"General", {}},
            KeyOGPair{"Simulation parameters", {}},
            KeyOGPair{"Environment Hyperparameters", {}},
    };
    program.add_description("Yet Another Artificial Life Program in cpp");
    help_groups["Environment Hyperparameters"] = {
            program.add_argument("-H", "--height").help("Height of the map").default_value(1000).scan<'i', int>(),
            program.add_argument("-W", "--width").help("Width of the map").default_value(1000).scan<'i', int>(),
            program.add_argument("-C", "--channels").help("Number of channels in the map").default_value(
                    5).scan<'i', int>(),
            ...
    };
    help_groups["General"] = {
            program.add_argument("-h", "--help").help("Print this help").nargs(0),
    };
    help_groups["Simulation parameters"] = {
            program.add_argument("-n", "--num-yaals").help(
                    "Number of yaals at the start of the simulation").default_value(100).scan<'i', int>(),
            program.add_argument("-t", "--timesteps").help("Number of timesteps to simulate").default_value(
                    10000).scan<'i', int>(),
    };

This gives:

➜ ./cmake-build-debug/yaalpp --help
Usage: yaalpp [--help] [--version] [--height VAR] [--width VAR] [--channels VAR] [--decay-factors VAR...] [--diffusion-rate VAR...] [--max-values VAR...] [--help] [--num-yaals VAR] [--timesteps VAR]
Environment Hyperparameters:
  -H, --height  Height of the map [nargs=0..1] [default: 1000]
  -W, --width  Width of the map [nargs=0..1] [default: 1000]
  -C, --channels  Number of channels in the map [nargs=0..1] [default: 5]
  -D, --decay-factors  Decay factors for each channel [nargs: 0 or more] [default: {0 0 0 0.9 0.5}]
  -d, --diffusion-rate  Diffusion rate for channel [nargs: 0 or more] [default: {0 0 0 0.1 0.9}]
  -m, --max-values  Max values for each channel [nargs: 0 or more] [default: {1 1 1 5 5}]

General:
  -h, --help  Print this help 

Simulation parameters:
  -n, --num-yaals  Number of yaals at the start of the simulation [nargs=0..1] [default: 100]
  -t, --timesteps  Number of timesteps to simulate [nargs=0..1] [default: 10000]

and

✗ ./cmake-build-debug/yaalpp --foo
Usage: yaalpp [--help] [--version] [--height VAR] [--width VAR] [--channels VAR] [--decay-factors VAR...] [--diffusion-rate VAR...] [--max-values VAR...] [--help] [--num-yaals VAR] [--timesteps VAR]
Environment Hyperparameters:
  -H, --height  Height of the map [nargs=0..1] [default: 1000]
  -W, --width  Width of the map [nargs=0..1] [default: 1000]
  -C, --channels  Number of channels in the map [nargs=0..1] [default: 5]
  -D, --decay-factors  Decay factors for each channel [nargs: 0 or more] [default: {0 0 0 0.9 0.5}]
  -d, --diffusion-rate  Diffusion rate for channel [nargs: 0 or more] [default: {0 0 0 0.1 0.9}]
  -m, --max-values  Max values for each channel [nargs: 0 or more] [default: {1 1 1 5 5}]

General:
  -h, --help  Print this help 

Simulation parameters:
  -n, --num-yaals  Number of yaals at the start of the simulation [nargs=0..1] [default: 100]
  -t, --timesteps  Number of timesteps to simulate [nargs=0..1] [default: 10000]

Unknown argument: --foo

Butanium avatar Feb 09 '24 00:02 Butanium