josevalim-thor 0.10.7 → 0.10.8

Sign up to get free protection for your applications and to get access to all the features.
data/lib/thor/actions.rb CHANGED
@@ -304,5 +304,13 @@ class Thor
304
304
  end
305
305
  end
306
306
 
307
+ protected
308
+
309
+ # Update dump_config to dump also behavior and root.
310
+ #
311
+ def _dump_config #:nodoc:
312
+ super.merge!(:behavior => self.behavior, :root => @root_stack[0])
313
+ end
314
+
307
315
  end
308
316
  end
data/lib/thor/base.rb CHANGED
@@ -7,7 +7,8 @@ require 'thor/task'
7
7
  require 'thor/util'
8
8
 
9
9
  class Thor
10
- HELP_MAPPINGS = ["-h", "-?", "--help", "-D"]
10
+ HELP_MAPPINGS = %w(-h -? --help -D)
11
+ RESERVED_TASK_NAMES = %w(all invoke shell options behavior root destination_root relative_root source_root)
11
12
 
12
13
  class Maxima < Struct.new(:usage, :options, :class_options)
13
14
  end
@@ -215,19 +216,6 @@ class Thor
215
216
  end
216
217
  end
217
218
 
218
- # Retrieve a specific task from this Thor class. If the desired Task cannot
219
- # be found, returns a dynamic Thor::Task that will map to the given method.
220
- #
221
- # ==== Parameters
222
- # meth<Symbol>:: the name of the task to be retrieved
223
- #
224
- # ==== Returns
225
- # Task
226
- #
227
- def [](meth)
228
- all_tasks[meth.to_s] || Thor::Task.dynamic(meth)
229
- end
230
-
231
219
  # All methods defined inside the given block are not added as tasks.
232
220
  #
233
221
  # So you can do:
@@ -384,6 +372,11 @@ class Thor
384
372
  end
385
373
 
386
374
  return if @no_tasks || !valid_task?(meth)
375
+
376
+ if RESERVED_TASK_NAMES.include?(meth)
377
+ raise ScriptError, "'#{meth}' is a Thor reserved word and cannot be defined as task"
378
+ end
379
+
387
380
  Thor::Base.register_klass_file(self)
388
381
  create_task(meth)
389
382
  end
@@ -456,20 +449,10 @@ class Thor
456
449
  end
457
450
 
458
451
  self.options = Thor::CoreExt::HashWithIndifferentAccess.new(options).freeze
459
- self.shell = config[:shell]
460
452
 
461
- # Add base to shell if an accessor is provided.
462
- self.shell.base = self if self.shell.respond_to?(:base)
463
- end
464
-
465
- # Common methods that are delegated to the shell.
466
- #
467
- SHELL_DELEGATED_METHODS.each do |method|
468
- module_eval <<-METHOD, __FILE__, __LINE__
469
- def #{method}(*args)
470
- shell.#{method}(*args)
471
- end
472
- METHOD
453
+ # Configure shell and set base if not already
454
+ self.shell = config[:shell]
455
+ self.shell.base ||= self if self.shell.respond_to?(:base)
473
456
  end
474
457
 
475
458
  # Holds the shell for the given Thor instance. If no shell is given,
@@ -485,15 +468,133 @@ class Thor
485
468
  @shell = shell
486
469
  end
487
470
 
488
- # Finds a task with the name given and invokes it with the given arguments.
489
- # This is the default interface to invoke tasks. You can always run a task
490
- # directly, but the invocation system will be implemented in a fashion
491
- # that a same task cannot be invoked twice (a la rake).
471
+ # Common methods that are delegated to the shell.
492
472
  #
493
- def invoke(name, *args)
494
- self.class[name].run(self, *args)
473
+ SHELL_DELEGATED_METHODS.each do |method|
474
+ module_eval <<-METHOD, __FILE__, __LINE__
475
+ def #{method}(*args)
476
+ shell.#{method}(*args)
477
+ end
478
+ METHOD
495
479
  end
496
- end
497
480
 
