小探 Thor


```ruby
    # Defines the usage and the description of the next command.
    #
    # ==== Parameters
    # usage<String>
    # description<String>
    # options<String>
    #
    def desc(usage, description, options = {})
      if options[:for]
        command = find_and_refresh_command(options[:for])
        command.usage = usage             if usage
        command.description = description if description
      else
        @usage, @desc, @hide = usage, description, options[:hide] || false
      end
    end

    # Defines the long description of the next command.
    #
    # ==== Parameters
    # long description<String>
    #
    def long_desc(long_description, options = {})
      if options[:for]
        command = find_and_refresh_command(options[:for])
        command.long_description = long_description if long_description
      else
        @long_desc = long_description
      end
    end
```

这个有点类似 python 中的装饰器的味道. 具体又是怎么实现的呢? 在他的注释中这样写的`for the next command`. 它是在寻找距他最近的方法定义吗?

上面的代码中, 会首判断当前的参数中是否显式指定了这是哪一个命令的描述, 

- case1.

```ruby
      # Finds a command with the given name. If the command belongs to the current
      # class, just return it, otherwise dup it and add the fresh copy to the
      # current command hash.
      def find_and_refresh_command(name) #:nodoc:
        if commands[name.to_s]
          commands[name.to_s]
        elsif command = all_commands[name.to_s] # rubocop:disable AssignmentInCondition
          commands[name.to_s] = command.clone
        else
          fail ArgumentError, "You supplied :for => #{name.inspect}, but the command #{name.inspect} could not be found."
        end
      end
      alias_method :find_and_refresh_task, :find_and_refresh_command

```

如果当前给的命令名能够在当前的类(继续自 Thor)找到的话, 直接返回这个类, 否则就复制这个命令到当前的命令hash 中.

那这个`commands`数组是从何而来?

```ruby

      # Returns the commands for this Thor class.
      #
      # ==== Returns
      # OrderedHash:: An ordered hash with commands names as keys and Thor::Command
      #               objects as values.
      #
      def commands
        @commands ||= Thor::CoreExt::OrderedHash.new
      end
      alias_method :tasks, :commands
      
      # Returns the commands for this Thor class and all subclasses.
      #
      # ==== Returns
      # OrderedHash:: An ordered hash with commands names as keys and Thor::Command
      #               objects as values.
      #
      def all_commands
        @all_commands ||= from_superclass(:all_commands, Thor::CoreExt::OrderedHash.new)
        @all_commands.merge!(commands)
      end
      alias_method :all_tasks, :all_commands
```

总之就是找到当前已经注册的所有的命令.然后把这个值给它的对应的属性.

- case 2:

就是把当前的值给`@long_desc`实例变量.

```ruby
	...
      else
        @long_desc = long_description
      end
	...
```
大部分情况下我们并不会走`case1`,而是就近写在想要描述的方法之上. 我们继续.
在下面的`create_command`方法中, 已经有出现这个实例变量`@long_desc`.

> thor/lib/thor.rb:383

```ruby
    def create_command(meth) #:nodoc:
      @usage ||= nil
      @desc ||= nil
      @long_desc ||= nil
      @disable_class_options ||= nil

      if @usage && @desc
        base_class = @hide ? Thor::HiddenCommand : Thor::Command
        commands[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options, @disable_class_options)
        @usage, @desc, @long_desc, @method_options, @hide, @disable_class_options = nil
        true
      elsif all_commands[meth] || meth == "method_missing"
        true
      else
        puts "[WARNING] Attempted to create command #{meth.inspect} without usage or description. " <<
             "Call desc if you want this method to be available as command or declare it inside a " <<
             "no_commands{} block. Invoked from #{caller[1].inspect}."
        false
      end
    end
    alias_method :create_task, :create_command
```

那其实何时这个方法会变使用呢?

我们发现在这里有一段:

> thor/lib/thor/base.rb:583

