How do I use Thor actions without requiring a Thor CLI app?
Asked Answered
M

3

5

There are some great helper methods from Thor::Actions (http://textmate.rubyforge.org/thor/Thor/Actions.html) that I want access to but I cannot seem to use them without employing a Thor CLI app.

I've tried simply:

require "rubygems"
require "thor"

Thor::Actions.create_file "foo.txt", "contents"

Which throws:

run.rb:4:in '<main>': undefined method 'create_file' for Thor::Actions:Module (NoMethodError)

I realize I might be missing something really simple here. Thanks.

Majors answered 26/6, 2013 at 23:43 Comment(0)
E
5

Thor intends for your classes to subclass the Thor class. The Thor class then includes and extends modules allowing their methods to be class methods. If you look at the source, for example Actions.rb, you will see what I mean:

# thor/lib/thor/actions.rb

class Thor
  module Actions

    # this is the interesting part and answers your question
    def self.included(base) #:nodoc:
      base.extend ClassMethods
    end

    module ClassMethods

This is a common Ruby idiom that uses a mixin to define class methods (as opposed to instance methods) on its inclusor.

As an example,

[2] pry(main)> class Klass
[2] pry(main)*   module Mod  
[2] pry(main)*     def self.included(base)    
[2] pry(main)*       base.extend ClassMethods      
[2] pry(main)*     end  
[2] pry(main)*     module ClassMethods    
[2] pry(main)*       def act_as_class_method      
[2] pry(main)*         puts "Im a class method now!!!"        
[2] pry(main)*       end  
[2] pry(main)*     end  
[2] pry(main)*   end  
[2] pry(main)* end  
=> nil

Now calling

Klass::Mod.act_as_class_method

results in the same error you had

NoMethodError: undefined method `act_as_class_method' for Klass::Mod:Module
from (pry):26:in `__pry__'

But if you subclass Klass and include Klass::Mod the included call back extends the ClassMethod module, letting you use the methods defined in ClassMethods as class methods

[4] pry(main)> class Example < Klass
[4] pry(main)*   include Klass::Mod

[4] pry(main)*   self.act_as_class_method
[4] pry(main)* end  

=> Im a class method now!!!
=> nil

This took me a while to figure out at first, so don't feel bad and no, its not that simple or obvious.

Essayist answered 25/7, 2013 at 4:59 Comment(0)
D
5

To use Thor::Actions without inheriting from Thor:

class Builder # or whatever
  # To get your hands on the `from_superclass` method
  include Thor::Base

  # What we're interested in…
  include Thor::Actions
  source_root "/path/to/where/things/come/out"

  def initialize(*)
    # whatever you might want to do
    @destination_stack = [self.class.source_root]
  end
end

Hope someone else finds this useful. Tried and tested with Thor v0.18.1; as this is internal API stuff it's likely to break at some point in the future.

You can then use the helper methods in your Builder class like so:

class Builder
  def build
    in_root { 'do things' }
    create_file 'etc'
  end
end

Edit: If you want to control where you create files and folders you need to set destination_root like so:

class Builder
  include Thor::Base
  include Thor::Actions
  source_root Dir.pwd

  def initialize(root)
    self.destination_root = File.expand_path(root)
  end

  def build
    directory 'templates', 'target'
  end
end
Deter answered 10/11, 2013 at 1:26 Comment(0)
C
0

I'm new to Thor myself, but I don't think it's set up to work that independently.

Try creating a Thor task internally, and then starting it.

Here's an example I've tried out, and placed in a file called thor_createfile.rb (I've put in some additional things I'll explain after the code that might be enlightening for you):

#!/usr/bin/env ruby

require 'rubygems'    
require 'thor'

class MyThorTasks < Thor
  include Thor::Actions

  default_task :createInflexibleFile

  desc "createFile <fname> <content>", "Creates a file with some content"
  def createFile(fname, content)
    create_file fname, content
  end

  INFLEXIBLE_FILENAME = "the_filename.txt"
  INFLEXIBLE_CONTENT = "Greetings, Earthlings!"

  desc "createInflexibleFile", "Creates a file called '#{INFLEXIBLE_FILENAME}' containing '#{INFLEXIBLE_CONTENT}'"
  def createInflexibleFile
    puts "Creating a file called '#{INFLEXIBLE_FILENAME}' containing '#{INFLEXIBLE_CONTENT}'"
    create_file INFLEXIBLE_FILENAME, INFLEXIBLE_CONTENT
  end
end

MyThorTasks.start

You can see it defines a class that extends Thor, and then calls the start method on it.

Now you should be able to call it simply like this:

./thor_createfile.rb

and it will use the task designated as default_task.

But if you need to take some command-line parameters, you can explicitly call tasks by name. So to call the other task, for example:

./thor_createfile.rb createFile fancy_file_name.txt "Text to go inside the file"

Note I've told it to include Thor::Actions so all the items you were interested in (like create_file) are available.

Now you can add other tasks inside it (make sure to add the desc for each one or it will probably complain) and use those too, as needed.

To have it tell you about all of the tasks defined inside it, you can call it this way:

./thor_createfile.rb -?
Cavity answered 30/6, 2013 at 20:15 Comment(1)
Just a small stylistic point… you'll tend to find methods use snake case in most Ruby code. en.wikipedia.org/wiki/Snake_caseDeter

© 2022 - 2024 — McMap. All rights reserved.