Git like sub-commands with argh

It has been a while, but finally I got a good motivator and a reason to write a blog post. Not that I am lazy or always need some extra stimulation to write something, but usually all may spare time that was originally planed to use for blogging is used for this meetup group and generating videos like those. But this motivation is a good one, and I hope I can contribute a little bit back to the great work of Adi Shavit is providing with his argh, so here the post.

arghlogo

Why argh

Over time I have used several command line helpers and 'frameworks' in C++. To name a few: Boost.Program_options, argagg, args, this argh and that argh

My favorite is the last in the list, that argh. As the description of argh says(I just copy the words for there).
A minimalist argument handler for frustration-free command line processing.
I like the minimalism, and the simplicity argh brings. As the logo says, less is more! And it does not come into may way.

3 reasons for me to use argh

What do I mean with less is more and does not come into my way? Here are the first 3 reasons that pop into my mind::

  • The help text. Some other parsers allow you to attach help text and will care about formating and putting it on the command line. This is nice, except, when it turns out that a slightly different formatting makes the help text more easy to read, and therefore, simpler to understand.

  • Type conversion. argh does not do much. It gives me either a string or a stringstream and lets me do the rest. I consider this to be of an advantage, in opposite to either be forced to exception handling or to some interesting behavior for unexpected or bad input.

  • Inclusion into your project. argh is a very small header. If wanted, just drop it into your project and you are good to go. And it is clean code that is easy to read and understand, you see what is going on. No surprises.

Adding some functionality to argh

Of course, adding functionality like help text, or sub command line parsing need to be added by your self.

But this is not a problem As I will show, its easy. The few lines of 'boiler plate' needed to be added by hand is negligible in the context of any applications, and when I look what I am getting I am more than willy to write those hand full lines of code myself!

Here an example that adds git like sub-commands and help text to your application when you use argh.

Please keep in mind that this is just one possible implementation. More advanced and/or different ways of adding this functionality are possible, especially if you use more modern C++ than what I do in this example. (C++11)

Git like sub-commands with argh

What are git like sub-commands

What is git like command line parsing?

In short, instead of calling you applications with arguments, like
app arguments …​
you use
app command arguments …​
where command is an action you want to perform, and the arguments are special for each command action of you program.

Let’s explore a possible implementation for this.

Arguments for our example application

First, some planning for the arguments of the application.

General arguments

We will have of course the usual --help or -h option for showing a general help message, and the also usual --version or -v option for showing some version information.

you might notice that I use a unix like command line options, usual for Linux and OSX. So --help or -h only, no '/?' in this example, but if needed, this would be easy to add.
Sub commands
  • a echo command, taking an optional --repeat argument and a message to write out. --repeat can be a number from 1 to 10 and if provided, the message will be repeated that often

  • a commands command that takes no arguments, and lists available commands

  • a help command, taking an existing command as argument for getting command specific help

Implementation

Please keep in mind that this is a naive implementation, mostly to demonstrate how to do sub command parsing with argh.

First, we need to define some helpers.

using Action = std::function<int (int, const char**)>;

struct Command {
  std::string name ;
  std::string help;
  Action exec;
};

struct CommandCompare {
  bool operator()(const Command& first, const Command& second) {
    return first.name < second.name ;
  }
};

using Commands = std::set<Command, CommandCompare> ;

It should be pretty obvious what is going on. The action for a sub command is a std::function, so it can either be a function, or a lambda, or any other callable object. This sub command handler returns success or a failure code, like the main function does. A command has a name, some help text, and a action. All commands are collected in a standard set, so we need a simple compare function for Command objects.

That’s it, now we need just to put Command instances into a Commands set, and we are done.

We start with creating a Commands set:

  Commands cmds ;

and add the first Commnad, the echo command.

Adding the echo command

We provide the name of the command, a descriptive help text for the command itself, and some command action.

  cmds.insert (
    Command{
      "echo" ,
      R"~(
  usage:
     echo [--repeat=<REP>^] <MSG>

     --repeat, optional, integer. default is 1
        How often MSG shall be echoed out
        Values from 1 to 10 are valid

     MSG, required, the message to show
        )~",
        [](int argc, const char** argv) -> int {
          auto cmdArgs = argh::parser (argc, argv);
          assert(cmdArgs.pos_args().at (0) == "echo") ;
          auto repeat_iter = cmdArgs.params ().find ("repeat");
          int repeat = 1 ;
          if (repeat_iter != cmdArgs.params ().end ()) {
            repeat = std::stoi(repeat_iter->second);
          }
          if(repeat < 1 || repeat > 10) {
            std::cerr << "invalid repeat cout\n";
            return EXIT_FAILURE ;
          }
          if (cmdArgs.pos_args ().size () < 2) {
            std::cerr << "Message argument missing\n";
            return EXIT_FAILURE ;
          }

          for (int i = 0; i < repeat; ++i) {
            std::cout << cmdArgs.pos_args().at (1) ;
          }
          return EXIT_SUCCESS ;
        }
    });