```ruby

      # Fire this callback whenever a method is added. Added methods are
      # tracked as commands by invoking the create_command method.
      def method_added(meth)
        meth = meth.to_s

        if meth == "initialize"
          initialize_added
          return
        end

        # Return if it's not a public instance method
        return unless public_method_defined?(meth.to_sym)

        @no_commands ||= false
        return if @no_commands || !create_command(meth) #here

        is_thor_reserved_word?(meth, :command)
        Thor::Base.register_klass_file(self)
      end

```

继续往上追, 停, 我们忘记一个时期,就是一个继承自`Thor`的类它只有在最后使用`start`方法之后才可以注入到当前的 cli 中的,我们是否需要从这个方法再看起呢? 来


```ruby
      # Parses the command and options from the given args, instantiate the class
      # and invoke the command. 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(:command, first_arg, second_arg, third_arg)
      #
      def start(given_args = ARGV, config = {})
        config[:shell] ||= Thor::Base.shell.new
        dispatch(nil, given_args.dup, nil, config) # this main line
      rescue Thor::Error => e
        config[:debug] || ENV["THOR_DEBUG"] == "1" ? (raise e) : config[:shell].error(e.message)
        exit(1) if exit_on_failure?
      rescue Errno::EPIPE
        # This happens if a thor command is piped to something like `head`,
        # which closes the pipe when it's done reading. This will also
        # mean that if the pipe is closed, further unnecessary
        # computation will not occur.
        exit(0)
      end
```

![Kobito.XdueBv.png](https://qiita-image-store.s3.amazonaws.com/0/56713/1ae0fd87-ad04-b310-cdba-17b9ab9e2435.png "Kobito.XdueBv.png")

> thor/lib/thor.rb:332

```ruby

    # The method responsible for dispatching given the args.
    def dispatch(meth, given_args, given_opts, config) #:nodoc: # rubocop:disable MethodLength
      meth ||= retrieve_command_name(given_args)
      command = all_commands[normalize_command_name(meth)]
	  # runtime value
	  #<struct Thor::Command name="help", description="Describe available commands or one specific command", long_description=nil, usage="help [COMMAND]", options={}>
      if !command && config[:invoked_via_subcommand]
        # We're a subcommand and our first argument didn't match any of our
        # commands. So we put it back and call our default command.
        given_args.unshift(meth)
        command = all_commands[normalize_command_name(default_command)]
      end

      if command
        args, opts = Thor::Options.split(given_args)
        if stop_on_unknown_option?(command) && !args.empty?
          # given_args starts with a non-option, so we treat everything as
          # ordinary arguments
          args.concat opts
          opts.clear
        end
      else
        args, opts = given_args, nil
        command = dynamic_command_class.new(meth)
      end

      opts = given_opts || opts || []
      config.merge!(:current_command => command, :command_options => command.options)

      instance = new(args, opts, config)
      yield instance if block_given?
      args = instance.args
      trailing = args[Range.new(arguments.size, -1)]
      instance.invoke_command(command, trailing || [])
    end
```


> thor/lib/thor/base.rb:574

```ruby
      # Everytime someone inherits from a Thor class, register the klass
      # and file into baseclass.
      def inherited(klass)
        Thor::Base.register_klass_file(klass)
        klass.instance_variable_set(:@no_commands, false)
      end
```


> thor/lib/thor/base.rb:115

```ruby
      # Whenever a class inherits from Thor or Thor::Group, we should track the
      # class and the file on Thor::Base. This is the method responsable for it.
      #
      def register_klass_file(klass) #:nodoc:
        file = caller[1].match(/(.*):\d+/)[1]
        Thor::Base.subclasses << klass unless Thor::Base.subclasses.include?(klass)

        file_subclasses = Thor::Base.subclass_files[File.expand_path(file)]
        file_subclasses << klass unless file_subclasses.include?(klass)
      end
    end
```