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 theN
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 forN
. And if we provide two arguments, they are used forM
andN
(in this order).How does
cmdparse
know thatM
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.