# commands.rb: implementation of command-driven approach # copyright (c) 2009 by Vincent Fourmond # 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 (in the COPYING file). require 'ctioga2/utils' require 'ctioga2/commands/arguments' require 'ctioga2/commands/groups' module CTioga2 Version::register_svn_info('$Revision: 144 $', '$Date: 2010-04-06 13:31:39 +0200 (Tue, 06 Apr 2010) $') module Commands # An exception raised when the command is called with an insufficient # number of arguments. class ArgumentNumberMismatch < Exception end # An exception raised when an optional argument is encountered # that does not match an entry in Command#optional_arguments class CommandOptionUnkown < Exception end # One of the commands that can be used. # # \todo Write a Shortcut command that would simply be a shortcut # for other things. Possibly taking arguments ? It could take a # description, though that wouldn't be necessary. # # \todo Use this Shortcut to write DeprecatedShortcut for old # ctioga options. class Command # The name of the command, ie how to call it in a commands file attr_accessor :name # Its short command-line option, or _nil_ if none attr_accessor :short_option # Its long command-line option, or _nil_ if it should not be # called from the command-line (but you *really* don't want that). attr_accessor :long_option # The compulsory arguments it can take, in the form of an # array of CommandArgument attr_accessor :arguments # Optional arguments to a command, in the form of a Hash # 'option name' => CommandArgument attr_accessor :optional_arguments # A short one-line description of the command attr_accessor :short_description # A longer description. Typically input using a here-document. attr_accessor :long_description # The code that will be called. It must be a Proc object, or any # objects that answers a #call method. # # The corresponding block will be called with the following arguments: # * first, the PlotMaker instance where the command will be running # * second, as many arguments as there are #arguments. # * third, if #optional_arguments is non-empty, a hash # containing the values of the optional arguments. It will be # an empty hash if no optional arguments are given in the # command). It *will* be empty if the command is called as # an option in the command-line. # # *Few* *rules* *for* *writing* *the* *code*: # * code should avoid as much as possible to rely on closures. # * the CommandArgument framework will make sure the arguments # are given with the appropriate type or raise an # exception. Don't bother. attr_accessor :code # The CommandGroup to which the command belongs attr_accessor :group # The context of definition [file, line] attr_accessor :context # Creates a Command, with all attributes set up. The code can be # set using #set_code. # # Single and double dashes are stripped from the beginning of the # short and long options respectively. def initialize(n, short, long, args = [], opts = {}, d_short = nil, d_long = nil, group = nil, register = true, &code) @name = n @short_option = short && short.gsub(/^-/,'') @long_option = long && long.gsub(/^--/,'') @arguments = args @optional_arguments = opts if(@short_option and ! @long_option) raise "A long option must always be present if a short one is" end @code = code self.describe(d_short, d_long, group) caller[1].gsub(/.*\/ctioga2\//, 'lib/ctioga2/') =~ /(.*):(\d+)/ @context = [$1, $2.to_i] # Registers automatically the command if register Commands::Interpreter.register_command(self) end end # Sets the code to the block given. def set_code(&code) @code = code end # Returns the number of compulsory arguments def argument_number return @arguments.size end # Sets the descriptions of the command. If the long description # is ommitted, the short is reused. def describe(short, long = nil, group = nil) @short_description = short @long_description = long || short if(group) @group = group group.commands << self end end # Returns a list of three strings: # * the short option # * the long option with arguments # * the description string # # Returns _nil_ if the long option is not defined. def option_strings if ! @long_option return nil end retval = [] # Short option retval << ( @short_option ? "-#{@short_option}" : nil) # Long option + arguments if @arguments.size > 0 retval << @arguments.first.type. option_parser_long_option(@long_option, @arguments.first.displayed_name) + if @arguments.size > 1 " " + @arguments[1..-1].map do |t| t.displayed_name.upcase end.join(" ") else "" end else retval << "--#{@long_option}" end retval << @short_description return retval end # Converts the Array of String given into an Array of the type # suitable for the #code of the Command. This deals only with # compulsory arguments. Returns the array. # # Any object which is not a String is left as is (useful for # instance for the OptionParser with boolean options) # # As a special case, if the command takes no arguments and the # arguments is [true], no exception is raised, and the correct # number of arguments is returned. def convert_arguments(args) if args.size != @arguments.size if(@arguments.size == 0 && args.size == 1 && args[0] == true) return [] else raise ArgumentNumberMismatch, "Command #{@name} was called with #{args.size} arguments, but it takes #{@arguments.size}" end end retval = [] @arguments.each_index do |i| if ! args[i].is_a? String retval << args[i] else retval << @arguments[i].type.string_to_type(args[i]) end end return retval end # Converts the Hash of String given into a Hash of the type # suitable for the #code of the Command. Only optional # arguments are taken into account. # # Any object which is not a String is left as is (useful for # instance for the OptionParser with boolean options) def convert_options(options) target_options = {} for k,v in options if ! @optional_arguments.key? k raise CommandOptionUnkown, "Unkown option #{k} for command #{@name}" end if v.is_a? String target_options[k] = @optional_arguments[k].type. string_to_type(v) else target_options[k] = v end end return target_options end # Whether the Command accepts the named _option_. # # \todo Several conversions could be used, to facilitate the # writing of options: # # * convert everything to lowercase . # * ignore the difference between _ and - (a bit delicate). def has_option?(option) return @optional_arguments.key? option end # Whether the Command accepts any option at all ? def has_options? return !(@optional_arguments.empty?) end # Runs the command with the given _plotmaker_target_, the # compulsory arguments and the optional ones. Any mismatch in # the number of things will result in an Exception. # # The arguments will *not* be processed further. def run_command(plotmaker_target, compulsory_args, optional_args = nil) args = [plotmaker_target] if compulsory_args.size != @arguments.size raise ArgumentNumberMismatch, "Command #{@name} was called with #{args.size} arguments, but it takes #{@arguments.size}" end args += compulsory_args if has_options? if optional_args args << optional_args else args << {} end end @code.call(*args) end end end end