Skip to content

Commit

Permalink
* included patches #4756 and #4729 from Assaph Mehr and slightly modi…
Browse files Browse the repository at this point in the history
…fied them

* added the test file for CommandHash from Assaph Mehr
* updated Rakefile to run test the test file
* updated the tutorial to include information about partial command matching
* minor update to index page
* update sampe net.rb script to use partial command matching
* added emacs desktop file
  • Loading branch information
gettalong committed Jun 17, 2006
1 parent 034acd4 commit 1777bd5
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 18 deletions.
5 changes: 4 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ SRC_RB = FileList['lib/**/*.rb']
# The default task is run if rake is given no explicit arguments.

desc "Default Task"
task :default => :doc
task :default => :test


# End user tasks ################################################################
Expand All @@ -67,6 +67,9 @@ task :clean do
ruby "setup.rb clean"
end

task :test do
ruby "-Ilib test/tc_commandhash.rb"
end

CLOBBER << "doc/output"
desc "Builds the documentation"
Expand Down
8 changes: 8 additions & 0 deletions doc/src/index.page
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ Have a look around the site!

h2. News

<b>2006-06-17 cmdparse 2.0.2 released!!!</b>

Changes:

* Included two patches from Assaph Mehr:
* partial command matching can now be used (see <a href="{reloctable: tutorial.page}">tutorial page</a>)
* now a banner for the help and version commands can be specified

<b>2006-04-05 cmdparse 2.0.1 released!!!</b>

Changes:
Expand Down
26 changes: 19 additions & 7 deletions doc/src/tutorial.page
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,12 @@ Create a new new file and write the necessary @require@ statements.
h3. The @CommandParser@ class

Next we will define our basic @CommandParser@ by defining the name of the program, its version and
the global options. The boolean argument to the constructor of the @CommandParser@ class defines
whether exceptions should be handled gracefully, i.e. by showing an appropriate error message and
the help screen.
the global options. The first boolean argument to the constructor of the @CommandParser@ class
defines whether exceptions should be handled gracefully, i.e. by showing an appropriate error
message and the help screen. The second boolean argument defines whether the top level commands
should use partial command matching instead of full command matching. If partial command matching is
used, then the shortest unambiguous part of a command name can be used instead of always specifing
the full command name.

<notextile>{extract: {file: ../net.rb, lines: !ruby/range 30..36}}</notextile>

Expand Down Expand Up @@ -97,10 +100,10 @@ However, we want the program to do something "useful". Therefore we define a new

<notextile>{extract: {file: ../net.rb, lines: !ruby/range 41..44}}</notextile>

This command is defined by using the default @Command@ class. First an instance is created
assigning a name to the command and defining whether this command takes subcommands. Next we add a
short description so that the @help@ command can produce something useful. And at last, we add this
command to our @CommandParser@ instance.
This command is defined by using the default @Command@ class. First an instance is created assigning
a name to the command and defining whether this command takes subcommands and uses partial command
matching. Next we add a short description so that the @help@ command can produce something useful.
And at last, we add this command to our @CommandParser@ instance.

We specified that our @ipaddr@ command takes subcommands. So we have to define them, too:

Expand Down Expand Up @@ -158,6 +161,15 @@ Why? As the @ipaddr@ command takes subcommands there should be an additional com
@list@) on the command line. However, as the @list@ command is the default command for @ipaddr@ you
do not need to type it.

*By the way:* You get the same output if you type
<pre>
$ ruby net.rb ip
</pre>

Why? As partial command matching is used for the top level commands, 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.

*Notice:* The options of a command which does not take subcommands do not need to be at the front;
they can be anywhere, like this
<pre>
Expand Down
43 changes: 35 additions & 8 deletions lib/cmdparse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
module CmdParse

# The version of this cmdparse implemention
VERSION = [2, 0, 1]
VERSION = [2, 0, 2]


