-
Notifications
You must be signed in to change notification settings - Fork 0
Subcommands
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.
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
- Tell the parser that the
Positional
for the command is a special "subcommand" one. You do this by usingaddSubCommand
method instead of the usualadd
. - Pass the actual selected command, if any, to
formatUsage
andformatHelp
methods.
Below is an example of doing so
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.
To make help
its own command you need to deal with two issues.
- 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. - 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.
- Home
-
Usage Guide
Basics
Error Handling
Thread Safety
Adding Help
Defining options
Positional arguments
Reporting errors from handlers
Parsing common data types
Validation
Depending on order
Subcommands
Response files
Partial Parsing -
Advanced
Syntax Description
Customizing syntax
Customizing usage and help
Localization