codereading / codereading_thor

cloned thor for codereading purposes - Not fit for development.
MIT License
0 stars 1 forks source link

Walkthrough #4

Open adamakhtar opened 11 years ago

adamakhtar commented 11 years ago

@codereading/readers

(Eventually this will get edited and placed on the readme. )

In a directory of your choice create this file

#example.thor
class App < Thor
  desc "boom", "explodes stuff"
  def boom
    raise "boom" #heres the raise to give us a stacktrace
  end
end

2 Run

thor app:boom

You should get a stacktrace roughly similiar to this

/Users/adam/Code/Temporary/thor_codereading/thor.thor:4:in `boom': boom (RuntimeError)
    from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/gems/thor-0.16.0/lib/thor/task.rb:27:in `run'
    from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/gems/thor-0.16.0/lib/thor/invocation.rb:120:in `invoke_task'
    from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/gems/thor-0.16.0/lib/thor.rb:275:in `dispatch'
    from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/gems/thor-0.16.0/lib/thor/base.rb:425:in `start'
    from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/gems/thor-0.16.0/lib/thor/runner.rb:36:in `method_missing'
    from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/gems/thor-0.16.0/lib/thor/task.rb:29:in `run'
    from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/gems/thor-0.16.0/lib/thor/task.rb:126:in `run'
    from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/gems/thor-0.16.0/lib/thor/invocation.rb:120:in `invoke_task'
    from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/gems/thor-0.16.0/lib/thor.rb:275:in `dispatch'
    from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/gems/thor-0.16.0/lib/thor/base.rb:425:in `start'
    from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/gems/thor-0.16.0/bin/thor:6:in `<top (required)>'
    from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/bin/thor:19:in `load'
    from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/bin/thor:19:in `<main>'
    from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/bin/ruby_noexec_wrapper:14:in `eval'
    from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/bin/ruby_noexec_wrapper:14:in `<main>'

The stacktrace is ordered with the last method called being first. i.e. The top line is the last method called - the method with our raise call. The bottom line is the birthpoint of the program.

Look for the first lines mentioning the thor codebase

These two lines are the first to do so

from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/bin/thor:19:in `load'
from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/bin/thor:19:in `<main>'

but on inspection of /bin/thor line 19 don't exist - Does anyone know why this happen???

anyway just skip them and move up the stacktrace untill you find something relevant. Luckily