481
+ # Receives a name and invokes it. The name can be either a namespaced name,
482
+ # a current class task or even a class. Arguments are given in an array and
483
+ # options given are merged with the invoker options.
484
+ #
485
+ # ==== Examples
486
+ #
487
+ # class A < Thor
488
+ # def foo
489
+ # invoke :bar
490
+ # invoke "b:lib", ["merb", "rails"]
491
+ # end
492
+ #
493
+ # def bar
494
+ # invoke "b:lib", ["merb", "rails"]
495
+ # # magic
496
+ # end
497
+ # end
498
+ #
499
+ # class B < Thor
500
+ # argument :preferred_framework, :type => :string
501
+ #
502
+ # def lib(second_framework)
503
+ # # magic
504
+ # end
505
+ # end
506
+ #
507
+ # You can notice that the method "foo" above invokes two tasks: "bar",
508
+ # which belongs to the same class and "lib" that belongs to the class B.
509
+ #
510
+ # By using an invocation system you ensure that a task is invoked only once.
511
+ # In the example above, invoking foo will invoke "b:lib" just once, even if
512
+ # it's invoked later by "bar" method.
513
+ #
514
+ # When invoking another class, there are a few things to keep in mind:
515
+ #
516
+ # 1) Class arguments are parsed first. In the example above, preferred
517
+ # framework is going to consume "merb" and second framework is going
518
+ # to be set to "rails".
519
+ #
520
+ # 2) All options and configurations are sent to the invoked class.
521
+ # So the invoked class is going to use the same shell instance, will
522
+ # have the same behavior (:invoke or :revoke) and so on.
523
+ #
524
+ # Invoking a Thor::Group happens in the same away as above:
525
+ #
526
+ # class C < Thor::Group
527
+ # def one
528
+ # end
529
+ # end
530
+ #
531
+ # Is invoked as:
532
+ #
533
+ # invoke "c"
534
+ #
535
+ # Or even as:
536
+ #
537
+ # invoke C
538
+ #
539
+ def invoke(name, method_args=[], options={})
540
+ @_invocations ||= Hash.new { |h,k| h[k] = [] }
541
+ instance, task = _setup_for_invoke(name, method_args, options)
542
+
543
+ current = @_invocations[instance.class]
544
+ return if current.include?("all")
545
+
546
+ if task
547
+ task = self.class.all_tasks[task.to_s] || Task.dynamic(task) unless task.is_a?(Thor::Task)
548
+ return if current.include?(task.name)
549
+
550
+ current << task.name
551
+ task.run(instance, method_args)
552
+ else
553
+ current << "all"
554
+ instance.class.all_tasks.collect { |_, task| task.run(instance) }
555
+ end
556
+ end
557
+
558
+ protected
559
+
560
+ # This is the method responsable for retrieving and setting up an
561
+ # instance to be used in invoke.
562
+ #
563
+ def _setup_for_invoke(name, method_args, options) #:nodoc:
564
+ case name
565
+ when NilClass, Thor::Task
566
+ # Do nothing, we already have what we want
567
+ when Class
568
+ klass = name
569
+ else
570
+ name = name.to_s
571
+ unless self.class.all_tasks[name]
572
+ klass, task = Thor::Util.namespace_to_thor_class(name) rescue Thor::Error
573
+ end
574
+ end
575
+
576
+ if klass.nil?
577
+ return self, name
578
+ elsif klass <= Thor::Base
579
+ size = klass.arguments.size
580
+ class_args = method_args.slice!(0, size)
581
+ instance = klass.new(class_args, self.options.merge(options), _dump_config)
582
+
583
+ task ||= klass.default_task if klass <= Thor
584
+ instance.instance_variable_set("@_invocations", @_invocations)
585
+
586
+ return instance, task
587
+ else
588
+ raise ScriptError, "Expected Thor class, got #{klass}"
589
+ end
590
+ end
591
+
592
+ # Dump the configuration values for this current class.
593
+ #
594
+ def _dump_config #:nodoc:
595
+ { :shell => self.shell }
596
+ end
597
+
598
+ end
498
599
  end
499
600
  end
@@ -11,15 +11,10 @@ class Thor
11
11
  #
12
12
  class HashWithIndifferentAccess < ::Hash
13
13
 
14
- def initialize(hash)
14
+ def initialize(hash={})
15
15
  super()
16
-
17
16
  hash.each do |key, value|
18
- if key.is_a?(Symbol)
19
- self[key.to_s] = value
20
- else
21
- self[key] = value
22
- end
17
+ self[convert_key(key)] = value
23
18
  end
24
19
  end
25
20
 
@@ -27,6 +22,10 @@ class Thor
27
22
  super(convert_key(key))
28
23
  end
29
24
 
25
+ def []=(key, value)
26
+ super(convert_key(key), value)
27
+ end
28
+
30
29
  def delete(key)
31
30
  super(convert_key(key))
32
31
  end
@@ -35,6 +34,17 @@ class Thor
35
34
  indices.collect { |key| self[convert_key(key)] }
36
35
  end
37
36
 
37
+ def merge(other)
38
+ dup.merge!(other)
39
+ end
40
+
41
+ def merge!(other)
42
+ other.each do |key, value|
43
+ self[convert_key(key)] = value
44
+ end
45
+ self
46
+ end
47
+
38
48
  protected
39
49
 
40
50
  def convert_key(key)
@@ -110,11 +110,7 @@ class Thor #:nodoc:
110
110
  end
111
111
 
112
112
  def merge(other)