Quite some code for a simple echo function. Most of it is the input validation where we check that

  • the function was called for the right command

  • an optional given repeat value is in the accepted range

  • there is a message to write out

As you can see, this sub command handler uses argh for parsing of its command arguments.

The description of how argh is working is not part of this post. Please visit the documentation of argh if you need to learn how argh works.

Adding the help command

The help command is simpler than the echo command, but even this command requires some input validation. (You start to see why I have written that the code you need to add for argh is not important in the context of a program?)

    cmds.insert (Command {
      "help" ,
      R"~(
  usage:
     help <COMMAND>

     COMMAND, required, the command for which help shall be shown
        )~",
        [&](int argc, const char** argv) -> int{

          auto cmdArgs = argh::parser (argc, argv);
          assert(cmdArgs.pos_args().at (0) == "help");
          if (cmdArgs.pos_args ().size () < 2) {
            std::cerr << "Command argument missing\n";
            return EXIT_FAILURE ;
          }
          const auto cmd = cmds.find(Command{cmdArgs.pos_args().at (1)}) ;
          if (cmd == cmds.end ()) {
            std::cerr << "No help for <not found> available\n";
            return EXIT_FAILURE ;
          }
          std::cout << cmd->help << std::endl ;
          return EXIT_SUCCESS ;
        }
    });

We again do some defensive programming and check that the function was called for the right command. Then we need to check that the given command is a known command and does the appropriate action for yes and no. And we capture the environment to have access to the commands set.

And we use argh for parsing of the command arguments, even if in this simple case it could be done different.

Adding the commands command

The commands command is the simplest one, since it takes no arguments.

    cmds.insert (Command {
      "commands" ,
      R"~(
  usage:
     commands

     prints the available commands
        )~",
        [&cmds](int /*argc*/, const char** /*argv*/) -> int{
          for (const auto cmd : cmds) {
            std::cout << cmd.name << "\n" ;
          }
          return EXIT_SUCCESS ;
        }
    }
  );

We create an other stateful lambda, capturing from the environment more selective than before, and work with this data.

Now we are nearly done.

The general help

We need some general help, in case someone runs app -help

std::string general_help = R"~(
  gitlike argh example program.
  A program to demonstrate git like subcommand parsing with argh.

  Usage: program COMMAND [OPTIONS^]

  Commands:
    help COMMAND
        print the help text to the given command
        use this to get more information about the following commands

    echo ARGS
        runs the test command example doing nothing

    commands
        print available commands

  General options:
    -v/--version
        print the program version and exit

    -h/--help
        print this hel text and exit
)~";

Easy, raw string literals FTW!

Lets put this all together

The main function

The main function looks as followed.

int main(int argc, const char** argv) {

  // insert the code from above here
  // than do the command and sub command handling

  if(argc < 2 ) {
    std::cerr << "arguments missing\n";
    return EXIT_FAILURE ;
  }

  std::string firstArg = argv[1^] ;

  if (firstArg == "--help" || firstArg == "-h") {
    std::cout << general_help ;
    return EXIT_SUCCESS;
  }

  if (firstArg == "--version" || firstArg == "-v") {
    std::cout << "Version 0.0.0\n";
    return EXIT_SUCCESS;
  }

  const auto cmd = cmds.find(Command{firstArg}) ;
  if(cmd == cmds.end()) {
    std::cerr << "No such command: "<< firstArg << "\n";
    return EXIT_FAILURE ;
  }

  return cmd->exec(argc-1, argv+1);

}

Note the argc-1, argv+1. We can strip away the first argument since it will be the name of the applications and is not needed. The sub-command will get as its first argument the name of the command Useful for validation, especially in more complex scenarios.

In a real world app you might want to have some more structure, but for giving you some idea how git like sub command handling with argh does work, this small example should be sufficient.

Now run it to see how it works

./gitlike echo -repeat=3 foo
foofoofoo

./gitlike echo  foo
foo

./gitlike commands
commands
echo
help

./gitlike help echo

  usage:
     echo [--repeat=<REP>] <MSG>

     --repeat, optional, integer. default is 1
        How often MSG shall be echoed out
        Values from 1 to 10 are valid

     MSG, required, the message to show

For more variations, the link to a working code example can be found below

Summary

Since we have raw string literals in C++, writing help text is fun. And since both, terminal and code editor, use usually a monospace font it is pure WYSIWYG :-)

Parsing of command line arguments is input parsing, and this is better under your control. Test it. Fuzzy it, if you deal with production code.

Less is more, so true for argh!

Link to example code to play with: https://github.com/a4z/argh_blog_example_code

Thanks for reading

As usual, if you find any mistakes, in the code or in my spelling, please let me know.
Ideas for improvements, or any other feedback, is also very much welcome.