How to make two thor tasks share options?
Asked Answered
G

5

12

With Thor one can use method_option to set the options for a particular task. To set the options for all tasks in a class one can use class_option. But what about the case where one wants some tasks of a class, but not all, to share options?

In the following task1 and task2 shares options but they do not share all options and they share no options with task3.

require 'thor'

class Cli < Thor
  desc 'task1', 'Task 1'
  method_option :type, :type => :string, :required => true, :default => 'foo'
  def task1
  end

  desc 'task2', 'Task 2'
  method_option :type, :type => :string, :required => true, :default => 'foo'
  method_option :value, :type => :numeric
  def task2
  end

  desc 'task3', 'Task 3'
  method_option :verbose, :type => :boolean, :aliases => '-v'
  def task3
  end
end

Cli.start(ARGV)

The problem with stating method_option :type, :type => :string, :required => true, :default => 'foo' for both task1 and task2 is that it violates the DRY principle. Is there an idiomatic way of handling this?

Godevil answered 15/1, 2013 at 20:28 Comment(0)
G
15

method_option is defined in thor.rb and it takes the following parameters according to the documentation:

  • name<Symbol>:: The name of the argument.
  • options<Hash>:: Described below.

Knowing this you can store the parameters to method_option in an array and expand that array into separate parameters as method_option is called.

require 'thor'

class Cli < Thor
  shared_options = [:type, {:type => :string, :required => true, :default => 'foo'}]

  desc 'task1', 'Task 1'
  method_option *shared_options
  def task1
  end

  desc 'task2', 'Task 2'
  method_option *shared_options
  method_option :value, :type => :numeric
  def task2
  end

  desc 'task3', 'Task 3'
  method_option :verbose, :type => :boolean, :aliases => '-v'
  def task3
  end
end

Cli.start(ARGV)

I have no idea if this is idiomatic and I do not think it is that elegant. Still, it is better than violating the DRY principle.

Godevil answered 16/1, 2013 at 14:23 Comment(1)
good idea, but can we go further and maybe define shared_options class method delegating to method_option + merging the common hash?Pia
M
3

I would just use a superclass like this:

require 'thor'

class CliBase < Thor
  def self.shared_options

    method_option :verbose,
                  :aliases => '-v',
                  :type => :boolean,
                  :desc => 'Verbose',
                  :default => false,
                  :required => false

  end
end

... then subclass as follows:

require 'cli_base'

class Cli < CliBase
  desc 'task1', 'Task 1'
  shared_options
  def task1
  end

  desc 'task2', 'Task 2'
  shared_options
  method_option :value, :type => :numeric
  def task2
  end

  desc 'task3', 'Task 3'
  method_option :colors, :type => :boolean, :aliases => '-c'
  def task3
  end
end

Cli.start(ARGV)
Melton answered 19/4, 2013 at 22:2 Comment(2)
Do you really need subclassing here? Why not just defining shared_options in CLI class?Pia
I think there are many ways to answer the OP. I happen to despise writing code more than once so with my solution, write the base class, put it in a gem and then use it whenever writing another Thor script. After all, -v for verbose is something I will use again outside of a single instance. But sure, you can do it however you like.Melton
P
3

So there is a nice dry way to do this now, but may not fall into the requirements of being as idiomatic, though I wanted to mention it for anyone looking for a more recent answer.

You can start by using the class_options to set the majority of shared options between your methods:

module MyModule
  class Hello < Thor
    class_option :name, :desc => "name", :required => true
    class_option :greet, :desc => "greeting to use", :required => true

    desc "Hello", "Saying hello"
    def say
      puts "#{options[:greet]}, #{options[:name]}!"
    end

    desc "Say", "Saying anything"
    remove_class_option :greet
    def hello
      puts "Hello, #{options[:name]}!"
    end

    def foo
      puts "Foo, #{options[:name]}!"
    end
  end
end

The best part about this, is that it pertains to all methods after the declaration. With these set to required, you can see that the first method requires both greet and name, but say and foo only require the name.

Pester answered 15/3, 2018 at 23:30 Comment(0)
A
2

I had the same problem and I used what N.N. answered. But I found some problems:

If you want to share more than one option as in the example, it doesn't work very well. Imagine you want to share :value between task2 and task3. You could create another shared_options or you could create an array with the shared options and access it with the shared_option name.

This works but it's verbose and hard to read. I've implemented something small to be able to share options.

Cli < Thor  
  class << self
      def add_shared_option(name, options = {})
        @shared_options = {} if @shared_options.nil?
        @shared_options[name] =  options
      end

      def shared_options(*option_names)
        option_names.each do |option_name|
          opt =  @shared_options[option_name]
          raise "Tried to access shared option '#{option_name}' but it was not previously defined" if opt.nil?
          option option_name, opt
        end
      end
    end
    #...commands 
end

This creates a hash with the option name as key, and the 'definition' (required, default, etc) as value (which is a hash). This is easily accessible afterwards.

With this, you can do the following:

require 'thor'

class Cli < Thor

  add_shared_option :type,  :type => :string, :required => true, :default => 'foo'
  add_shared_option :value, :type => :numeric

  desc 'task1', 'Task 1'
  shared_options :type
  def task1
  end

  desc 'task2', 'Task 2'
  shared_options :type, :value
  def task2
  end

  desc 'task3', 'Task 3'
  shared_options :value
  def task3
  end
end

Cli.start(ARGV)

For me it looks more readable, and if the number of commands is bigger than 3 or 4 it's a great improvement.

Aerophagia answered 18/7, 2014 at 16:20 Comment(0)
S
0

So as not to type "shared_options" all the time, you can also do this:

require 'thor'

class Cli < Thor
  class << self
    private
    def shared_options!
      # list your shared options here
      method_option :opt1, type: :boolean
      method_option :opt2, type: :numeric
      # etc
    end

    # alias original desc so we can call it from inside new desc
    alias_method :orig_desc, :desc

    # redefine desc, calling original desc, and then applying shared_options!
    def desc(*args)
      orig_desc(*args)
      shared_options!
    end
  end

  desc 'task1', 'Task 1'

  def task1
  end

  desc 'task2', 'Task 2'

  def task2
  end

  desc 'task3', 'Task 3'

  def task3
  end
end

Or if you don't want acrobatics with method aliasing, you could just define your own method "my_desc" and call that instead of "desc".

Scarabaeoid answered 5/4, 2016 at 18:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.