# #-- # # $Id: cmdparse.rb 297 2005-06-14 09:30:22Z thomas $ # # cmdparse: an advanced command line parser using optparse which supports commands # Copyright (C) 2004 Thomas Leitner # # This program is free software; you can redistribute it and/or modify it under the terms of the GNU # General Public License as published by the Free Software Foundation; either version 2 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without # even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License along with this program; if not, # write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # #++ # # Look at the +CommandParser+ class for details and an example. # require 'optparse' # Some extension to the standard option parser class class OptionParser if const_defined?( 'Officious' ) Officious.delete( 'version' ) Officious.delete( 'help' ) else DefaultList.long.delete( 'version' ) DefaultList.long.delete( 'help' ) end # Returns the @banner value. Needed because the method OptionParser#banner does # not return the internal value of @banner but a modified one. def get_banner @banner end # Returns the @program_name value. Needed because the method # OptionParser#program_name does not return the internal value of @program_name # but a modified one. def get_program_name @program_name end end # = CommandParser # # == Introduction # # +CommandParser+ is a class for analyzing the command line of a program. It uses the standard # +OptionParser+ class internally for parsing the options and additionally allows the # specification of commands. Programs which use commands as part of their command line interface # are, for example, Subversion's +svn+ program and Rubygem's +gem+ program. # # == Example # # require 'cmdparse' # require 'ostruct' # # class TestCommand < CommandParser::Command # # def initialize # super('test') # @internal = OpenStruct.new # @internal.function = nil # @internal.audible = false # options.separator "Options:" # options.on("-t", "--test FUNCTION", "Test only FUNCTION") do |func| # @internal.function = func # end # options.on("-a", "--[no-]audible", "Run audible") { |@internal.audible| } # end # # def description # "Executes various tests" # end # # def execute( commandParser, args ) # puts "Test: "+ args.inspect # puts @internal.inspect # end # # end # # cmd = CommandParser.new # cmd.options do |opt| # opt.program_name = "testProgram" # opt.version = [0, 1, 0] # opt.release = "1.0" # opt.separator "Global options:" # opt.on("-r", "--require TEST", "Require the TEST") # opt.on("--delay N", Integer, "Delay test for N seconds before executing") # end # cmd.add_command TestCommand.new, true # sets this command as default command # cmd.add_command CommandParser::HelpCommand.new # cmd.add_command CommandParser::VersionCommand.new # cmd.parse!( ARGV ) # class CommandParser # The version of the command parser VERSION = [1, 0, 4] # This error is thrown when an invalid command is encountered. class InvalidCommandError < OptionParser::ParseError const_set( :Reason, 'invalid command'.freeze ) end # This error is thrown when no command was given and no default command was specified. class NoCommandGivenError < OptionParser::ParseError const_set( :Reason, 'no command given'.freeze ) end # Base class for the commands. This class implements all needed methods so that it can be used by # the +OptionParser+ class. class Command # The name of the command attr_reader :name # The command line options, an instance of +OptionParser+. attr_reader :options # Initializes the command and assignes it a +name+. def initialize( name ) @name = name @options = OptionParser.new end # For sorting commands by name def <=>( other ) @name <=> other.name end # Should be overridden by specific implementations. This method is called after the command is # added to a +CommandParser+ instance. def init( commandParser ) end # Default method for showing the help for the command. def show_help( commandParser ) @options.program_name = commandParser.options.program_name if @options.get_program_name.nil? puts "#{@name}: #{description}" puts usage puts "" puts options.summarize end # Should be overridden by specific implementations. Defines the description of the command. def description '' end # Defines the usage line for the command. Can be overridden if a more specific usage line is needed. def usage "Usage: #{@options.program_name} [global options] #{@name} [options] args" end # Must be overridden by specific implementations. This method is called by the +CommandParser+ # if this command was specified on the command line. def execute( commandParser, args ) raise NotImplementedError end end # The default help command.It adds the options "-h" and "--help" to the global +CommandParser+ # options. When specified on the command line, it can show the main help or an individual command # help. class HelpCommand < Command def initialize super( 'help' ) end def init( commandParser ) commandParser.options do |opt| opt.on_tail( "-h", "--help [command]", "Show help" ) do |cmd| execute( commandParser, cmd.nil? ? [] : [cmd] ) end end end def description 'Provides help for the individual commands' end def usage "Usage: #{@options.program_name} help COMMAND" end def execute( commandParser, args ) if args.length > 0 if commandParser.commands.include?( args[0] ) commandParser.commands[args[0]].show_help( commandParser ) else raise OptionParser::InvalidArgument, args[0] end else show_program_help( commandParser ) end exit end private def show_program_help( commandParser ) if commandParser.options.get_banner.nil? puts "Usage: #{commandParser.options.program_name} [global options] [options] [args]" else puts commandParser.options.banner end puts "" puts "Available commands:" width = commandParser.commands.keys.max {|a,b| a.length <=> b.length }.length commandParser.commands.sort.each do |name, command| print commandParser.options.summary_indent + name.ljust( width + 4 ) + command.description print " (=default command)" if name == commandParser.default print "\n" end puts "" puts commandParser.options.summarize end end # The default version command. It adds the options "-v" and "--version" to the global # +CommandParser+ options. When specified on the command line, it shows the version of the # program. The output can be controlled by options. class VersionCommand < Command def initialize super( 'version' ) @fullversion = false options.separator "Options:" options.on( "-f", "--full", "Show the full version string" ) { @fullversion = true } end def init( commandParser ) commandParser.options do |opt| opt.on_tail( "--version", "-v", "Show the version of the program" ) do execute( commandParser, [] ) end end end def description "Shows the version of the program" end def usage "Usage: #{@options.program_name} version [options]" end def execute( commandParser, args ) if @fullversion version = commandParser.options.ver else version = commandParser.options.version version = version.join( '.' ) if version.instance_of? Array end version = "" if version.nil? puts version exit end end # Holds the registered commands. attr_reader :commands # Returns the name of the default command. attr_reader :default # Are Exceptions be handled gracefully? I.e. by printing error message and help screen? attr_reader :handleExceptions # Create a new CommandParser object. The optional argument +handleExceptions+ specifies if the # object should handle exceptions gracefully. def initialize( handleExceptions = false ) @options = OptionParser.new @commands = {} @default = nil @parsed = {} @handleExceptions = handleExceptions end # If called with a block, this method yields the global options of the +CommandParser+. If no # block is specified, it returns the global options. def options # :yields: options if block_given? yield @options else @options end end # Adds a command to the command list. If the optional parameter +default+ is true, then this # command is used when no command is specified on the command line. def add_command( command, default = false ) @commands[command.name] = command @default = command.name if default command.init( self ) end # Calls +parse+ - implemented to mimic OptionParser def permute( args ); parse( args ); end # Calls +parse!+ - implemented to mimic OptionParser def permute!( args ); parse!( args ); end # Calls +parse+ - implemented to mimic OptionParser def order( args ); parse( args ); end # Calls +parse!+ - implemented to mimic OptionParser def order!( args ); parse!( args ); end # see CommandParser#parse! def parse( args ); parse!( args.dup ); end # Parses the given argument. First it tries to parse global arguments if given. After that the # command name is analyzied and the options for the specific commands parsed. If +execCommand+ # is true, the command is executed immediately. If false, the +CommandParser#execute+ has to be # called to execute the command. def parse!( args, execCommand = true ) # parse global options begin @options.order!( args ) @parsed[:command] = args.shift if @parsed[:command].nil? if @default.nil? raise NoCommandGivenError else @parsed[:command] = @default end else raise InvalidCommandError.new( @parsed[:command] ) unless commands.include?( @parsed[:command] ) end rescue OptionParser::ParseError => e handle_exception( e, :global ) end # parse local options begin commands[@parsed[:command]].options.permute!( args ) unless commands[@parsed[:command]].options.nil? rescue OptionParser::ParseError => e handle_exception( e, :local ) end @parsed[:args] = args execute if execCommand end # Executes the command. The method +CommandParser#parse!+ has to be called before this one! def execute begin commands[@parsed[:command]].execute( self, @parsed[:args] ) if @parsed[:command] rescue OptionParser::ParseError => e handle_exception( e, :local ) end end private def handle_exception( exception, context ) raise unless @handleExceptions s = (context == :global ? "global" : "command specific") puts "Error parsing #{s} options:\n " + exception.message puts commands['help'].execute( self, (context == :global ? [] : [@parsed[:command]]) ) if commands['help'] exit end end