113
- new = clone
114
- other.each do |key, value|
115
- new[key] = value
116
- end
117
- new
113
+ dup.merge!(other)
118
114
  end
119
115
 
120
116
  def merge!(other)
data/lib/thor/group.rb CHANGED
@@ -22,6 +22,71 @@ class Thor::Group
22
22
  end
23
23
  end
24
24
 
25
+ # Sets the condition for some task to be executed in the class level. Why
26
+ # is this important? Setting the conditions in the class level allows to
27
+ # an inherited class change the conditions and customize the Thor::Group as
28
+ # it wishes.
29
+ #
30
+ # The conditions given are retrieved from the options hash. Let's suppose
31
+ # that a task is only executed if --test-framework is rspec. You could do
32
+ # this:
33
+ #
34
+ # class_option :test_framework, :type => :string
35
+ #
36
+ # conditions :test_framework => :rspec
37
+ # def create_rspec_files
38
+ # # magic
39
+ # end
40
+ #
41
+ # Later someone creates a framework on top of rspec and need rspec files to
42
+ # generated as well. He could then change the conditions:
43
+ #
44
+ # conditions :test_framework => [ :rspec, :remarkable ], :for => :create_rspec_files
45
+ #
46
+ # He could also use remove_conditions and remove previous set conditions:
47
+ #
48
+ # remove_conditions :test_framework, :for => :create_rspec_files
49
+ #
50
+ # Conditions only work with the class option to be comparead to is a boolean,
51
+ # string or numeric (no array or hash comparisions).
52
+ #
53
+ # ==== Parameters
54
+ # conditions<Hash>:: the conditions for the task. The key is the option name
55
+ # and the value is the condition to be checked. If the
56
+ # condition is an array, it checkes if the current value
57
+ # is included in the array. If a regexp, checks if the
58
+ # value matches, all other values are simply compared (==).
59
+ #
60
+ def conditions(conditions=nil)
61
+ subject = if conditions && conditions[:for]
62
+ find_and_refresh_task(conditions.delete(:for)).conditions
63
+ else
64
+ @conditions ||= {}
65
+ end
66
+
67
+ subject.merge!(conditions) if conditions
68
+ subject
69
+ end
70
+
71
+ # Remove a previous specified condition. Check <tt>conditions</tt> above for
72
+ # a complete example.
73
+ #
74
+ # ==== Parameters
75
+ # conditions<Array>:: An array of conditions to be removed.
76
+ # for<Hash>:: A hash with :for as key indicating the task to remove the conditions from.
77
+ #
78
+ # ==== Examples
79
+ #
80
+ # remove_conditions :test_framework, :orm, :for => :create_app_skeleton
81
+ #
82
+ def remove_conditions(*conditions)
83
+ subject = find_and_refresh_task(conditions.pop[:for]).conditions
84
+ conditions.each do |condition|
85
+ subject.delete(condition)
86
+ end
87
+ subject
88
+ end
89
+
25
90
  # Start in Thor::Group works differently. It invokes all tasks inside the
26
91
  # class and does not have to parse task options.
27
92
  #
@@ -34,7 +99,7 @@ class Thor::Group
34
99
  opts = Thor::Options.new(class_options)
35
100
  opts.parse(args)
36
101
 
37
- new(opts.arguments, opts.options, config).invoke_all
102
+ new(opts.arguments, opts.options, config).invoke(:all)
38
103
  end
39
104
  rescue Thor::Error => e
40
105
  config[:shell].error e.message
@@ -75,15 +140,21 @@ class Thor::Group
75
140
  end
76
141
 
77
142
  def create_task(meth) #:nodoc:
78
- tasks[meth.to_s] = Thor::Task.new(meth, nil, nil, nil)
143
+ tasks[meth.to_s] = Thor::Task.new(meth, nil, nil, nil, @conditions)
144
+ @conditions = nil
79
145
  end
80
146
  end
81
147
 
82
- # Invokes all tasks in the instance.
83
- #
84
- def invoke_all
85
- self.class.all_tasks.map { |_, task| task.run(self) }
86
- end
87
-
88
148
  include Thor::Base
149
+
150
+ protected
151
+
152
+ # Overwrite _setup_for_invoke to force invocation of all tasks when :all is
153
+ # supplied.
154
+ #
155
+ def _setup_for_invoke(name, method_args, options)
156
+ name = nil if name.to_s == "all"
157
+ super(name, method_args, options)
158
+ end
159
+
89
160
  end
data/lib/thor/task.rb CHANGED
@@ -1,30 +1,31 @@
1
1
  class Thor
2
- class Task < Struct.new(:name, :description, :usage, :options)
2
+ class Task < Struct.new(:name, :description, :usage, :options, :conditions)
3
3
 
4
4
  # Creates a dynamic task. Dynamic tasks are created on demand to allow method
5
5
  # missing calls (since a method missing does not have a task object for it).