from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/gems/thor-0.16.0/bin/thor:6:in `<top (required)>'

show us the entry point. Not surprisingly it's the exec file at thor/bin/thor

#thor/bin/thor
#!/usr/bin/env ruby
# -*- mode: ruby -*-

require 'thor/runner'
$thor_runner = true
Thor::Runner.start

this is run whenever you run thor blahblahblah on the command line.

Thor::Runner.start is easy to find in your editor, just look at the stacktrace, it tells us it's at

from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/gems/thor-0.16.0/lib/thor.rb:275:in 'dispatch' from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/gems/thor-0.16.0/ lib/thor/base.rb:425 :in 'start' from /Users/adam/.rvm/gems/ruby-1.9.3-p194-Ruby@codereading/gems/thor-0.16.0/bin/thor:6:in '<top (required)>'

#lib/thor/base.rb:425

      # 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={})
        config[:shell] ||= Thor::Base.shell.new
        dispatch(nil, given_args.dup, nil, config)
      rescue Thor::Error => e
        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 task 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
adamakhtar commented 11 years ago

will add more tomorrow.

ericgj commented 11 years ago

but on inspection of /bin/thor line 19 don't exist - Does anyone know why this happen???

It looks like that's the rvm wrapper, not thor/bin/thor. Note the path.

adamakhtar commented 11 years ago

Well ive been busy working on a side project but going to try and plow through some more here.

resuming where I left off:

I actually missed something previously.

in the bin/thor

Thor::Runner.start is called

If you go to lib/thor/runner.rb

you will notice there is no start method, so where is it?

Thor::Runner inherits from Thor like so

class Thor::Runner < Thor:

A bit mind bending but ill ignore asking why for the moment.

So maybe the start method is in Thor's class definition in lib/thor.rb .

No such luck. But after doing a project wide search for "start" I found it in thor/base.rb

class Thor
   ...
   module Base
         ...
         def start

I spent ages thinking how Runner get its hands on it. Then in thor.rb I noticed at line 378 it includes Thor::Base

and Thor::Base has a hook for when it is included via

line 81

def included(base) #:nodoc:
        base.send :extend,  ClassMethods #=> BAM!! start is defined within Base::ClassMethods
        base.send :include, Invocation
        base.send :include, Shell
end

So it extends the Thor class with Base.start. Thor is then inherited by Runner which now has a shiny start method.

Ok with that over lets resume from this start method.

adamakhtar commented 11 years ago

Why did the authors create a Base module

Actually I wanted to know why did the authors created this Base module. Why didnt they just write those methods into the Thor class definition in lib/thor.rb.

I believe the answer is because of the class Thor::Groups. I havent used thor groups before but you can learn more on the wiki about this functionality.

The main difference is explained from code comments

# Thor has a special class called Thor::Group. The main difference to Thor class
# is that it invokes all tasks at once. It also include some methods that allows
# invocations to be done at the class method, which are not available to Thor
# tasks.
class Thor::Group
...
...
...
 include Thor::Base
end

The class Thor::Groups also requires some of the Base methods and includes Thor::Base just like the regular Thor class does.

Runner gets lots of methods like start

So Runner, which inherits from Thor now has a number of class methods such as .start and .dispatch.

Lets look at Runners inherited start method

def start(given_args=ARGV, config={})
        config[:shell] ||= Thor::Base.shell.new
        dispatch(nil, given_args.dup, nil, config)
      rescue Thor::Error => e

Ignoring the config[:shell] line for now, the next method dispatch(nil, given_args.dup, nil, config) is situated in lib/thor.rb at line 257 ( you can see that from the stacktrace produced earlier)

This too is another class method that was originally in Base::ClassMethods but was extended into Thor and via inheritance available to Runner.

# The method responsible for dispatching given the args.
   def dispatch(meth, given_args, given_opts, config) #:nodoc:
        meth ||= retrieve_task_name(given_args)
        task = all_tasks[normalize_task_name(meth)]
        ...

Extracting the task name from the arguments list

Since .start called dispatch passing nil for the meth argument retrieve_task_name(given_args) is called to extract the name of the task provided on the command line (thor app:boom).


#lib/thor.rb 312
# Retrieve the task name from given args.
    def retrieve_task_name(args) #:nodoc:
        meth = args.first.to_s unless args.empty?
        if meth && (map[meth] || meth !~ /^\-/)
             args.shift
        else
             nil
        end
     end

if we use pry to inspect what args (ARGV) looks like ala

def retrieve_task_name(args) #:nodoc:
     require 'pry'; binding.pry #make sure you have installed the pry gem via => gem install pry
     meth = args.first.to_s unless args.empty?
     if meth && (map[meth] || meth !~ /^\-/)

we get ["app:boom"] as expected.

This is set as the value for meth and extracted via args.shift from the argument args.

regarding (map[meth] || meth !~ /^\-/)

map is a method defined on line 82 which stores any mappings - or command line shortcuts as I like to think of them - you may have set in your thor definition file. We havent so that part of the OR expression returns nil.

The second part of the OR expression is simply making sure no dashes are present in the args.

Im not sure what the significance of this but its probably to do with -- flags or something.

all_tasks and superclasses

Moving on the next line in dispatch is task = all_tasks[normalize_task_name(meth)]

all_tasks is another class method that is defined in Base::ClassMethods and eventually extended into Thor just like the start method above.

This is being called in the context of Runner

base.rb line 331

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

from_superclass, another inherited method, is defined further down in base.rb and looks like this

        # Retrieves a value from superclass. If it reaches the baseclass,
        # returns default.
        def from_superclass(method, default=nil)
          if self == baseclass || !superclass.respond_to?(method, true)
            default
          else
            value = superclass.send(method)

            if value
              if value.is_a?(TrueClass) || value.is_a?(Symbol)
                value
              else
                value.dup
              end
            end
          end
        end

Ill save these for the next comment.