Skip to content

Commit

Permalink
Allow hidden tasks.
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed Jul 16, 2010
1 parent 0c8d36f commit 71dc13e
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 53 deletions.
14 changes: 9 additions & 5 deletions lib/thor.rb
Expand Up @@ -23,14 +23,15 @@ def default_task(meth=nil)
# ==== Parameters
# usage<String>
# description<String>
# options<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
@usage, @desc, @hide = usage, description, options[:hide] || false
end
end

Expand Down Expand Up @@ -135,6 +136,7 @@ def method_option(name, options={})
#
def start(original_args=ARGV, config={})
@@original_args = original_args

super do |given_args|
meth = given_args.first.to_s

Expand All @@ -154,7 +156,7 @@ def start(original_args=ARGV, config={})
args, opts = given_args, {}
end

task ||= Thor::Task::Dynamic.new(meth)
task ||= Thor::DynamicTask.new(meth)
trailing = args[Range.new(arguments.size, -1)]
new(args, opts, config).invoke(task, trailing || [])
end
Expand Down Expand Up @@ -204,11 +206,12 @@ def help(shell, subcommand = false)
# Returns tasks ready to be printed.
def printable_tasks(all = true, subcommand = false)
(all ? all_tasks : tasks).map do |_, task|
next if task.hidden?
item = []
item << banner(task, false, subcommand)
item << (task.description ? "# #{task.description.gsub(/\s+/m,' ')}" : "")
item
end
end.compact
end

def subcommands
Expand Down Expand Up @@ -239,8 +242,9 @@ def baseclass #:nodoc:

def create_task(meth) #:nodoc:
if @usage && @desc
tasks[meth.to_s] = Thor::Task.new(meth, @desc, @long_desc, @usage, method_options)
@usage, @desc, @long_desc, @method_options = nil
base_class = @hide ? Thor::HiddenTask : Thor::Task
tasks[meth.to_s] = base_class.new(meth, @desc, @long_desc, @usage, method_options)
@usage, @desc, @long_desc, @method_options, @hide = nil
true
elsif self.all_tasks[meth.to_s] || meth.to_sym == :method_missing
true
Expand Down
2 changes: 1 addition & 1 deletion lib/thor/group.rb
Expand Up @@ -233,7 +233,7 @@ def banner

# Represents the whole class as a task.
def self_task #:nodoc:
Thor::Task::Dynamic.new(self.namespace, class_options)
Thor::DynamicTask.new(self.namespace, class_options)
end

def baseclass #:nodoc:
Expand Down
2 changes: 1 addition & 1 deletion lib/thor/invocation.rb
Expand Up @@ -156,7 +156,7 @@ def _validate_task(object, task) #:nodoc:
raise "Expected Thor class, got #{klass}" unless klass <= Thor::Base

task ||= klass.default_task if klass.respond_to?(:default_task)
task = klass.all_tasks[task.to_s] || Thor::Task::Dynamic.new(task) if task && !task.is_a?(Thor::Task)
task = klass.all_tasks[task.to_s] || Thor::DynamicTask.new(task) if task && !task.is_a?(Thor::Task)
task
end

Expand Down
92 changes: 51 additions & 41 deletions lib/thor/task.rb
Expand Up @@ -2,21 +2,6 @@ class Thor
class Task < Struct.new(:name, :description, :long_description, :usage, :options)
FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/

# A dynamic task that handles method missing scenarios.
class Dynamic < Task
def initialize(name, options=nil)
super(name.to_s, "A dynamically-generated task", name.to_s, name.to_s, options)
end

def run(instance, args=[])
if (instance.methods & [name.to_s, name.to_sym]).empty?
super
else
instance.class.handle_no_task_error(name)
end
end
end

def initialize(name, description, long_description, usage, options=nil)
super(name.to_s, description, long_description, usage, options || {})
end
Expand All @@ -26,6 +11,10 @@ def initialize_copy(other) #:nodoc:
self.options = other.options.dup if other.options
end

def hidden?
false
end

# By default, a task invokes a method in the thor class. You can change this
# implementation to create custom tasks.
def run(instance, args=[])
Expand Down Expand Up @@ -66,39 +55,60 @@ def formatted_usage(klass, namespace = true, subcommand = false)
formatted.strip
end

protected
protected

def not_debugging?(instance)
!(instance.class.respond_to?(:debugging) && instance.class.debugging)
end
def not_debugging?(instance)
!(instance.class.respond_to?(:debugging) && instance.class.debugging)
end