6
6
  #
7
7
  def self.dynamic(name)
8
- new(name, "A dynamically-generated task", name.to_s, nil)
8
+ new(name, "A dynamically-generated task", name.to_s)
9
9
  end
10
10
 
11
- def initialize(name, description, usage, options)
12
- super(name, description, usage, options || {})
11
+ def initialize(name, description, usage, options=nil, conditions=nil)
12
+ super(name.to_s, description, usage, options || {}, conditions || {})
13
13
  end
14
14
 
15
15
  # Dup the options hash on clone.
16
16
  #
17
17
  def initialize_copy(other)
18
18
  super(other)
19
- self.options = other.options.dup if other.options
19
+ self.options = other.options.dup if other.options
20
+ self.conditions = other.conditions.dup if other.conditions
20
21
  end
21
22
 
22
23
  # By default, a task invokes a method in the thor class. You can change this
23
24
  # implementation to create custom tasks.
24
25
  #
25
- def run(instance, *args)
26
+ def run(instance, args=[])
26
27
  raise UndefinedTaskError, "the '#{name}' task of #{instance.class} is private" unless public_method?(instance)
27
- instance.send(name, *args)
28
+ instance.send(name, *args) if valid_conditions?(instance)
28
29
  rescue ArgumentError => e
29
30
  backtrace = sans_backtrace(e.backtrace, caller)
30
31
 
@@ -86,7 +87,30 @@ class Thor
86
87
  # Given a target, checks if this class name is not a private/protected method.
87
88
  #
88
89
  def public_method?(instance)
89
- !(instance.private_methods + instance.protected_methods).include?(name.to_s)
90
+ collection = instance.private_methods + instance.protected_methods
91
+ !(collection).include?(name.to_s) && !(collection).include?(name.to_sym) # For Ruby 1.9
92
+ end
93
+
94
+ # Check if the task conditions are met before invoking.
95
+ #
96
+ def valid_conditions?(instance)
97
+ return true if conditions.empty?
98
+
99
+ conditions.each do |key, expected|
100
+ actual = stringify!(instance.options[key])
101
+ expected = stringify!(expected)
102
+
103
+ return false if case expected
104
+ when Regexp
105
+ actual !~ expected
106
+ when Array
107
+ !expected.include?(actual)
108
+ else
109
+ actual != expected
110
+ end
111
+ end
112
+
113
+ true
90
114
  end
91
115
 
92
116
  # Clean everything that comes from the Thor gempath and remove the caller.
@@ -97,5 +121,18 @@ class Thor
97
121
  saned -= caller
98
122
  end
99
123
 
124
+ # Receives an object and convert any symbol to string.
125
+ #
126
+ def stringify!(duck)
127
+ case duck
128
+ when Array
129
+ duck.map!{ |i| i.is_a?(Symbol) ? i.to_s : i }
130
+ when Symbol
131
+ duck.to_s
132
+ else
133
+ duck
134
+ end
135
+ end
136
+
100
137
  end
101
138
  end
data/lib/thor.rb CHANGED
@@ -127,14 +127,14 @@ class Thor
127
127
  config[:shell] ||= Thor::Base.shell.new
128
128
 
129
129
  meth = normalize_task_name(args.shift)
130
- task = self[meth]
130
+ task = all_tasks[meth] || Task.dynamic(meth)
131
131
 
132
132
  options = class_options.merge(task.options)
133
133
  opts = Thor::Options.new(options)
134
134
  opts.parse(args)
135
135
 
136
136
  instance = new(opts.arguments, opts.options, config)
137
- instance.invoke(task.name, *opts.trailing)
137
+ instance.invoke(task, opts.trailing)
138
138
  rescue Thor::Error => e
139
139
  config[:shell].error e.message
140
140
  end
@@ -153,7 +153,7 @@ class Thor
153
153
  meth, options = nil, meth if meth.is_a?(Hash)
154
154
 
155
155
  if meth
156
- task = self.all_tasks[meth]
156
+ task = all_tasks[meth]
157
157
  raise UndefinedTaskError, "task '#{meth}' could not be found in namespace '#{self.namespace}'" unless task
158
158
 
159
159
  shell.say "Usage:"
@@ -198,7 +198,7 @@ class Thor
198
198
  end
199
199
 
200
200
  def create_task(meth) #:nodoc:
201
- tasks[meth.to_s] = Thor::Task.new(meth, @desc, @usage, method_options)
201
+ tasks[meth.to_s] = Thor::Task.new(meth, @desc, @usage, method_options, nil)
202
202
  @usage, @desc, @method_options = nil
203
203
  end
204
204
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: josevalim-thor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.7
4
+ version: 0.10.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yehuda Katz
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-06-17 00:00:00 -07:00
12
+ date: 2009-06-20 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15