clipp icon indicating copy to clipboard operation
clipp copied to clipboard

Is there a way to define a "default" command (if none is given)?

Open risa2000 opened this issue 4 years ago • 3 comments

I am using the commands semantics to implement several mandatory choices, but I would like to have a default command choice, when the user does not include the command on the command line.

So far I have found a workaround which parses the arguments twice. First with the (mandatory) commands and then without them. If the second parsing succeeds I assume the command was not given and supply the default one. I.e. like this:

clipp::group cli_fallback = (
    (option("-a"),
    (option("-b"),
    (option("-c")
    );

clipp::group cli = (
    (command("cmd1") \
        | command("cmd2") \
        | command("cmd3") \
        | command("cmd4")),
    cli_fallback);

if (parse(argc, argv, cli)) {
        res = run(...);
} else {
    if (parse(argc, argv, cli_fallback)) {
        res = run(default_cmd, ...);
    } else {
        std::cout << "Usage:\n" << usage_lines(...) << '\n';
    }
}

This works, but seems a bit heavy.

risa2000 avatar Jul 11 '19 15:07 risa2000

You could achieve that by separating the definitions of the commands and options from the parsing control flow logic:

    auto opts = (
        option("-a"),
        option("-b"),
        option("-c")
    );
    auto cmds = ( 
          command("cmd1")
        | command("cmd2")
        | command("cmd3")
        | command("cmd4")
    );
    auto cli = ( (cmds, opts) | opts );  // <--

muellan avatar Jul 11 '19 19:07 muellan

This would be probably good enough for me, but I cannot make it work. Here is a testing code to reproduce the behavior:

#include <iostream>
#include <string>
#include <clipp.h>

int main(int argc, char *argv[])
{
    using namespace clipp;

    //variables storing the parsing result; initialized with their default values
    enum class mode { cmd1, cmd2, cmd3 };
    mode selected = mode::cmd1;
    std::string input;
    int verb = 0;

    auto cli_cmds = (
        (command("cmd1").set(selected, mode::cmd1).doc("command 1") \
            | command("find").set(selected, mode::cmd2).doc("command 2") \
            | command("help").set(selected, mode::cmd3).doc("command 3")));
 
    auto cli_opts = (
        option("-i", "--input") & value("data", input) % "testing input",
        (option("-v", "--verb").set(verb, 1) & opt_value("level", verb)) % "verbosity"
        );

    auto cli = ((cli_cmds, cli_opts) | cli_opts);

    if (parse(argc, argv, cli)) {
        switch (selected) {
        case mode::cmd1:
        case mode::cmd2:
        case mode::cmd3:
            std::cout << "selected: " << static_cast<int>(selected) << '\n';
            std::cout << "input: " << input << '\n';
            std::cout << "verb: " << verb << '\n';
        }
    }
    else {
        std::cout << usage_lines(cli, "tester") << '\n';
    }
    return 0;
}

If I do not give any command on the CL, the parsing fails. Changing the processing to the original idea works however:

    if (parse(argc, argv, (cli_cmds, cli_opts))) {
        switch (selected) {
        case mode::cmd1:
        case mode::cmd2:
        case mode::cmd3:
            std::cout << "selected: " << static_cast<int>(selected) << '\n';
            std::cout << "input: " << input << '\n';
            std::cout << "verb: " << verb << '\n';
        }
    }
    else {
        if (parse(argc, argv, cli_opts)) {
            std::cout << "selected: " << static_cast<int>(mode::cmd1) << '\n';
            std::cout << "input: " << input << '\n';
            std::cout << "verb: " << verb << '\n';
        }
        std::cout << usage_lines(cli, "tester") << '\n';
    }
    return 0;

risa2000 avatar Jul 12 '19 08:07 risa2000

FWIW I accidentally figured out the situation when it works as you proposed. Here is a streamlined testcase:

int main(int argc, char *argv[])
{
    using namespace clipp;

    enum class mode { cmd1, cmd2 };
    mode selected = mode::cmd1;
    std::string name;
    std::string opt1;

    auto cli_cmds = (
        command("cmd1").set(selected, mode::cmd1).doc("command 1")
        | command("cmd2").set(selected, mode::cmd2).doc("command 2"));
 
    auto cli_args = value("name", name) % "pos. arg. name";
    auto cli_opts = (option("-o", "--option") & value("option", opt1)) % "option";
    //auto cli_nocmd = (cli_args, cli_opts);
    auto cli_nocmd = cli_opts;
    auto cli = ((cli_cmds, cli_nocmd) | cli_nocmd);

    if (parse(argc, argv, cli)) {
        switch (selected) {
        case mode::cmd1:
        case mode::cmd2:
            std::cout << "selected: " << static_cast<int>(selected) << '\n';
            std::cout << "option: " << opt1 << '\n';
            std::cout << "name: " << name << '\n';
        }
    }
    else {
        std::cout << "Usage:\n" << usage_lines(cli, "tester") << '\n';
    }
    return 0;
}

Basically the CLI definition has two parts: cli_cmds which contains commands, and cli_nocmd which is the rest. The rest can differ as will be explained below. Then I use:

auto cli = ((cli_cmds, cli_nocmd) | cli_nocmd);

to define the "composite" definition to also accept a command line without any command (i.e. using "default" command).

The code as presented above does not work. Running the executable without any arguments does not get parsed properly (as "no command & no option").

However if I add additional positional argument name into "no commands" definition (currently commented out) and run the code, it starts working as expected. I.e. running it with only the positional argument gets parsed correctly.

It seems that if there is no argument on the command line and no required argument in cli_nocmd definition the whole cli_nocmd part gets ignored and the parsing fails.

Can it be qualified as a bug?

risa2000 avatar Aug 25 '19 06:08 risa2000