lib/cmdparse.rb in cmdparse-2.0.6 vs lib/cmdparse.rb in cmdparse-3.0.0

- old
+ new

@@ -1,45 +1,45 @@ # #-- # cmdparse: advanced command line parser supporting commands -# Copyright (C) 2004-2014 Thomas Leitner +# Copyright (C) 2004-2015 Thomas Leitner # -# This file is part of cmdparse. -# -# cmdparse is free software: you can redistribute it and/or modify it under the terms of the GNU -# Lesser General Public License as published by the Free Software Foundation, either version 3 of -# the License, or (at your option) any later version. -# -# cmdparse 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 Lesser -# General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License along with cmdparse. If -# not, see <http://www.gnu.org/licenses/>. -# +# This file is part of cmdparse which is licensed under the MIT. #++ # -# Namespace module for cmdparse. +require 'optparse' + +OptionParser::Officious.delete('version') +OptionParser::Officious.delete('help') + +# Namespace module for cmdparse. +# +# See CmdParse::CommandParser and CmdParse::Command for the two important classes. module CmdParse # The version of this cmdparse implemention - VERSION = [2, 0, 6] + VERSION = '3.0.0' # Base class for all cmdparse errors. - class ParseError < RuntimeError + class ParseError < StandardError - # Set the reason for a subclass. - def self.reason(reason, has_arguments = true) - (@@reason ||= {})[self] = [reason, has_arguments] + # Sets the error reason for the subclass. + def self.reason(reason) + @reason = reason end - # Return the reason plus the message. + # Returns the error reason or 'CmdParse error' if it has not been set. + def self.get_reason + @reason ||= 'CmdParse error' + end + + # Returns the reason plus the original message. def message - data = @@reason[self.class] || ['Unknown error', true] - data[0] + (data[1] ? ": " + super : '') + str = super + self.class.get_reason + (str.empty? ? "" : ": #{str}") end end # This error is thrown when an invalid command is encountered. @@ -57,421 +57,812 @@ reason 'Invalid option' end # This error is thrown when no command was given and no default command was specified. class NoCommandGivenError < ParseError - reason 'No command given', false + reason 'No command given' + + def initialize #:nodoc: + super('') + end end # This error is thrown when a command is added to another command which does not support commands. class TakesNoCommandError < ParseError - reason 'This command takes no other commands', false + reason 'This command takes no other commands' end + # This error is thrown when not enough arguments are provided for the command. + class NotEnoughArgumentsError < ParseError + reason 'Not enough arguments provided, minimum is' + end - # Base class for all parser wrappers. - class ParserWrapper + # Command Hash - will return partial key matches as well if there is a single non-ambigous + # matching key + class CommandHash < Hash #:nodoc: - # Return the parser instance for the object and, if a block is a given, yield the instance. - def instance - yield @instance if block_given? - @instance + def key?(name) #:nodoc: + !self[name].nil? end - # Parse the arguments in order, i.e. stops at the first non-option argument, and returns all - # remaining arguments. - def order(args) - raise InvalidOptionError.new(args[0]) if args[0] =~ /^-/ - args + def [](cmd_name) #:nodoc: + super or begin + possible = keys.select {|key| key[0, cmd_name.length] == cmd_name } + fetch(possible[0]) if possible.size == 1 + end end - # Permute the arguments so that all options anywhere on the command line are parsed and the - # remaining non-options are returned. - def permute(args) - raise InvalidOptionError.new(args[0]) if args.any? {|a| a =~ /^-/} - args + end + + # Container for multiple OptionParser::List objects. + # + # This is needed for providing what's equivalent to stacked OptionParser instances and the global + # options implementation. + class MultiList #:nodoc: + + def initialize(list) #:nodoc: + @list = list end - # Return a summary string of the options. - def summarize - "" + def summarize(*args, &block) #:nodoc: + # We don't want summary information of the global options to automatically appear. end + [:accept, :reject, :prepend, :append].each do |mname| + module_eval <<-EOF + def #{mname}(*args, &block) + @list[-1].#{mname}(*args, &block) + end + EOF + end + + [:search, :complete, :each_option, :add_banner, :compsys].each do |mname| + module_eval <<-EOF + def #{mname}(*args, &block) #:nodoc: + @list.reverse_each {|list| list.#{mname}(*args, &block)} + end + EOF + end + end - # Require default option parser wrapper - require 'cmdparse/wrappers/optparse' + # Extension for OptionParser objects to allow access to some internals. + class ::OptionParser #:nodoc: - # Command Hash - will return partial key matches as well if there is a single - # non-ambigous matching key - class CommandHash < Hash + # Access the option list stack. + attr_reader :stack - def [](cmd_name) - super or begin - possible = keys.select {|key| key[0, cmd_name.length] == cmd_name } - fetch(possible[0]) if possible.size == 1 + # Returns +true+ if at least one local option is defined. + # + # The zeroth stack element is not respected when doing the query because it contains either the + # OptionParser::DefaultList or a CmdParse::MultiList with the global options of the + # CmdParse::CommandParser. + def options_defined? + stack[1..-1].each do |list| + list.each_option do |switch| + return true if ::OptionParser::Switch === switch && (switch.short || switch.long) + end end + false end + # Returns +true+ if a banner has been set. + def banner? + !@banner.nil? + end + end - # Base class for the commands. This class implements all needed methods so that it can be used by - # the +CommandParser+ class. + # === Base class for commands + # + # This class implements all needed methods so that it can be used by the CommandParser class. + # + # Commands can either be created by sub-classing or on the fly when using the #add_command method. + # The latter allows for a more terse specification of a command while the sub-class approach + # allows to customize all aspects of a command by overriding methods. + # + # Basic example for sub-classing: + # + # class TestCommand < CmdParse::Command + # def initialize + # super('test', takes_commands: false) + # options.on('-m', '--my-opt', 'My option') { 'Do something' } + # end + # end + # + # parser = CmdParse::CommandParser.new + # parser.add_command(TestCommand.new) + # parser.parse + # + # Basic example for on the fly creation: + # + # parser = CmdParse::CommandParser.new + # parser.add_command('test') do |cmd| + # takes_commands(false) + # options.on('-m', '--my-opt', 'My option') { 'Do something' } + # end + # parser.parse + # + # === Basic Properties + # + # The only thing that is mandatory to set for a Command is its #name. If the command does not take + # any sub-commands, then additionally an #action block needs to be specified or the #execute + # method overridden. + # + # However, there are several other methods that can be used to configure the behavior of a + # command: + # + # #takes_commands:: For specifying whether sub-commands are allowed. + # #options:: For specifying command specific options. + # #add_command:: For specifying sub-commands if the command takes them. + # + # === Help Related Methods + # + # Many of this class' methods are related to providing useful help output. While the most common + # methods can directly be invoked to set or retrieve information, many other methods compute the + # needed information dynamically and therefore need to be overridden to customize their return + # value. + # + # #short_desc:: + # For a short description of the command (getter/setter). + # #long_desc:: + # For a detailed description of the command (getter/setter). + # #argument_desc:: + # For describing command arguments (setter). + # #help, #help_banner, #help_short_desc, #help_long_desc, #help_commands, #help_arguments, #help_options:: + # For outputting the general command help or individual sections of the command help (getter). + # #usage, #usage_options, #usage_arguments, #usage_commands:: + # For outputting the usage line or individual parts of it (getter). + # + # === Built-in Commands + # + # cmdparse ships with two built-in commands: + # * HelpCommand (for showing help messages) and + # * VersionCommand (for showing version information). class Command - # The name of the command + # The name of the command. attr_reader :name - # A short description of the command. Should ideally be smaller than 60 characters. - attr_accessor :short_desc - - # A detailed description of the command. Maybe a single string or an array of strings for - # multiline description. Each string should ideally be smaller than 76 characters. - attr_accessor :description - - # The wrapper for parsing the command line options. - attr_accessor :options - - # Returns the name of the default command. + # Returns the name of the default sub-command or +nil+ if there isn't any. attr_reader :default_command - # Sets or returns the super command of this command. The super command is either a +Command+ - # instance for normal commands or a +CommandParser+ instance for the root command. + # Sets or returns the super-command of this command. The super-command is either a Command + # instance for normal commands or a CommandParser instance for the main command (ie. + # CommandParser#main_command). attr_accessor :super_command - # Returns the list of commands for this command. + # Returns the mapping of command name to command for all sub-commands of this command. attr_reader :commands - # Initialize the command called +name+. + # A data store (initially an empty Hash) that can be used for storing anything. For example, it + # can be used to store option values. cmdparse itself doesn't do anything with it. + attr_accessor :data + + # Initializes the command called +name+. # - # Parameters: + # Options: # - # [has_commands] - # Specifies if this command takes other commands as argument. - # [partial_commands (optional)] - # Specifies whether partial command matching should be used. - # [has_args (optional)] - # Specifies whether this command takes arguments - def initialize(name, has_commands, partial_commands = false, has_args = true) - @name = name - @options = ParserWrapper.new - @has_commands = has_commands - @has_args = has_args - @commands = Hash.new + # takes_commands:: Specifies whether this command can take sub-commands. + def initialize(name, takes_commands: true) + @name = name.freeze + @options = OptionParser.new + @commands = CommandHash.new @default_command = nil - use_partial_commands(partial_commands) + @action = nil + @argument_desc ||= {} + @data = {} + takes_commands(takes_commands) end - # Define whether partial command matching should be used. - def use_partial_commands(use_partial) - temp = (use_partial ? CommandHash.new : Hash.new) - temp.update(@commands) - @commands = temp + # Sets whether this command can take sub-command. + # + # The argument +val+ needs to be +true+ or +false+. + def takes_commands(val) + if !val && commands.size > 0 + raise Error, "Can't change value of takes_commands to false because there are already sub-commands" + else + @takes_commands = val + end end + alias takes_commands= takes_commands - # Return +true+ if this command supports sub commands. - def has_commands? - @has_commands + # Return +true+ if this command can take sub-commands. + def takes_commands? + @takes_commands end - # Return +true+ if this command uses arguments. - def has_args? - @has_args + # :call-seq: + # command.options {|opts| ...} -> opts + # command.options -> opts + # + # Yields the OptionParser instance that is used for parsing the options of this command (if a + # block is given) and returns it. + def options #:yields: options + yield(@options) if block_given? + @options end - # Add a command to the command list if this command takes other commands as argument. + # :call-seq: + # command.add_command(other_command, default: false) {|cmd| ... } -> command + # command.add_command('other', default: false) {|cmd| ...} -> command # - # 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) - raise TakesNoCommandError.new(@name) if !has_commands? + # Adds a command to the command list. + # + # The argument +command+ can either be a Command object or a String in which case a new Command + # object is created. In both cases the Command object is yielded. + # + # If the optional argument +default+ is +true+, then the command is used when no other + # sub-command is specified on the command line. + # + # If this command takes no other commands, an error is raised. + def add_command(command, default: false) # :yields: command_object + raise TakesNoCommandError.new(name) unless takes_commands? + + command = Command.new(command) if command.kind_of?(String) + command.super_command = self @commands[command.name] = command @default_command = command.name if default - command.super_command = self - command.init - end + command.fire_hook_after_add + yield(command) if block_given? - # For sorting commands by name. - def <=>(other) - @name <=> other.name + self end - # Return the +CommandParser+ instance for this command or +nil+ if this command was not assigned - # to a +CommandParser+ instance. - def commandparser - cmd = super_command - cmd = cmd.super_command while !cmd.nil? && !cmd.kind_of?(CommandParser) - cmd - end - - # Return a list of super commands, ie.: - # [command, super_command, super_super_command, ...] - def super_commands + # :call-seq: + # command.command_chain -> [top_level_command, super_command, ..., command] + # + # Returns the command chain, i.e. a list containing this command and all of its super-commands, + # starting at the top level command. + def command_chain cmds = [] cmd = self while !cmd.nil? && !cmd.super_command.kind_of?(CommandParser) - cmds << cmd + cmds.unshift(cmd) cmd = cmd.super_command end cmds end - # This method is called when the command is added to a +Command+ instance. - def init; end + # Returns the associated CommandParser instance for this command or +nil+ if no command parser + # is associated. + def command_parser + cmd = super_command + cmd = cmd.super_command while !cmd.nil? && !cmd.kind_of?(CommandParser) + cmd + end - # Set the given +block+ as execution block. See also: +execute+. - def set_execution_block(&block) - @exec_block = block + # Sets the given +block+ as the action block that is used on when executing this command. + # + # If a sub-class is created for specifying a command, then the #execute method should be + # overridden instead of setting an action block. + # + # See also: #execute + def action(&block) + @action = block end - # Invoke the block set by +set_execution_block+. + # Invokes the action block with the parsed arguments. # - # This method is called by the +CommandParser+ instance if this command was specified on the - # command line. - def execute(args) - @exec_block.call(args) + # This method is called by the CommandParser instance if this command was specified on the + # command line to be executed. + # + # Sub-classes can either specify an action block or directly override this method (the latter is + # preferred). + def execute(*args) + @action.call(*args) end - # Define the usage line for the command. + # Sets the short description of the command if an argument is given. Always returns the short + # description. + # + # The short description is ideally shorter than 60 characters. + def short_desc(*val) + @short_desc = val[0] unless val.empty? + @short_desc + end + alias short_desc= short_desc + + # Sets the detailed description of the command if an argument is given. Always returns the + # detailed description. + # + # This may be a single string or an array of strings for multiline description. Each string + # is ideally shorter than 76 characters. + def long_desc(*val) + @long_desc = val.flatten unless val.empty? + @long_desc + end + alias long_desc= long_desc + + # :call-seq: + # cmd.argument_desc(name => desc, ...) + # + # Sets the descriptions for one or more arguments using name-description pairs. + # + # The used names should correspond to the names used in #usage_arguments. + def argument_desc(hash) + @argument_desc.update(hash) + end + + # Returns the number of arguments required for the execution of the command, i.e. the number of + # arguments the #action block or the #execute method takes. + # + # If the returned number is negative, it means that the minimum number of arguments is -n-1. + # + # See: Method#arity, Proc#arity + def arity + (@action || method(:execute)).arity + end + + # Returns +true+ if the command can take one or more arguments. + def takes_arguments? + arity.abs > 0 + end + + # Returns a string containing the help message for the command. + def help + output = '' + output << help_banner + output << help_short_desc + output << help_long_desc + output << help_commands + output << help_arguments + output << help_options('Options (take precedence over global options)', options) + output << help_options('Global Options', command_parser.global_options) + end + + # Returns the banner (including the usage line) of the command. + # + # The usage line is command specific but the rest is the same for all commands and can be set + # via +command_parser.main_options.banner+. + def help_banner + output = '' + if command_parser.main_options.banner? + output << format(command_parser.main_options.banner, indent: 0) << "\n\n" + end + output << format(usage, indent: 7) << "\n\n" + end + + # Returns the usage line for the command. + # + # The usage line is automatically generated from the available information. If this is not + # suitable, override this method to provide a command specific usage line. + # + # Typical usage lines looks like the following: + # + # Usage: program [options] command [options] {sub_command1 | sub_command2} + # Usage: program [options] command [options] ARG1 [ARG2] [REST...] + # + # See: #usage_options, #usage_arguments, #usage_commands def usage - tmp = "Usage: #{commandparser.program_name}" - tmp << " [global options]" if !commandparser.options.instance_of?(ParserWrapper) - tmp << super_commands.reverse.collect do |c| - t = " #{c.name}" - t << " [options]" if !c.options.instance_of?(ParserWrapper) - t - end.join('') - tmp << " COMMAND [options]" if has_commands? - tmp << " [ARGS]" if has_args? + tmp = "Usage: #{command_parser.main_options.program_name}" + tmp << command_parser.main_command.usage_options + tmp << command_chain.map {|cmd| " #{cmd.name}#{cmd.usage_options}"}.join('') + if takes_commands? + tmp << " #{usage_commands}" + elsif takes_arguments? + tmp << " #{usage_arguments}" + end tmp end - # Default method for showing the help for the command. - def show_help - puts commandparser.banner + "\n" if commandparser.banner - puts usage - puts - if short_desc && !short_desc.empty? - puts short_desc - puts + # Returns a string describing the options of the command for use in the usage line. + # + # If there are any options, the resulting string also includes a leading space! + # + # A typical return value would look like the following: + # + # [options] + # + # See: #usage + def usage_options + (options.options_defined? ? ' [options]' : '') + end + + # Returns a string describing the arguments for the command for use in the usage line. + # + # By default the names of the action block or #execute method arguments are used (done via + # Ruby's reflection API). If this is not wanted, override this method. + # + # A typical return value would look like the following: + # + # ARG1 [ARG2] [REST...] + # + # See: #usage, #argument_desc + def usage_arguments + (@action || method(:execute)).parameters.map do |type, name| + case type + when :req then name.to_s + when :opt then "[#{name}]" + when :rest then "[#{name}...]" + end + end.join(" ").upcase + end + + # Returns a string describing the sub-commands of the commands for use in the usage line. + # + # Override this method for providing a command specific specialization. + # + # A typical return value would look like the following: + # + # {command | other_command | another_command } + def usage_commands + (commands.size > 0 ? "{#{commands.keys.join(" | ")}}" : '') + end + + # Returns the formatted short description. + # + # For the output format see #cond_format_help_section + def help_short_desc + cond_format_help_section("Summary", "#{name} - #{short_desc}", + condition: short_desc && !short_desc.empty?) + end + + # Returns the formatted detailed description. + # + # For the output format see #cond_format_help_section + def help_long_desc + cond_format_help_section("Description", [long_desc].flatten, + condition: long_desc && !long_desc.empty?) + end + + # Returns the formatted sub-commands of this command. + # + # For the output format see #cond_format_help_section + def help_commands + describe_commands = lambda do |command, level = 0| + command.commands.sort.collect do |name, cmd| + str = " "*level << name << (name == command.default_command ? " (*)" : '') + str = str.ljust(command_parser.help_desc_indent) << cmd.short_desc.to_s + str = format(str, width: command_parser.help_line_width - command_parser.help_indent, + indent: command_parser.help_desc_indent) + str << "\n" << (cmd.takes_commands? ? describe_commands.call(cmd, level + 1) : "") + end.join('') end - if description && !description.empty? - puts " " + [description].flatten.join("\n ") - puts + cond_format_help_section("Available commands", describe_commands.call(self), + condition: takes_commands?) + end + + # Returns the formatted arguments of this command. + # + # For the output format see #cond_format_help_section + def help_arguments + desc = @argument_desc.map {|k, v| k.to_s.ljust(command_parser.help_desc_indent) << v.to_s} + cond_format_help_section('Arguments', desc, condition: @argument_desc.size > 0) + end + + # Returns the formatted option descriptions for the given OptionParser instance. + # + # The section title needs to be specified with the +title+ argument. + # + # For the output format see #cond_format_help_section + def help_options(title, options) + summary = '' + summary_width = command_parser.main_options.summary_width + options.summarize([], summary_width, summary_width - 1, '') do |line| + summary << format(line, width: command_parser.help_line_width - command_parser.help_indent, + indent: summary_width + 1, indent_first_line: false) << "\n" end - if has_commands? - list_commands - puts + cond_format_help_section(title, summary, condition: !summary.empty?) + end + + # This hook method is called when the command (or one of its super-commands) is added to another + # Command instance that has an associated command parser (#see command_parser). + # + # It can be used, for example, to add global options. + def on_after_add + end + + # For sorting commands by name. + def <=>(other) + self.name <=> other.name + end + + protected + + # Conditionally formats a help section. + # + # Returns either the formatted help section if the condition is +true+ or an empty string + # otherwise. + # + # The help section starts with a title and the given lines are indented to easily distinguish + # different sections. + # + # A typical help section would look like the following: + # + # Summary: + # help - Provide help for individual commands + def cond_format_help_section(title, *lines, condition: true, indent: true) + if condition + "#{title}:\n" << format(lines.flatten.join("\n"), + indent: (indent ? command_parser.help_indent : 0), + indent_first_line: true) << "\n\n" + else + '' end - if !(summary = options.summarize).empty? - puts summary - puts - end - if self != commandparser.main_command && - !(summary = commandparser.main_command.options.summarize).empty? - puts summary - puts - end end - ####### - private - ####### + # Returns the text in +content+ formatted so that no line is longer than +width+ characters. + # + # Options: + # + # width:: The maximum width of a line. If not specified, the CommandParser#help_line_width value + # is used. + # + # indent:: This option specifies the amount of spaces prepended to each line. If not specified, + # the CommandParser#help_indent value is used. + # + # indent_first_line:: If this option is +true+, then the first line is also indented. + def format(content, width: command_parser.help_line_width, + indent: command_parser.help_indent, indent_first_line: false) + content = (content || '').dup + line_length = width - indent + first_line_pattern = other_lines_pattern = /\A.{1,#{line_length}}\z|\A.{1,#{line_length}}[ \n]/ + (first_line_pattern = /\A.{1,#{width}}\z|\A.{1,#{width}}[ \n]/) unless indent_first_line + pattern = first_line_pattern - def list_commands(command = self) - puts "Available commands:" - puts " " + collect_commands_info(command).join("\n ") + content.split(/\n\n/).map do |paragraph| + lines = [] + while paragraph.length > 0 + unless (str = paragraph.slice!(pattern).sub(/[ \n]\z/, '')) + str = paragraph.slice!(0, line_length) + end + lines << (lines.empty? && !indent_first_line ? '' : ' '*indent) + str.gsub(/\n/, ' ') + pattern = other_lines_pattern + end + lines.join("\n") + end.join("\n\n") end - def collect_commands_info(command, level = 1) - command.commands.sort.collect do |name, cmd| - str = " "*level + name - str = str.ljust(18) + cmd.short_desc.to_s - str += " (default command)" if name == command.default_command - [str] + (cmd.has_commands? ? collect_commands_info(cmd, level + 1) : []) - end.flatten + def fire_hook_after_add #:nodoc: + return unless command_parser + @options.stack[0] = MultiList.new(command_parser.global_options.stack) + on_after_add + @commands.each_value {|cmd| cmd.fire_hook_after_add} end end - # The default help command. It adds the options "-h" and "--help" to the global options of the - # associated +CommandParser+. When the command is specified on the command line, it can show the - # main help or individual command help. + # The default help Command. + # + # It adds the options "-h" and "--help" to the CommandParser#global_options. + # + # When the command is specified on the command line (or one of the above mentioned options), it + # shows the main help or individual command help. class HelpCommand < Command - def initialize - super('help', false) - self.short_desc = 'Provide help for individual commands' - self.description = ['This command prints the program help if no arguments are given. If one or', - 'more command names are given as arguments, these arguments are interpreted', - 'as a hierachy of commands and the help for the right most command is show.'] + def initialize #:nodoc: + super('help', takes_commands: false) + short_desc('Provide help for individual commands') + long_desc('This command prints the program help if no arguments are given. If one or ' << + 'more command names are given as arguments, these arguments are interpreted ' << + 'as a hierachy of commands and the help for the right most command is show.') + argument_desc(COMMAND: 'The name of a command or sub-command') end - def init - case commandparser.main_command.options - when OptionParserWrapper - commandparser.main_command.options.instance do |opt| - opt.on_tail("-h", "--help", "Show help") do - execute([]) - end - end + def on_after_add #:nodoc: + command_parser.global_options.on_tail("-h", "--help", "Show help") do + execute(*command_parser.current_command.command_chain.map(&:name)) + exit end end - def usage - "Usage: #{commandparser.program_name} help [COMMAND SUBCOMMAND ...]" + def usage_arguments #:nodoc: + "[COMMAND COMMAND...]" end - def execute(args) + def execute(*args) #:nodoc: if args.length > 0 - cmd = commandparser.main_command + cmd = command_parser.main_command arg = args.shift - while !arg.nil? && cmd.commands[arg] + while !arg.nil? && cmd.commands.key?(arg) cmd = cmd.commands[arg] arg = args.shift end if arg.nil? - cmd.show_help + puts cmd.help else raise InvalidArgumentError, args.unshift(arg).join(' ') end else - commandparser.main_command.show_help + puts command_parser.main_command.help end - exit end end - # The default version command. It adds the options "-v" and "--version" to the global options of - # the associated +CommandParser+. When specified on the command line, it shows the version of the - # program. + # The default version command. + # + # It adds the options "-v" and "--version" to the CommandParser#global_options. + # + # When the command is specified on the command line (or one of the above mentioned options), it + # shows the version of the program configured by the settings + # + # * command_parser.main_options.program_name + # * command_parser.main_options.version class VersionCommand < Command - def initialize - super('version', false, false, false) - self.short_desc = "Show the version of the program" + def initialize #:nodoc: + super('version', takes_commands: false) + short_desc("Show the version of the program") end - def init - case commandparser.main_command.options - when OptionParserWrapper - commandparser.main_command.options.instance do |opt| - opt.on_tail("--version", "-v", "Show the version of the program") do - execute([]) - end - end + def on_after_add #:nodoc: + command_parser.main_options.on_tail("--version", "-v", "Show the version of the program") do + execute end end - def usage - "Usage: #{commandparser.program_name} version" - end - - def execute(args) - version = commandparser.program_version - version = version.join('.') if version.instance_of?(Array) - puts commandparser.banner + "\n" if commandparser.banner - puts "#{commandparser.program_name} #{version}" + def execute #:nodoc: + version = command_parser.main_options.version + version = version.join('.') if version.kind_of?(Array) + puts command_parser.main_options.banner + "\n" if command_parser.main_options.banner? + puts "#{command_parser.main_options.program_name} #{version}" exit end end - # The main class for creating a command based CLI program. + # === Main Class for Creating a Command Based CLI Program + # + # This class can directly be used (or sub-classed, if need be) to create a command based CLI + # program. + # + # The CLI program itself is represented by the #main_command, a Command instance (as are all + # commands and sub-commands). This main command can either hold sub-commands (the normal use case) + # which represent the programs top level commands or take no commands in which case it acts + # similar to a simple OptionParser based program (albeit with better help functionality). + # + # Parsing the command line for commands is done by this class, option parsing is delegated to the + # battle tested OptionParser of the Ruby standard library. + # + # === Usage + # + # After initialization some optional information is expected to be set on the Command#options of + # the #main_command: + # + # banner:: A banner that appears in the help output before anything else. + # program_name:: The name of the program. If not set, this value is computed from $0. + # version:: The version string of the program. + # + # In addition to the main command's options instance (which represents the top level options that + # need to be specified before any command name), there is also a #global_options instance which + # represents options that can be specified anywhere on the command line. + # + # Top level commands can be added to the main command by using the #add_command method. + # + # Once everything is set up, the #parse method is used for parsing the command line. class CommandParser - # A standard banner for help & version screens - attr_accessor :banner - # The top level command representing the program itself. attr_reader :main_command - # The name of the program. - attr_accessor :program_name + # The command that is being executed. Only available during parsing of the command line + # arguments. + attr_reader :current_command - # The version of the program. - attr_accessor :program_version + # A data store (initially an empty Hash) that can be used for storing anything. For example, it + # can be used to store global option values. cmdparse itself doesn't do anything with it. + attr_accessor :data # Should exceptions be handled gracefully? I.e. by printing error message and the help screen? + # + # See ::new for possible values. attr_reader :handle_exceptions - # Create a new CommandParser object. + # The maximum width of the help lines. + attr_accessor :help_line_width + + # The amount of spaces to indent the content of help sections. + attr_accessor :help_indent + + # The indentation used for, among other things, command descriptions. + attr_accessor :help_desc_indent + + # Creates a new CommandParser object. # - # [handleExceptions (optional)] - # Specifies if the object should handle exceptions gracefully. - # [partial_commands (optional)] - # Specifies if you want partial command matching for the top level commands. - # [has_args (optional)] - # Specifies whether the command parser takes arguments (only used when no sub commands are - # defined). - def initialize(handleExceptions = false, partial_commands = false, has_args = true) - @main_command = Command.new('mainCommand', true, partial_commands, has_args) + # Options: + # + # handle_exceptions:: Set to +true+ if exceptions should be handled gracefully by showing the + # error and a help message, or to +false+ if exception should not be handled + # at all. If this options is set to :no_help, the exception is handled but no + # help message is shown. + # + # takes_commands:: Specifies whether the main program takes any commands. + def initialize(handle_exceptions: false, takes_commands: true) + @global_options = OptionParser.new + @main_command = Command.new('main', takes_commands: takes_commands) @main_command.super_command = self - @program_name = $0 - @program_version = "0.0.0" - @handle_exceptions = handleExceptions + @main_command.options.stack[0] = MultiList.new(@global_options.stack) + @handle_exceptions = handle_exceptions + @help_line_width = 80 + @help_indent = 4 + @help_desc_indent = 18 + @data = {} end - # Return the wrapper for parsing the global options. - def options + # :call-seq: + # cmdparse.main_options -> OptionParser instance + # cmdparse.main_options {|opts| ...} -> opts (OptionParser instance) + # + # Yields the main options (that are only available directly after the program name) if a block + # is given and returns them. + # + # The main options are also used for setting the program name, version and banner. + def main_options + yield(@main_command.options) if block_given? @main_command.options end - # Set the wrapper for parsing the global options. - def options=(wrapper) - @main_command.options = wrapper + # :call-seq: + # cmdparse.global_options -> OptionParser instance + # cmdparse.gloabl_options {|opts| ...} -> opts (OptionParser instance) + # + # Yields the global options if a block is given and returns them. + # + # The global options are those options that can be used on the top level and with any + # command. + def global_options + yield(@global_options) if block_given? + @global_options end - # Add a top level command. - def add_command(*args) - @main_command.add_command(*args) + # Adds a top level command. + # + # See Command#add_command for detailed invocation information. + def add_command(*args, &block) + @main_command.add_command(*args, &block) end - # Parse the command line arguments. + # Parses the command line arguments. # - # If a block is specified, the current hierarchy level and the name of the current command is + # If a block is given, the current hierarchy level and the name of the current command is # yielded after the option parsing is done but before a command is executed. - def parse(argv = ARGV) # :yields: level, commandName + def parse(argv = ARGV) # :yields: level, command_name level = 0 - command = @main_command + @current_command = @main_command - while !command.nil? - argv = if command.has_commands? || ENV.include?('POSIXLY_CORRECT') - command.options.order(argv) + while true + argv = if @current_command.takes_commands? || ENV.include?('POSIXLY_CORRECT') + @current_command.options.order(argv) else - command.options.permute(argv) + @current_command.options.permute(argv) end - yield(level, command.name) if block_given? + yield(level, @current_command.name) if block_given? - if command.has_commands? - cmdName, argv = argv[0], argv[1..-1] || [] + if @current_command.takes_commands? + cmd_name = argv.shift || @current_command.default_command - if cmdName.nil? - if command.default_command.nil? - raise NoCommandGivenError - else - cmdName = command.default_command - end - else - raise InvalidCommandError.new(cmdName) unless command.commands[ cmdName ] + if cmd_name.nil? + raise NoCommandGivenError.new + elsif !@current_command.commands.key?(cmd_name) + raise InvalidCommandError.new(cmd_name) end - command = command.commands[cmdName] + @current_command = @current_command.commands[cmd_name] level += 1 else - command.execute(argv) - command = nil + original_n = @current_command.arity + n = (original_n < 0 ? -original_n - 1 : original_n) + raise NotEnoughArgumentsError.new(n) if argv.size < n + + argv.slice!(n..-1) unless original_n < 0 + @current_command.execute(*argv) + break end end rescue ParseError, OptionParser::ParseError => e - raise if !@handle_exceptions + raise unless @handle_exceptions puts "Error while parsing command line:\n " + e.message - puts - @main_command.commands['help'].execute(command.super_commands.reverse.collect {|c| c.name}) if @main_command.commands['help'] - exit + if @handle_exceptions != :no_help && @main_command.commands.key?('help') + puts + @main_command.commands['help'].execute(*@current_command.command_chain.map(&:name)) + end + exit(64) # FreeBSD standard exit error for "command was used incorrectly" + ensure + @current_command = nil end end end