Skip to content

Subcommands

gershnik edited this page Mar 17, 2022 · 14 revisions

As described in the page about depending on order, handlers for options and positional arguments are invoked sequentially as the command line is being processed. Moreover, Argum allows you to modify its own definitions from inside a handler. Taken together these two facts allow you to easily define context sensitive or adaptive parsers without resorting to rigid constructs like subparsers.

The most familiar example of context sensitive command line is provided by utilities like git that have subcommand which generally have syntax like

git [common options] <command> [command specific options] [command specific arguments]

Common options are always understood while command specific options depend on a given command. For such utilities it is also common to provide help for each individual command like git help checkout in addition to general help.

Simple subcommand

Coding something like this is, again, pretty straightforward with Argum. First a simplified example with no help.

void configureCommand1(Parser & parser) {
    parser.add(
        Option("--option1", "-o1").
        help("option 1").
        handler([&]() { 
            //do something
    }));
    parser.add(
        Positional("positional1").
        help("some positional").
        handler([&](const std::string_view & value) { 
            //do something
    }));
}

void configureCommand2(Parser & parser) {
    parser.add(
        Option("--option2", "-o2").
        help("option 2").
        handler([&]() { 
            //do something
    }));
    parser.add(
        Positional("positional2").
        help("another positional").
        handler([&](const std::string_view & value) { 
            //do something
    }));
}

void configureCommand(Parser & parser, string_view command) {
    if (command == "command1") 
        configureCommand1(parser);
    else if (command == "command2")
        configureCommand2(parser);
    else
        throw Parser::ValidationError("invalid command: " + string(command));
}

int main(int arc, char * argv[]) {
    //global option
    string username;
    //command to execute
    string command;
    ...

    //first add global options as usual
    parser.add(
        Option("--user", "-u").
        argName("USERNAME").
        help("username to use. this option is common to all commands").
        handler([&](string_view value) { 
            username = value;
    }));
    parser.add(
        Positional("command").
        help("command to perform").
        handler([&](string_view value) { 
            configureCommand(parser, value);
    }));

    ...

    cout << "executing command: " << *command << '\n';
}

This works perfectly but you cannot use simple parser.formatUsage() and parser.formatHelp() with such a parser. If you do the results will be pretty bad:

$ ./prog command1 --help
Usage: ./prog [--user USERNAME] [--help] [--option1] command positional1

positional arguments:
  command               command to perform
  positional1           some positional

options:
  --user USERNAME, -u USERNAME
                        username to use. this option is common to all commands
  --help, -h            show this help message and exit
  --option1, -o1        option 1

And similar for command2. This is obviously incorrect and misleading for a user.

To provide help in such scenario you need to

  1. Tell the parser that the Positional for the command is a special "subcommand" one. You do this by using addSubCommand method instead of the usual add.
  2. Pass the actual selected command, if any, to formatUsage and formatHelp methods.

Below is an example of doing so

Subcommand with help via --help option

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

    //global option
    string username;
    //the command to execute, if any
    optional<string> command;
    
    const char * progname = (argc ? argv[0] : "prog");
    
    Parser parser;
    //first add global options as usual
    parser.add(
            Option("--user", "-u").
            argName("USERNAME").
            help("username to use. this option is common to all commands").
            handler([&](const std::string_view & value) { 
                username = value;
        }));
    parser.add(
        Option("--help", "-h"). 
        help("show this help message and exit"). 
        handler([&]() {  
            //pass the optional selected command
            std::cout << parser.formatHelp(progname, command);
            std::exit(EXIT_SUCCESS);
    }));
    
    parser.addSubCommand(Positional("command").
        help("command to perform").
        handler([&](const std::string_view & value) { 
            //record desired command
            command = value;
            //configure parser
            configureCommand(parser, value);
    }));

    try {
        parser.parse(argc, argv);
    } catch (ParsingException & ex) {
        cerr << ex.message() << '\n';
        //use actual parser and command here
        cerr << parser.formatUsage(progname, command) << '\n';
        return EXIT_FAILURE;
    }

    assert(bool(command)); //it is guaranteed that command will be populated here

    cout << "executing command: " << *command << '\n';
}

Running it produces

$ ./prog
invalid arguments: positional argument command must be present
Usage: ./prog [--user USERNAME] [--help] command
$ ./prog --help
Usage: ./prog [--user USERNAME] [--help] command

positional arguments:
  command               command to perform

options:
  --user USERNAME, -u USERNAME
                        username to use. this option is common to all commands
  --help, -h            show this help message and exit

$ ./prog command1 --help
Usage: ./prog [--user USERNAME] [--help] command1 [--option1] positional1

