cmdparse

Tutorial

The complete code for this example can be found in the file example/net.rb of the cmdparse package (or online at cmdparse’s Github repository!

Tl;dr: In this tutorial we will create a small net program which can add, delete and list IP addresses as well as show ‘network statistics’. By doing this we show how easy it is to use the cmdparse library for creating a command based program. The last part shows how our created program can be invoked and the built-in help facility of cmdparse.

Note that the shown code fragments do not comprise the whole program. So, depending on what you like, just look at the code fragments and the explanations or open the example file alongside this tutorial. Later use the example/net.rb file for running the program yourself and testing different command line arguments.

Require statements

First we create a new new file and add the necessary require statements:

require 'cmdparse'

When requiring cmdparse, the optparse library from Ruby’s standard library is also automatically required which is used for doing the option parsing part.

The Basic Command Parser Object

Next we will define our command parser object through the CmdParse::CommandParser class. Objects of this class are used for defining the top level commands of a program as well as the global options and other information like the name of the program or its version.

parser = CmdParse::CommandParser.new(handle_exceptions: :no_help)
parser.main_options.program_name = "net"
parser.main_options.version = "0.1.1"
parser.main_options.banner = "This is net, a s[ai]mple network analytics program"

We use the handle_exceptions argument of the constructor so that exceptions are automatically handled gracefully, i.e. by showing an appropriate error message. If we used true as value, the help screen would be shown in addition to the error message.

The next lines set the name of the program, its version and a banner that is shown on all help messages. All this information is set on the main options. Setting this information on the global options or any other OptionParser instance has no effect!

Specifying Options

An integral part of any CLI program are options that can be set when invoking the program. A command based CLI program has several kinds of options:

  • The main options which can only be used directly after the program name itself. An example for such an option would be a --version switch that only makes sense at the top level.

  • The command specific options which can only be used after the command name and before any sub-command name.

  • The global options which can be used directly after the program name as well as after any command. Therefore global options are normally used for things that affect all commands, like a global verbosity setting.

All these options are specified using the Ruby standard library optparse and its OptionParser class. The OptionParser implementation is battle tested, easy to use and allows great flexibility.

We go back to our example now and define a global option:

parser.global_options do |opt|
  opt.on("-v", "--verbose", "Be verbose when outputting info") do
    parser.data[:verbose] = true
  end
end

The data attribute on the command parse object (or any command object) can be used to store arbitrary information. Here we use it to store the verbosity level so that it can easily be used later by any command.

We could have used a global variable for this but storing such information with the command parser object usually makes for a better design. Note that the data attribute is only really useful when the CmdParse::CommandParser is not sub-classed.

Parsing the Command Line Arguments

We have set up everything that is needed for a basic command based program. The last step is to tell the program to use our newly defined command parser object to process the command line arguments:

parser.parse

The parse method parses the given array of arguments (or ARGV if no array is specified). All the command line arguments are parsed and the given command executed.

The program could now be executed but it won’t be useful as we did not specify any commands yet.

Defining commands

After performing the basic setup, we need to add some commands so that our program actually does something useful.

First, we will add two built-in commands, namely the help and the version command:

parser.add_command(CmdParse::HelpCommand.new, default: true)
parser.add_command(CmdParse::VersionCommand.new)

That was easy! Now you can execute the program and specify the commands help or version. You will also find that the help command is automatically invoked when you don’t specify any command. This is because we used the default: true argument when adding the help command which sets the added command as the default command that should be used if no explicit command name is given.

The next step is to create the needed commands for our program. There are several different ways of doing this.

ipaddr = CmdParse::Command.new('ipaddr')
ipaddr.short_desc = "Manage IP addresses"
parser.add_command(ipaddr, default: true)

One way is to create an instance of CmdParse::Command, update it with all needed properties and then add it to the command parser object or another command.

Since the ipaddr command takes other commands and doesn’t do anything by itself, no action is defined for it. Also notice that we have redefined the default command to be the ipaddr command. The last added command with the argument default: true will be the default command.

ipaddr.add_command('add') do |cmd|
  cmd.takes_commands(false)
  cmd.short_desc("Add an IP address")
  cmd.action do |*ips|
    puts "Adding ip addresses: #{ips.join(', ')}" if parser.data[:verbose]
    parser.data[:ipaddrs] += ips
  end
end

Another way would be to take advantage of the fact that the add_command method creates a CmdParse::Command object when a name is passed as argument instead of a command object itself and that the added command is always yielded if a block is given.

Using this block we can now easily customize the command. Since the ipaddr add command does not take any commands, we need to define an action block that gets called if the command should be executed. By using *ips we advertise to cmdparse that this command takes an arbitrary number of IP addresses as arguments. Note that this is also automatically reflected in the usage line for the command!

We add the ipaddr del and ipaddr list commands in a similar manner and set the list command to be the default sub-command for the ipaddr command.

class NetStatCommand < CmdParse::Command

  def initialize
    super('stat', takes_commands: false)
    short_desc("Show network statistics")
    long_desc("This command shows very useful 'network' statistics - eye catching!!!")
    argument_desc(M: 'start row number', N: 'end row number')
  end

  def execute(m = 1, n)
    puts "Showing network statistics" if command_parser.data[:verbose]
    puts
    m.to_i.upto(n.to_i) do |row|
      puts " "*(20 - row).abs + "#"*(row*2 - 1).abs
    end
    puts
  end

end

parser.add_command(NetStatCommand.new)

The last way for creating a command is to sub-class the CmdParse::Command class and do all customization there. If this is done, it is recommended to override the #execute method instead of setting an action block.

We can also see that the execute method takes one or two arguments and that these arguments are also properly documented.

Running the Program

Now that we have completed our program we can finally run it!

Below are some sample invocations with their respective output and some explanations.

$ ruby example/net.rb 
$

When called with no arguments, the default command is executed. The default top level command is ipaddr and its default command is list which shows the added IP addresses. So far, no IP addresses are stored - we should change that.

$ ruby example/net.rb ip add 192.168.0.1
$

Now we have added one IP address. You might have noticed that we used ip instead of ipaddr. Since partial command matching is automatically done, the shortest unambiguous name for a command can be used. As there is no other command starting with ip (or even with the letter i), it is sufficient to write the above to select the ipaddr command.

Now lets add some more IPs but with some informational output.

$ ruby example/net.rb i a 192.168.0.2 192.168.0.4 192.168.0.3 -v
Adding ip addresses: 192.168.0.2, 192.168.0.4, 192.168.0.3
$

This time we added three IP addresses and by using the global option -v we got some informational output, too.

Let’s display which IP addresses are currently stored.

$ ruby example/net.rb ipaddr list
192.168.0.1
192.168.0.2
192.168.0.4
192.168.0.3
$

So we have four IPs stored. However, we really only need three so we delete one.

$ ruby example/net.rb ip -v del 192.16.8.0.4 
Deleting ip addresses: 192.16.8.0.4
$

That’s much better! But we all are getting sick of this and don’t want any IP addresses stored anymore.

$ ruby example/net.rb ip -a del
Error while parsing command line:
    invalid option: -a
$

Alas, I mistyped that last command. The option -a is a command specific option of ipaddr del and therefore not recognized by ipaddr.

$ ruby example/net.rb ip del -av
All IP adresses deleted!
$ ruby example/net.rb 
$

After deleting all IP addresses none are shown anymore - perfect!

Now we want to see the “network statistics” part of our program. What was the command name again? Let’s get some help!

$ ruby example/net.rb help
This is net, a s[ai]mple network analytics program

Usage: net [options] {help | ipaddr | stat | version}

Available commands:
    help              Provide help for individual commands
    ipaddr (*)        Manage IP addresses
      add             Add an IP address
      del             Delete an IP address
      list (*)        Lists all IP addresses
    stat              Show network statistics
    version           Show the version of the program

Options (take precedence over global options):
    -v, --version                    Show the version of the program

Global Options:
    -v, --verbose                    Be verbose when outputting info
    -h, --help                       Show help

$

Ah, yes, the name was stat, now we remember!

And there are some interesting things in the help output worth pointing out:

  • The usage line shows us that we can define top level option (in addition to the global options) and also nicely lists the available commands.

  • The asterisks in the section for the commands show us the default commands.

  • We have a top level option -v for showing the version as well as a global option -v for setting the verbosity level. As mentioned in the help output, the top level option (or a command specific option) always takes precedence over global options.

To make the last point clear, we run the command with the -v option.

$ ruby example/net.rb -v
This is net, a s[ai]mple network analytics program
net 0.1.1
$

This shows us the version information as expected instead of invoking the default command.

Back to the network statistics. Now we now the command name but we have forgotten how to use the command itself. The help command comes to our rescue again!

$ ruby example/net.rb help stat
This is net, a s[ai]mple network analytics program

Usage: net [options] stat [M] N

Summary:
    stat - Show network statistics

Description:
    This command shows very useful 'network' statistics - eye catching!!!

Arguments:
    M                 start row number N                 end row number

Global Options:
    -v, --verbose                    Be verbose when outputting info
    -h, --help                       Show help

$

There are again some things to point out:

  • We get a short summary and a more detailed description of the command. The short description is the one shown in the general help overview. The detailed description is normally longer than this and fully explains the command.

  • The arguments for the command are also described. When looking at the usage line, you can see that the M argument is optional, but the N argument isn’t (indicated by the brackets). This means that we need at least one argument. If we provide only one argument, it is used for N. And if we provide two arguments, they are used for M and N (in this order).

    How does cmdparse know that M is optional? It has inferred this (as well as the names themselves) by looking at the signature of the execute method of the command!

Now we know everything to invoke the command.

$ ruby example/net.rb stat 5

                   #
                  ###
                 #####
                #######
               #########

$ ruby example/net.rb stat 3 5

                 #####
                #######
               #########

$

Final Words

Our net program is certainly only useful for this tutorial, however, it nicely showcases many of the features of cmdparse and how easy cmdparse is to use.

If you haven’t done so by now, install the cmdparse library, download our sample net program and experiment a bit with it. The API documentation is quite extensive and will answer all remaining questions. And if it doesn’t, you can contact me.