lib/thor.rb



$:.unshift File.expand_path(File.dirname(__FILE__))
require 'thor/base'
require 'thor/group'
require 'thor/actions'

class Thor
  class << self
    # Sets the default task when thor is executed without an explicit task to be called.
    #
    # ==== Parameters
    # meth<Symbol>:: name of the defaut task
    #
    def default_task(meth=nil)
      case meth
        when :none
          @default_task = 'help'
        when nil
          @default_task ||= from_superclass(:default_task, 'help')
        else
          @default_task = meth.to_s
      end
    end

    # Defines the usage and the description of the next task.
    #
    # ==== Parameters
    # usage<String>
    # description<String>
    #
    def desc(usage, description, options={})
      if options[:for]
        task = find_and_refresh_task(options[:for])
        task.usage = usage             if usage
        task.description = description if description
      else
        @usage, @desc = usage, description
      end
    end

    # Maps an input to a task. If you define:
    #
    #   map "-T" => "list"
    #
    # Running:
    #
    #   thor -T
    #
    # Will invoke the list task.
    #
    # ==== Parameters
    # Hash[String|Array => Symbol]:: Maps the string or the strings in the array to the given task.
    #
    def map(mappings=nil)
      @map ||= from_superclass(:map, {})

      if mappings
        mappings.each do |key, value|
          if key.respond_to?(:each)
            key.each {|subkey| @map[subkey] = value}
          else
            @map[key] = value
          end
        end
      end

      @map
    end

    # Declares the options for the next task to be declared.
    #
    # ==== Parameters
    # Hash[Symbol => Object]:: The hash key is the name of the option and the value
    # is the type of the option. Can be :string, :array, :hash, :boolean, :numeric
    # or :required (string). If you give a value, the type of the value is used.
    #
    def method_options(options=nil)
      @method_options ||= {}
      build_options(options, @method_options) if options
      @method_options
    end

    # Adds an option to the set of class options. If :for is given as option,
    # it allows you to change the options from a previous defined task.
    #
    #   def previous_task
    #     # magic
    #   end
    #
    #   method_options :foo => :bar, :for => :previous_task
    #
    #   def next_task
    #     # magic
    #   end
    #
    # ==== Parameters
    # name<Symbol>:: The name of the argument.
    # options<Hash>:: Described below.
    #
    # ==== Options
    # :desc     - Description for the argument.
    # :required - If the argument is required or not.
    # :default  - Default value for this argument. It cannot be required and have default values.
    # :aliases  - Aliases for this option.
    # :type     - The type of the argument, can be :string, :hash, :array, :numeric or :boolean.
    # :group    - The group for this options. Use by class options to output options in different levels.
    # :banner   - String to show on usage notes.
    #
    def method_option(name, options={})
      scope = if options[:for]
        find_and_refresh_task(options[:for]).options
      else
        method_options
      end

      build_option(name, options, scope)
    end

    # Parses the task and options from the given args, instantiate the class
    # and invoke the task. This method is used when the arguments must be parsed
    # from an array. If you are inside Ruby and want to use a Thor class, you
    # can simply initialize it:
    #
    #   script = MyScript.new(args, options, config)
    #   script.invoke(:task, first_arg, second_arg, third_arg)
    #
    def start(given_args=ARGV, config={})
      super do
        meth = normalize_task_name(given_args.shift)
        task = all_tasks[meth]

        if task
          args, opts = Thor::Options.split(given_args)
          config.merge!(:task_options => task.options)
        else
          args, opts = given_args, {}
        end

        task ||= Thor::Task::Dynamic.new(meth)
        trailing = args[Range.new(arguments.size, -1)]
        new(args, opts, config).invoke(task, trailing || [])
      end
    end

    # Prints help information. If a task name is given, it shows information
    # only about the specific task.
    #
    # ==== Parameters
    # meth<String>:: An optional task name to print usage information about.
    #
    # ==== Options
    # namespace:: When true, shows the namespace in the output before the usage.
    # skip_inherited:: When true, does not show tasks from superclass.
    #
    def help(shell, meth=nil, options={})
      meth, options = nil, meth if meth.is_a?(Hash)

      if meth
        task = all_tasks[meth]
        raise UndefinedTaskError, "task '#{meth}' could not be found in namespace '#{self.namespace}'" unless task

        shell.say "Usage:"
        shell.say "  #{banner(task, options[:namespace], false)}"
        shell.say
        class_options_help(shell, "Class", :Method => task.options.map { |_, o| o })
        shell.say task.description
      else
        list = (options[:short] ? tasks : all_tasks).map do |_, task|
          item = [ banner(task, options[:namespace]) ]
          item << "# #{task.short_description}" if task.short_description
          item << " "
        end

        options[:ident] ||= 2
        if options[:short]
          shell.print_list(list, :ident => options[:ident])
        else
          shell.say "Tasks:"
          shell.print_list(list, :ident => options[:ident])
        end

        Thor::Util.thor_classes_in(self).each do |subclass|
          namespace = options[:namespace] == true || subclass.namespace.gsub(/^#{self.namespace}:/, '')
          subclass.help(shell, options.merge(:short => true, :namespace => namespace))
        end

        class_options_help(shell, "Class") unless options[:short]
      end
    end

    protected

      # The banner for this class. You can customize it if you are invoking the
      # thor class by another ways which is not the Thor::Runner. It receives
      # the task that is going to be invoked and a boolean which indicates if
      # the namespace should be displayed as arguments.
      #
      def banner(task, namespace=true, show_options=true)
        task.formatted_usage(self, namespace, show_options)
      end

      def baseclass #:nodoc:
        Thor
      end

      def create_task(meth) #:nodoc:
        if @usage && @desc
          tasks[meth.to_s] = Thor::Task.new(meth, @desc, @usage, method_options)
          @usage, @desc, @method_options = nil
          true
        elsif self.all_tasks[meth.to_s] || meth.to_sym == :method_missing
          true
        else
          puts "[WARNING] Attempted to create task #{meth.inspect} without usage or description. " <<
               "Call desc if you want this method to be available as task or declare it inside a " <<
               "no_tasks{} block. Invoked from #{caller[1].inspect}."
          false
        end
      end

      def initialize_added #:nodoc:
        class_options.merge!(method_options)
        @method_options = nil
      end

      # Receives a task name (can be nil), and try to get a map from it.
      # If a map can't be found use the sent name or the default task.
      #
      def normalize_task_name(meth) #:nodoc:
        mapping = map[meth.to_s]
        meth = mapping || meth || default_task
        meth.to_s.gsub('-','_') # treat foo-bar > foo_bar
      end
  end

  include Thor::Base

  map HELP_MAPPINGS => :help

  desc "help [TASK]", "Describe available tasks or one specific task"
  def help(task=nil)
    self.class.help(shell, task, :namespace => task && task.include?(?:))
  end
end