positional arguments:
  positional1           some positional

options:
  --user USERNAME, -u USERNAME
                        username to use. this option is common to all commands
  --help, -h            show this help message and exit
  --option1, -o1        option 1
$ ./prog command1 
invalid arguments: positional argument positional1 must be present
Usage: ./prog [--user USERNAME] [--help] command1 [--option1] positional1
$ ./prog command1 foo
executing command: command1

This works great now but people often like to have help as a command itself rather than an option.

Subcommand with help as a subcommand

To make help its own command you need to deal with two issues.

  1. A help for ./prog help command is the same as for ./prog command. The parser needs to be configured for both syntaxes. This is impossible to accomplish but the solution is simple: use 2 parsers instead. One will be used to actually parse command line and another for help generation.
  2. Even with above you need to be careful to handle help help recursion.

Below is an example of how to do so.

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

    //global option
    string username;
    //the command to execute, if any
    optional<string> command;
    //command to provide help about, if any
    optional<string> helpCommand;
    //dummy parser to generate help
    Parser helpParser;
    
    
    const char * progname = (argc ? argv[0] : "prog");
    
    //regular parser
    Parser parser;
    //first add global options as usual
    parser.add(
            Option("--user", "-u").
            argName("USERNAME").
            help("username to use. this option is common to all commands").
            handler([&](const std::string_view & value) { 
                username = value;
        }));
    
    //"topic" positional for help command
    //we will use it twice to deal with "help help" recursion
    auto helpTopic = Positional("topic").
                        occurs(neverOrOnce).
                        help("command to provide help about");

    //use addSubCommand instead of regular add!
    parser.addSubCommand(Positional("command").
        help("command to perform").
        handler([&](const std::string_view & value) { 
            //record desired command
            command = value;
            if (value == "help") {
                //for "help help" command use helpTopic positional with special handler 
                helpTopic.handler([&](const std::string_view & value) { 
                    //record command to provide help for
                    helpCommand = value;
                    if (value != "help") {
                        //if the command to provide help for is not "help" itself
                        //configure help parser like regular parser would be for that command
                        configureCommand(helpParser, value);
                    } else {
                        //otherwise configure help parser for "help" command
                        helpParser.add(helpTopic);
                    }
                });
                parser.add(helpTopic);
            } else {
                //otherwise configure both parser and help parser for command
                configureCommand(parser, value);
                //parsers can be simply copied
                helpParser = parser;
            }
    }));

    //parsers can be simply copied
    helpParser = parser;

    try {
        parser.parse(argc, argv);
    } catch (ParsingException & ex) {
        cerr << ex.message() << '\n';
        //use actual parser and command here
        cerr << parser.formatUsage(progname, command) << '\n';
        return EXIT_FAILURE;
    }

    assert(bool(command)); //it is guaranteed that command will be populated here

    if (*command == "help") {
        //use help parser and command here
        cout << helpParser.formatHelp(progname, helpCommand) << '\n';
    } else {
        cout << "executing command: " << *command << '\n';
    }
}

Running this produces

$ ./prog
invalid arguments: positional argument command must be present
Usage: ./prog [--user USERNAME] command
$ ./prog help
Usage: ./prog [--user USERNAME] command

positional arguments:
  command               command to perform

options:
  --user USERNAME, -u USERNAME
                        username to use. this option is common to all commands

$ ./prog help help
Usage: ./prog [--user USERNAME] help [topic]

positional arguments:
  topic                 command to provide help about

options:
  --user USERNAME, -u USERNAME
                        username to use. this option is common to all commands

$ ./prog help command1
Usage: ./prog [--user USERNAME] command1 [--option1] positional1

positional arguments:
  positional1           some positional

options:
  --user USERNAME, -u USERNAME
                        username to use. this option is common to all commands
  --option1, -o1        option 1

$ ./prog help foo     
invalid arguments: invalid command: foo
Usage: ./prog [--user USERNAME] help [topic]
$ ./prog command1 --foo
unrecognized option: --foo
Usage: ./prog [--user USERNAME] command1 [--option1] positional1

As you can see, the main complexity here lies in dealing with the fact that help is itself a command and not an options and the resultant help help recursion.

This code is obviously just an example. In real production code you would probably want to use ChoiceParser for command (see Parsing common data types) and extend help with description of what the command does.

Finally very often with sophisticated command line syntaxes using multiple subcommands you really want a more elaborate help messages than the ones shown above. Perhaps you want special sections for different option types, or examples or something else.

In such case you will need to generate and format help content on your own. Argum can still help with chores like formatting argument syntax and laying out tables, if you want. See Customizing usage and help page for more details.