Ruby standard library provides a good solution for parsing command line options passed to the script or application. It’s called OptionParser. It’s powerful and very easy to use. The code you have to write is easy to read and modify. It also supports different kind of options: short and long flags, required or optional arguments, also different types of arguments including dates, lists and so on.
The only thing it cannot do, is parsing commands - that is a word without any flag before it. For
example, it will not pick do_something
from this command line:
Soon we will learn how to improve OptionParser, so that it is able to do that. Of course there are many other different libraries that do command line argument parsing, including:
They were created for different purposes, so not all of them support commands. Definitely you could pick one of them to do the job, but I personally do not like to be dependant on 3rd party libraries, that are not included in standard ruby installation. In this case also because as we will see, it is very easy to add needed functionality to the OptionParser.
We are creating a parser, that will allow having different commands in cli, these commands can have their own attributes and there are also some global attributes, that can be provided with any command.
We want to use the new option parser similar to existing one. At the same time we should be able to have separate command options for every command, like that:
1
2
3
4
5
6
7
8
9
10
11
12
13
@global_options = OptionParser::OptionMap.new
@command_options = OptionParser::OptionMap.new
parser = CommandParser.new 'Usage: my_program [global-options] <command> [command-options]' do |opts|
opts.separator "Global options (accepted by all commands):"
opts.on '-h', '--help', 'Show help' do
@global_options[:help] = true
end
opts.command 'my-command', 'It surely does something' do |cmd|
cmd.on '-a', '--attribute INT', Integer, 'Some integer attribute for the command.' do |opt|
@command_options[:attribute] = opt
end
end
end
We can achieve that by creating our OptionParserWithCommands
that inherits from OptionParser
and
giving it a new method called command
, which just should create a OptionParser::Switch
and add
it to an instance variable commands
.
1
2
3
4
5
6
7
8
9
10
11
class OptionParserWithCommands < OptionParser
def command( key, desc, &block )
sw = OptionParser::Switch::NoArgument.new( key, nil, [key], nil, nil, [desc],
Proc.new{ OptionParser.new( &block ) } ), [], [key]
commands[key.to_s] = sw[0]
end
def commands
@commands ||= {}
end
end
Still the most important part is missing: parsing. We have to rewrite the parse!
method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def parse!( argv=default_argv )
# Find, if there is a command in argv
@command_name = argv.detect{|c| commands.has_key? c }
if command_name
#create a temporary parser with option definitions from both - global and particular command -
# and parse all the options
OptionParser.new do |parser|
parser.instance_variable_set(:@stack,
commands[command_name.to_s].block.call.instance_variable_get(:@stack) + @stack)
end.parse! argv
else
# we do not have any (right) command provided, fallback to just parsing the options.
super( argv )
end
end
Perfect! There is just 1 thing missing - help message. Remember, OptionParser generates help automatically based on options’ definitions. We want our extended parser to do that too. But providing all the options for all the commands may be too much. So let’s do it this way. The global options are always listed in help. And then, if a command is not provided, we list all the available commands with descriptions (without their personal options) and, if a command is provided, we list all the personal options for this command together with command’s description.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
attr_reader :command_name
private
def summarize(to = [], width = @summary_width, max = width - 1, indent = @summary_indent, &blk)
super(to, width, max, indent, &blk) # display global options
# command is provided
if command_name and commands.has_key?( command_name )
to << "Command:\n"
# display command description
commands[command_name].summarize( {}, {}, width, max, indent ) do |l|
to << (l.index($/, -1) ? l : l + $/)
end
to << "Command options:\n"
# display command options
commands[command_name].block.call.summarize( to, width, max, indent, &blk )
else
to << "Commands:\n"
# display available commands
commands.each do |name, command|
command.summarize( {}, {}, width, max, indent ) do |l|
to << (l.index($/, -1) ? l : l + $/)
end
end
end
to
end
I have it implemented in a ruby gem jenkins2. There are also some tests, which show the supported options and commands.
So now if you would like to parse commands with OptionParser, all you need is just copy the code from this file to your project.
This approach does not support nested commands, but it should be faily easy to add it, by replacing
OptionParser
with OptionParserWithCommands
inside parse!
method and may be adding some checks.