def required_options
@required_options ||= options.map{ |_, o| o.usage if o.required? }.compact.sort.join(" ")
end
def required_options
@required_options ||= options.map{ |_, o| o.usage if o.required? }.compact.sort.join(" ")
end

# Given a target, checks if this class name is not a private/protected method.
def public_method?(instance) #:nodoc:
collection = instance.private_methods + instance.protected_methods
(collection & [name.to_s, name.to_sym]).empty?
end
# Given a target, checks if this class name is not a private/protected method.
def public_method?(instance) #:nodoc:
collection = instance.private_methods + instance.protected_methods
(collection & [name.to_s, name.to_sym]).empty?
end

def sans_backtrace(backtrace, caller) #:nodoc:
saned = backtrace.reject { |frame| frame =~ FILE_REGEXP }
saned -= caller
end
def sans_backtrace(backtrace, caller) #:nodoc:
saned = backtrace.reject { |frame| frame =~ FILE_REGEXP }
saned -= caller
end

def handle_argument_error?(instance, error, caller)
not_debugging?(instance) && error.message =~ /wrong number of arguments/ && begin
saned = sans_backtrace(error.backtrace, caller)
# Ruby 1.9 always include the called method in the backtrace
saned.empty? || (saned.size == 1 && RUBY_VERSION >= "1.9")
end
def handle_argument_error?(instance, error, caller)
not_debugging?(instance) && error.message =~ /wrong number of arguments/ && begin
saned = sans_backtrace(error.backtrace, caller)
# Ruby 1.9 always include the called method in the backtrace
saned.empty? || (saned.size == 1 && RUBY_VERSION >= "1.9")
end
end

def handle_no_method_error?(instance, error, caller)
not_debugging?(instance) &&
error.message =~ /^undefined method `#{name}' for #{Regexp.escape(instance.to_s)}$/
end
def handle_no_method_error?(instance, error, caller)
not_debugging?(instance) &&
error.message =~ /^undefined method `#{name}' for #{Regexp.escape(instance.to_s)}$/
end
end

# A task that is hidden in help messages but still invocable.
class HiddenTask < Task
def hidden?
true
end
end

# A dynamic task that handles method missing scenarios.
class DynamicTask < Task
def initialize(name, options=nil)
super(name.to_s, "A dynamically-generated task", name.to_s, name.to_s, options)
end

def run(instance, args=[])
if (instance.methods & [name.to_s, name.to_sym]).empty?
super
else
instance.class.handle_no_task_error(name)
end
end
end
end
5 changes: 5 additions & 0 deletions spec/fixtures/script.thor
Expand Up @@ -26,6 +26,11 @@ class MyScript < Thor
[type]
end

desc "hidden TYPE", "this is hidden", :hide => true
def hidden(type)
[type]
end

desc "foo BAR", <<END
do some fooing
This is more info!
Expand Down
10 changes: 5 additions & 5 deletions spec/task_spec.rb
Expand Up @@ -31,16 +31,16 @@ def task(options={})

describe "#dynamic" do
it "creates a dynamic task with the given name" do
Thor::Task::Dynamic.new('task').name.must == 'task'
Thor::Task::Dynamic.new('task').description.must == 'A dynamically-generated task'
Thor::Task::Dynamic.new('task').usage.must == 'task'
Thor::Task::Dynamic.new('task').options.must == {}
Thor::DynamicTask.new('task').name.must == 'task'
Thor::DynamicTask.new('task').description.must == 'A dynamically-generated task'
Thor::DynamicTask.new('task').usage.must == 'task'
Thor::DynamicTask.new('task').options.must == {}
end

it "does not invoke an existing method" do
mock = mock()
mock.class.should_receive(:handle_no_task_error).with("to_s")
Thor::Task::Dynamic.new('to_s').run(mock)
Thor::DynamicTask.new('to_s').run(mock)
end
end

Expand Down
10 changes: 10 additions & 0 deletions spec/thor_spec.rb
Expand Up @@ -97,6 +97,16 @@
capture(:stdout) { MyChildScript.start(["help"]) }.must =~ /animal KIND \s+# fish around/m
end
end

describe "when :hide is supplied" do
it "does not show the task in help" do
capture(:stdout) { MyScript.start(["help"]) }.must_not =~ /this is hidden/m
end

it "but the task is still invokcable not show the task in help" do
MyScript.start(["hidden", "yesyes"]).must == ["yesyes"]
end
end
end

describe "#method_options" do
Expand Down

0 comments on commit 71dc13e

Please sign in to comment.