# Base class for all cmdparse errors.
Expand Down Expand Up @@ -103,6 +103,18 @@ def summarize
# Require default option parser wrapper
require 'cmdparse/wrappers/optparse'

# Command Hash - will return partial key matches as well if there is a single
# non-ambigous matching key
class CommandHash < Hash

def []( cmd_name )
super or begin
possible = keys.select {|key| key =~ /^#{cmd_name}.*/ }
fetch( possible[0] ) if possible.size == 1
end
end

end

# Base class for the commands. This class implements all needed methods so that it can be used by
# the +CommandParser+ class.
Expand Down Expand Up @@ -131,13 +143,21 @@ class Command
attr_reader :commands

# Initializes the command called +name+. The parameter +has_commands+ specifies if this command
# takes other commands as argument.
def initialize( name, has_commands )
# takes other commands as argument. The optional argument +partial_commands+ specifies, if
# partial command matching should be used.
def initialize( name, has_commands, partial_commands = false )
@name = name
@options = ParserWrapper.new
@has_commands = has_commands
@commands = {}
@commands = Hash.new
@default_command = nil
use_partial_commands( partial_commands )
end

def use_partial_commands( use_partial )
temp = ( use_partial ? CommandHash.new : Hash.new )
temp.update( @commands )
@commands = temp
end

# Returns +true+ if this command supports sub commands.
Expand Down Expand Up @@ -272,7 +292,7 @@ def execute( args )
if args.length > 0
cmd = commandparser.main_command
arg = args.shift
while !arg.nil? && cmd.commands.keys.include?( arg )
while !arg.nil? && cmd.commands[ arg ]
cmd = cmd.commands[arg]
arg = args.shift
end
Expand All @@ -292,6 +312,7 @@ def execute( args )
#######

def show_program_help
puts commandparser.banner + "\n" if commandparser.banner
puts "Usage: #{commandparser.program_name} [options] COMMAND [options] [COMMAND [options] ...] [args]"
puts ""
list_commands( 1, commandparser.main_command )
Expand Down Expand Up @@ -331,6 +352,7 @@ def usage
def execute( args )
version = commandparser.program_version
version = version.join( '.' ) if version.instance_of?( Array )
puts commandparser.banner + "\n" if commandparser.banner
puts version
exit
end
Expand All @@ -341,6 +363,9 @@ def execute( args )
# The main class for creating a command based CLI program.
class CommandParser

# A standard banner for help & version screens
attr_accessor :banner

# The top level command representing the program itself.
attr_reader :main_command

Expand All @@ -354,10 +379,12 @@ class CommandParser
attr_reader :handle_exceptions

# Create a new CommandParser object. The optional argument +handleExceptions+ specifies if the
# object should handle exceptions gracefully.
def initialize( handleExceptions = false )
# object should handle exceptions gracefully. Set +partial_commands+ to +true+, if you want
# partial command matching for the top level commands.
def initialize( handleExceptions = false, partial_commands = false )
@main_command = Command.new( 'mainCommand', true )
@main_command.super_command = self
@main_command.use_partial_commands( partial_commands )
@program_name = $0
@program_version = "0.0.0"
@handle_exceptions = handleExceptions
Expand Down Expand Up @@ -402,7 +429,7 @@ def parse( argv = ARGV ) # :yields: level, commandName
cmdName = command.default_command
end
else
raise InvalidCommandError.new( cmdName ) unless command.commands.include?( cmdName )
raise InvalidCommandError.new( cmdName ) unless command.commands[ cmdName ]
end

command = command.commands[cmdName]
Expand Down
4 changes: 2 additions & 2 deletions net.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def execute( args )

end

cmd = CmdParse::CommandParser.new( true )
cmd = CmdParse::CommandParser.new( true, true )
cmd.program_name = "net"
cmd.program_version = [0, 1, 1]
cmd.options = CmdParse::OptionParserWrapper.new do |opt|
Expand All @@ -39,7 +39,7 @@ def execute( args )
cmd.add_command( NetStatCommand.new )

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

Expand Down
96 changes: 96 additions & 0 deletions test/tc_commandhash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
require 'test/unit'
require 'cmdparse'

class CommandHashTest < Test::Unit::TestCase
def setup
@cmd = CmdParse::CommandHash.new
end

def test_basic
assert_equal 0, @cmd.size
assert_nil @cmd['import']
@cmd['import'] = 1
assert_equal 1, @cmd['import']
assert_equal 1, @cmd['i']
assert_equal 1, @cmd['im']
assert_equal 1, @cmd['imp']
assert_equal 1, @cmd['impo']
assert_equal 1, @cmd['impor']
assert_nil @cmd['importer']

@cmd['implode'] = 2
assert_equal 2, @cmd.size

assert_equal 1, @cmd['import']
assert_equal 2, @cmd['implode']
assert_nil @cmd['impart']

assert_nil @cmd['i']
assert_nil @cmd['im']
assert_nil @cmd['imp']
assert_equal 1, @cmd['impo']
assert_equal 1, @cmd['impor']
assert_equal 1, @cmd['import']
assert_equal 2, @cmd['implo']
assert_equal 2, @cmd['implod']
assert_equal 2, @cmd['implode']
end

def test_edge_cases
@cmd['import'] = 1
@cmd['important'] = 2

assert_equal 1, @cmd['import']
assert_equal 2, @cmd['important']
assert_nil @cmd['i']
assert_nil @cmd['im']
assert_nil @cmd['imp']
assert_nil @cmd['impo']
assert_nil @cmd['impor']
assert_equal 2, @cmd['importa']
assert_equal 2, @cmd['importan']

assert_nil @cmd['impart']
end

def test_integration
# define and setup the commands
cmd = CmdParse::CommandParser.new(handle_exceptions = false)
cmd.main_command.use_partial_commands( true )
Object.const_set(:ImportCommand, Class.new(CmdParse::Command) do
def initialize() super('import', false) end
def execute(args) raise 'import' end
def show_help() raise 'import' end
end)
Object.const_set(:ImpolodeCommand, Class.new(CmdParse::Command) do
def initialize() super('implode', false) end
def execute(args) raise 'implode' end
def show_help() raise 'implode' end
end)
cmd.add_command( ImportCommand.new )
cmd.add_command( ImpolodeCommand.new )

# simulate running the program
assert_raises(RuntimeError, 'import') {cmd.parse(['import'])}
assert_raises(RuntimeError, 'implode') {cmd.parse(['implode'])}
assert_raises(CmdParse::InvalidCommandError) {cmd.parse(['impart'])}

assert_raises(CmdParse::InvalidCommandError) {cmd.parse(['i'])}
assert_raises(CmdParse::InvalidCommandError) {cmd.parse(['im'])}
assert_raises(CmdParse::InvalidCommandError) {cmd.parse(['imp'])}
assert_raises(RuntimeError, 'import') {cmd.parse(['impo'])}
assert_raises(RuntimeError, 'import') {cmd.parse(['impor'])}
assert_raises(RuntimeError, 'implode') {cmd.parse(['impl'])}
assert_raises(RuntimeError, 'implode') {cmd.parse(['implo'])}
assert_raises(RuntimeError, 'implode') {cmd.parse(['implod'])}

# simulate the help command
cmd.add_command( CmdParse::HelpCommand.new )
assert_raises(RuntimeError, 'import') {cmd.parse(['help', 'import'])}
assert_raises(RuntimeError, 'implode') {cmd.parse(['help', 'implode'])}

cmd.main_command.use_partial_commands( false )
assert_raises(CmdParse::InvalidCommandError, 'import') {cmd.parse(['impo'])}
assert_raises(CmdParse::InvalidCommandError, 'implode') {cmd.parse(['impl'])}
end
end

0 comments on commit 1777bd5

Please sign in to comment.