josevalim-thor 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ class Thor
2
+ module Actions
3
+
4
+ # Injects the given content into a file. Different from append_file,
5
+ # prepend_file and gsub_file, this method is reversible. By this reason,
6
+ # the flag can only be strings. gsub_file is your friend if you need to
7
+ # deal with more complex cases.
8
+ #
9
+ # ==== Parameters
10
+ # destination<String>:: Relative path to the destination root
11
+ # data<String>:: Data to add to the file. Can be given as a block.
12
+ # flag<String>:: Flag of where to add the changes.
13
+ # log_status<Boolean>:: If false, does not log the status. True by default.
14
+ # If a symbol is given, uses it as the output color.
15
+ #
16
+ # ==== Examples
17
+ #
18
+ # inject_into_file "config/environment.rb", "config.gem thor", :after => "Rails::Initializer.run do |config|\n"
19
+ #
20
+ # inject_into_file "config/environment.rb", :after => "Rails::Initializer.run do |config|\n" do
21
+ # gems = ask "Which gems would you like to add?"
22
+ # gems.split(" ").map{ |gem| " config.gem #{gem}" }.join("\n")
23
+ # end
24
+ #
25
+ def inject_into_file(destination, *args, &block)
26
+ if block_given?
27
+ data, flag = block, args.shift
28
+ else
29
+ data, flag = args.shift, args.shift
30
+ end
31
+
32
+ log_status = args.empty? || args.pop
33
+ action InjectIntoFile.new(self, destination, data, flag, log_status)
34
+ end
35
+
36
+ class InjectIntoFile #:nodoc:
37
+ attr_reader :base, :destination, :relative_destination, :flag, :replacement
38
+
39
+ def initialize(base, destination, data, flag, log_status=true)
40
+ @base, @log_status = base, log_status
41
+ behavior, @flag = flag.keys.first, flag.values.first
42
+
43
+ self.destination = destination
44
+ data = data.call if data.is_a?(Proc)
45
+
46
+ @replacement = if behavior == :after
47
+ @flag + data
48
+ else
49
+ data + @flag
50
+ end
51
+ end
52
+
53
+ def invoke!
54
+ say_status :inject
55
+ replace!(flag, replacement)
56
+ end
57
+
58
+ def revoke!
59
+ say_status :deinject
60
+ replace!(replacement, flag)
61
+ end
62
+
63
+ protected
64
+
65
+ # Sets the destination value from a relative destination value. The
66
+ # relative destination is kept to be used in output messages.
67
+ #
68
+ def destination=(destination)
69
+ if destination
70
+ @destination = ::File.expand_path(destination.to_s, base.destination_root)
71
+ @relative_destination = base.relative_to_absolute_root(@destination)
72
+ end
73
+ end
74
+
75
+ # Shortcut to say_status base method.
76
+ #
77
+ def say_status(status)
78
+ base.send :say_status_if_log, status, relative_destination, @log_status
79
+ end
80
+
81
+ # Adds the content to the file.
82
+ #
83
+ def replace!(regexp, string)
84
+ unless base.options[:pretend]
85
+ content = File.read(destination)
86
+ content.gsub!(regexp, string)
87
+ File.open(destination, 'wb') { |file| file.write(content) }
88
+ end
89
+ end
90
+
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,37 @@
1
+ require 'thor/actions/templater'
2
+ require 'erb'
3
+
4
+ class Thor
5
+ module Actions
6
+
7
+ # Gets an ERB template at the relative source, executes it and makes a copy
8
+ # at the relative destination. If the destination is not given it's assumed
9
+ # to be equal to the source.
10
+ #
11
+ # ==== Parameters
12
+ # source<String>:: the relative path to the source root
13
+ # destination<String>:: the relative path to the destination root
14
+ # log_status<Boolean>:: if false, does not log the status. True by default.
15
+ #
16
+ # ==== Examples
17
+ #
18
+ # template "README", "doc/README"
19
+ #
20
+ # template "doc/README"
21
+ #
22
+ def template(source, destination=nil, log_status=true)
23
+ action Template.new(self, source, destination || source, log_status)
24
+ end
25
+
26
+ class Template < Templater #:nodoc:
27
+
28
+ def render
29
+ @render ||= begin
30
+ context = base.instance_eval('binding')
31
+ ERB.new(::File.read(source), nil, '-').result(context)
32
+ end
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,163 @@
1
+ class Thor
2
+ module Actions
3
+
4
+ # This is the base class for templater actions, ie. that copies something
5
+ # from some directory (source) to another (destination).
6
+ #
7
+ # This implementation is completely based in Templater actions, created
8
+ # by Jonas Nicklas and Michael S. Klishin under MIT LICENSE.
9
+ #
10
+ class Templater #:nodoc:
11
+ attr_reader :base, :source, :destination, :relative_destination
12
+
13
+ # Initializes given the source and destination.
14
+ #
15
+ # ==== Parameters
16
+ # base<Thor::Base>:: A Thor::Base instance
17
+ # source<String>:: Relative path to the source of this file
18
+ # destination<String>:: Relative path to the destination of this file
19
+ # log_status<Boolean>:: If false, does not log the status. True by default.
20
+ # Templater log status does not accept color.
21
+ #
22
+ def initialize(base, source, destination, log_status=true)
23
+ @base, @log_status = base, log_status
24
+ self.source = source
25
+ self.destination = destination
26
+ end
27
+
28
+ # Returns the contents of the source file as a String. If render is
29
+ # available, a diff option is shown in the file collision menu.
30
+ #
31
+ # ==== Returns
32
+ # String:: The source file.
33
+ #
34
+ # def render
35
+ # end
36
+
37
+ # Checks if the destination file already exists.
38
+ #
39
+ # ==== Returns
40
+ # Boolean:: true if the file exists, false otherwise.
41
+ #
42
+ def exists?
43
+ ::File.exists?(destination)
44
+ end
45
+
46
+ # Checks if the content of the file at the destination is identical to the rendered result.
47
+ #
48
+ # ==== Returns
49
+ # Boolean:: true if it is identical, false otherwise.
50
+ #
51
+ def identical?
52
+ exists? && (is_not_comparable? || ::File.read(destination) == render)
53
+ end
54
+
55
+ # Invokes the action. By default it adds to the file the content rendered,
56
+ # but you can modify in the subclass.
57
+ #
58
+ def invoke!
59
+ invoke_with_options!(base.options) do
60
+ ::FileUtils.mkdir_p(::File.dirname(destination))
61
+ ::File.open(destination, 'w'){ |f| f.write render }
62
+ end
63
+ end
64
+
65
+ # Revokes the action.
66
+ #
67
+ def revoke!
68
+ say_status :deleted, :green
69
+ ::FileUtils.rm_rf(destination) unless pretend?
70
+ end
71
+
72
+ protected
73
+
74
+ # Shortcut for pretend.
75
+ #
76
+ def pretend?
77
+ base.options[:pretend]
78
+ end
79
+
80
+ # A templater is comparable if responds to render. In such cases, we have
81
+ # to show the conflict menu to the user unless the files are identical.
82
+ #
83
+ def is_not_comparable?
84
+ !respond_to?(:render)
85
+ end
86
+
87
+ # Sets the source value from a relative source value.
88
+ #
89
+ def source=(source)
90
+ if source
91
+ @source = ::File.expand_path(source.to_s, base.source_root)
92
+ end
93
+ end
94
+
95
+ # Sets the destination value from a relative destination value. The
96
+ # relative destination is kept to be used in output messages.
97
+ #
98
+ def destination=(destination)
99
+ if destination
100
+ @destination = ::File.expand_path(destination.to_s, base.destination_root)
101
+ @relative_destination = base.relative_to_absolute_root(@destination)
102
+ end
103
+ end
104
+
105
+ # Receives a hash of options and just execute the block if some
106
+ # conditions are met.
107
+ #
108
+ def invoke_with_options!(options, &block)
109
+ if exists?
110
+ if is_not_comparable?
111
+ say_status :exist, :blue
112
+ elsif identical?
113
+ say_status :identical, :blue
114
+ else
115
+ force_or_skip_or_conflict(options[:force], options[:skip], &block)
116
+ end
117
+ else
118
+ say_status :create, :green
119
+ block.call unless pretend?
120
+ end
121
+ end
122
+
123
+ # If force is true, run the action, otherwise check if it's not being
124
+ # skipped. If both are false, show the file_collision menu, if the menu
125
+ # returns true, force it, otherwise skip.
126
+ #
127
+ def force_or_skip_or_conflict(force, skip, &block)
128
+ if force
129
+ say_status :force, :yellow
130
+ block.call unless pretend?
131
+ elsif skip
132
+ say_status :skip, :yellow
133
+ else
134
+ say_status :conflict, :red
135
+ force_or_skip_or_conflict(force_on_collision?, true, &block)
136
+ end
137
+ end
138
+
139
+ # Shows the file collision menu to the user and gets the result.
140
+ #
141
+ def force_on_collision?
142
+ base.shell.file_collision(destination){ render }
143
+ end
144
+
145
+ # Shortcut to say_status shell method.
146
+ #
147
+ def say_status(status, color)
148
+ base.shell.say_status status, relative_destination, color if @log_status
149
+ end
150
+
151
+ # TODO Add this behavior to all actions.
152
+ #
153
+ def after_invoke
154
+ # Optionally change permissions.
155
+ FileUtils.chmod(base.options[:chmod], destination) if base.options[:chmod]
156
+
157
+ # Optionally add file to subversion or git
158
+ system("git add -v #{relative_destination}") if options[:git]
159
+ end
160
+
161
+ end
162
+ end
163
+ end
data/lib/thor/base.rb ADDED
@@ -0,0 +1,447 @@
1
+ require 'thor/core_ext/hash_with_indifferent_access'
2
+ require 'thor/core_ext/ordered_hash'
3
+ require 'thor/shell/basic'
4
+ require 'thor/error'
5
+ require 'thor/options'
6
+ require 'thor/task'
7
+ require 'thor/util'
8
+
9
+ class Thor
10
+ HELP_MAPPINGS = ["-h", "-?", "--help", "-D"]
11
+
12
+ class Maxima < Struct.new(:usage, :options, :class_options)
13
+ end
14
+
15
+ module Base
16
+
17
+ def self.included(base) #:nodoc:
18
+ base.send :extend, ClassMethods
19
+ base.send :include, SingletonMethods
20
+ end
21
+
22
+ # Returns the classes that inherits from Thor or Thor::Group.
23
+ #
24
+ # ==== Returns
25
+ # Array[Class]
26
+ #
27
+ def self.subclasses
28
+ @subclasses ||= []
29
+ end
30
+
31
+ # Returns the files where the subclasses are kept.
32
+ #
33
+ # ==== Returns
34
+ # Hash[path<String> => Class]
35
+ #
36
+ def self.subclass_files
37
+ @subclass_files ||= Hash.new{ |h,k| h[k] = [] }
38
+ end
39
+
40
+ # Returns the shell used in all Thor classes.
41
+ #
42
+ def self.shell
43
+ @shell || Thor::Shell::Basic
44
+ end
45
+
46
+ # Sets the shell used in all Thor classes.
47
+ #
48
+ def self.shell=(klass)
49
+ @shell = klass
50
+ end
51
+
52
+ # Whenever a class inherits from Thor or Thor::Group, we should track the
53
+ # class and the file on Thor::Base. This is the method responsable for it.
54
+ #
55
+ def self.register_klass_file(klass) #:nodoc:
56
+ file = caller[1].match(/(.*):\d+/)[1]
57
+ Thor::Base.subclasses << klass unless Thor::Base.subclasses.include?(klass)
58
+
59
+ file_subclasses = Thor::Base.subclass_files[File.expand_path(file)]
60
+ file_subclasses << klass unless file_subclasses.include?(klass)
61
+ end
62
+
63
+ module ClassMethods
64
+ # Adds an argument to the class and creates an attr_accessor for it.
65
+ #
66
+ # Arguments are different from options in several aspects. The first one
67
+ # is how they are parsed from the command line, arguments are retrieved
68
+ # from position:
69
+ #
70
+ # thor task NAME
71
+ #
72
+ # Instead of:
73
+ #
74
+ # thor task --name=NAME
75
+ #
76
+ # Besides, arguments are used inside your code as an accessor (self.argument),
77
+ # while options are all kept in a hash (self.options).
78
+ #
79
+ # Finally, arguments cannot have type :default or :boolean but can be
80
+ # optional (supplying :optional => :true or :required => false), although
81
+ # you cannot have a required argument after a non-required argument. If you
82
+ # try it, an error is raised.
83
+ #
84
+ # ==== Parameters
85
+ # name<Symbol>:: The name of the argument.
86
+ # options<Hash>:: The description, type, default value for this argument.
87
+ # The type can be :string, :numeric, :hash or :array. If none, string is assumed.
88
+ #
89
+ # ==== Errors
90
+ # ArgumentError:: Raised if you supply a required argument after a non required one.
91
+ #
92
+ def argument(name, options={})
93
+ no_tasks { attr_accessor name }
94
+
95
+ required = if options.key?(:optional)
96
+ !options[:optional]
97
+ elsif options.key?(:required)
98
+ options[:required]
99
+ else
100
+ options[:default].nil?
101
+ end
102
+
103
+ class_options.values.each do |option|
104
+ next unless option.argument? && !option.required?
105
+ raise ArgumentError, "You cannot have #{name.to_s.inspect} as required argument after " <<
106
+ "the non-required argument #{option.human_name.inspect}."
107
+ end if required
108
+
109
+ class_options[name] = Thor::Argument.new(name, options[:desc], required,
110
+ options[:type], options[:default])
111
+ end
112
+
113
+ # Returns this class arguments, looking up in the ancestors chain.
114
+ #
115
+ # ==== Returns
116
+ # Array[Thor::Argument]
117
+ #
118
+ def arguments
119
+ class_options.values.select{ |o| o.argument? }
120
+ end
121
+
122
+ # Adds a bunch of options to the set of class options.
123
+ #
124
+ # class_options :foo => :optional, :bar => :required, :baz => :string
125
+ #
126
+ # If you prefer more detailed declaration, check class_option.
127
+ #
128
+ # ==== Parameters
129
+ # Hash[Symbol => Object]
130
+ #
131
+ def class_options(options=nil)
132
+ @class_options ||= from_superclass(:class_options, Thor::CoreExt::OrderedHash.new)
133
+ build_options(options, @class_options) if options
134
+ @class_options
135
+ end
136
+
137
+ # Adds an option to the set of class options
138
+ #
139
+ # ==== Parameters
140
+ # name<Symbol>:: The name of the argument.
141
+ # options<Hash>:: The description, type, default value, aliases and if this option is required or not.
142
+ # The type can be :string, :boolean, :numeric, :hash or :array. If none is given
143
+ # a default type which accepts both (--name and --name=NAME) entries is assumed.
144
+ #
145
+ def class_option(name, options)
146
+ build_option(name, options, class_options)
147
+ end
148
+
149
+ # Defines the group. This is used when thor list is invoked so you can specify
150
+ # that only tasks from a pre-defined group will be shown. Defaults to standard.
151
+ #
152
+ # ==== Parameters
153
+ # name<String|Symbol>
154
+ #
155
+ def group(name)
156
+ @group_name = name.to_s
157
+ end
158
+
159
+ # Returns the group name.
160
+ #
161
+ # ==== Returns
162
+ # String
163
+ #
164
+ def group_name
165
+ @group_name ||= from_superclass(:group_name, 'standard')
166
+ end
167
+
168
+ # Returns the tasks for this Thor class.
169
+ #
170
+ # ==== Returns
171
+ # OrderedHash:: An ordered hash with this class tasks.
172
+ #
173
+ def tasks
174
+ @tasks ||= Thor::CoreExt::OrderedHash.new
175
+ end
176
+
177
+ # Returns the tasks for this Thor class and all subclasses.
178
+ #
179
+ # ==== Returns
180
+ # OrderedHash
181
+ #
182
+ def all_tasks
183
+ @all_tasks ||= from_superclass(:all_tasks, Thor::CoreExt::OrderedHash.new)
184
+ @all_tasks.merge(tasks)
185
+ end
186
+
187
+ # Removes a given task from this Thor class. This is usually done if you
188
+ # are inheriting from another class and don't want it to be available
189
+ # anymore.
190
+ #
191
+ # By default it only remove the mapping to the task. But you can supply
192
+ # :undefine => true to undefine the method from the class as well.
193
+ #
194
+ # ==== Parameters
195
+ # name<Symbol|String>:: The name of the task to be removed
196
+ # options<Hash>:: You can give :undefine => true if you want tasks the method
197
+ # to be undefined from the class as well.
198
+ #
199
+ def remove_task(*names)
200
+ options = names.last.is_a?(Hash) ? names.pop : {}
201
+
202
+ names.each do |name|
203
+ tasks.delete(name.to_s)
204
+ all_tasks.delete(name.to_s)
205
+ undef_method name if options[:undefine]
206
+ end
207
+ end
208
+
209
+ # Retrieve a specific task from this Thor class. If the desired Task cannot
210
+ # be found, returns a dynamic Thor::Task that will map to the given method.
211
+ #
212
+ # ==== Parameters
213
+ # meth<Symbol>:: the name of the task to be retrieved
214
+ #
215
+ # ==== Returns
216
+ # Task
217
+ #
218
+ def [](meth)
219
+ all_tasks[meth.to_s] || Thor::Task.dynamic(meth)
220
+ end
221
+
222
+ # All methods defined inside the given block are not added as tasks.
223
+ #
224
+ # So you can do:
225
+ #
226
+ # class MyScript < Thor
227
+ # no_tasks do
228
+ # def this_is_not_a_task
229
+ # end
230
+ # end
231
+ # end
232
+ #
233
+ # You can also add the method and remove it from the task list:
234
+ #
235
+ # class MyScript < Thor
236
+ # def this_is_not_a_task
237
+ # end
238
+ # remove_task :this_is_not_a_task
239
+ # end
240
+ #
241
+ def no_tasks
242
+ @no_tasks = true
243
+ yield
244
+ @no_tasks = false
245
+ end
246
+
247
+ # Sets the namespace for the Thor or Thor::Group class. By default the
248
+ # namespace is retrieved from the class name. If your Thor class is named
249
+ # Scripts::MyScript, the help method, for example, will be called as:
250
+ #
251
+ # thor scripts:my_script -h
252
+ #
253
+ # If you change the namespace:
254
+ #
255
+ # namespace :my_scripts
256
+ #
257
+ # You change how your tasks are invoked:
258
+ #
259
+ # thor my_scripts -h
260
+ #
261
+ # Finally, if you change your namespace to default:
262
+ #
263
+ # namespace :default
264
+ #
265
+ # Your tasks can be invoked with a shortcut. Instead of:
266
+ #
267
+ # thor :my_task
268
+ #
269
+ def namespace(name=nil)
270
+ case name
271
+ when nil
272
+ @namespace ||= Thor::Util.constant_to_namespace(self, false)
273
+ else
274
+ @namespace = name.to_s
275
+ end
276
+ end
277
+
278
+ protected
279
+
280
+ # Build an option and adds it to the given scope.
281
+ #
282
+ # ==== Parameters
283
+ # name<Symbol>:: The name of the argument.
284
+ # options<Hash>:: The desc, type, default value and aliases for this option.
285
+ # The type can be :string, :boolean, :numeric, :hash or :array. If none is given
286
+ # a default type which accepts both (--name and --name=NAME) entries is assumed.
287
+ #
288
+ def build_option(name, options, scope)
289
+ scope[name] = Thor::Option.new(name, options[:desc], options[:required],
290
+ options[:type], options[:default], options[:aliases])
291
+ end
292
+
293
+ # Receives a hash of options, parse them and add to the scope. This is a
294
+ # fast way to set a bunch of options:
295
+ #
296
+ # build_options :foo => :optional, :bar => :required, :baz => :string
297
+ #
298
+ # ==== Parameters
299
+ # Hash[Symbol => Object]
300
+ #
301
+ def build_options(options, scope)
302
+ options.each do |key, value|
303
+ scope[key] = Thor::Option.parse(key, value)
304
+ end
305
+ end
306
+
307
+ # Finds a task with the given name. If the task belongs to the current
308
+ # class, just return it, otherwise dup it and add the fresh copy to the
309
+ # current task hash.
310
+ #
311
+ def find_and_refresh_task(name)
312
+ task = if task = tasks[name.to_s]
313
+ task
314
+ elsif task = all_tasks[name.to_s]
315
+ tasks[name.to_s] = task.clone
316
+ else
317
+ raise ArgumentError, "You supplied :for => #{name.inspect}, but the task #{name.inspect} could not be found."
318
+ end
319
+ end
320
+
321
+ # Everytime someone inherits from a Thor class, register the klass
322
+ # and file into baseclass.
323
+ #
324
+ def inherited(klass)
325
+ Thor::Base.register_klass_file(klass)
326
+ end
327
+
328
+ # Fire this callback whenever a method is added. Added methods are
329
+ # tracked as tasks if the requirements set by valid_task? are valid.
330
+ #
331
+ def method_added(meth)
332
+ meth = meth.to_s
333
+
334
+ if meth == "initialize"
335
+ initialize_added
336
+ return
337
+ end
338
+
339
+ return if @no_tasks || !valid_task?(meth)
340
+ Thor::Base.register_klass_file(self)
341
+ create_task(meth)
342
+ end
343
+
344
+ def from_superclass(method, default=nil)
345
+ self == baseclass ? default : superclass.send(method).dup
346
+ end
347
+
348
+ # SIGNATURE: Sets the baseclass. This is where the superclass lookup
349
+ # finishes.
350
+ def baseclass #:nodoc:
351
+ end
352
+
353
+ # SIGNATURE: Defines if a given method is a valid_task?. This method is
354
+ # called before a new method is added to the class.
355
+ def valid_task?(meth) #:nodoc:
356
+ end
357
+
358
+ # SIGNATURE: Creates a new task if valid_task? is true. This method is
359
+ # called when a new method is added to the class.
360
+ def create_task(meth) #:nodoc:
361
+ end
362
+
363
+ # SIGNATURE: Defines behavior when the initialize method is added to the
364
+ # class.
365
+ def initialize_added #:nodoc:
366
+ end
367
+ end
368
+
369
+ module SingletonMethods
370
+ attr_accessor :options
371
+
372
+ SHELL_DELEGATED_METHODS = [:ask, :yes?, :no?, :say, :say_status, :print_list, :print_table]
373
+
374
+ # It receives arguments in an Array and two hashes, one for options and
375
+ # other for configuration.
376
+ #
377
+ # Notice that it does not check arguments type neither if all required
378
+ # arguments were supplied. It should be done by the parser.
379
+ #
380
+ # ==== Parameters
381
+ # args<Array[Object]>:: An array of objects. The objects are applied to their
382
+ # respective accessors declared with <tt>argument</tt>.
383
+ #
384
+ # options<Hash>:: An options hash that will be available as self.options.
385
+ # The hash given is converted to a hash with indifferent
386
+ # access, magic predicates (options.skip?) and then frozen.
387
+ #
388
+ # config<Hash>:: Configuration for this Thor class.
389
+ #
390
+ # ==== Configuration
391
+ # shell<Object>:: An instance of the shell to be used.
392
+ #
393
+ # ==== Examples
394
+ #
395
+ # class MyScript < Thor
396
+ # argument :first, :type => :numeric
397
+ # end
398
+ #
399
+ # MyScript.new [1.0], { :foo => :bar }, :shell => Thor::Shell::Basic.new
400
+ #
401
+ def initialize(args=[], options={}, config={})
402
+ self.class.arguments.zip(args).each do |argument, value|
403
+ send("#{argument.human_name}=", value)
404
+ end
405
+
406
+ self.options = Thor::CoreExt::HashWithIndifferentAccess.new(options).freeze
407
+ self.shell = config[:shell]
408
+
409
+ # Add base to shell if an accessor is provided.
410
+ self.shell.base = self if self.shell.respond_to?(:base)
411
+ end
412
+
413
+ # Common methods that are delegated to the shell.
414
+ #
415
+ SHELL_DELEGATED_METHODS.each do |method|
416
+ module_eval <<-METHOD, __FILE__, __LINE__
417
+ def #{method}(*args)
418
+ shell.#{method}(*args)
419
+ end
420
+ METHOD
421
+ end
422
+
423
+ # Holds the shell for the given Thor instance. If no shell is given,
424
+ # it gets a default shell from Thor::Base.shell.
425
+ #
426
+ def shell
427
+ @shell ||= Thor::Base.shell.new
428
+ end
429
+
430
+ # Sets the shell for this thor class.
431
+ #
432
+ def shell=(shell)
433
+ @shell = shell
434
+ end
435
+
436
+ # Finds a task with the name given and invokes it with the given arguments.
437
+ # This is the default interface to invoke tasks. You can always run a task
438
+ # directly, but the invocation system will be implemented in a fashion
439
+ # that a same task cannot be invoked twice (a la rake).
440
+ #
441
+ def invoke(name, *args)
442
+ self.class[name].run(self, *args)
443
+ end
444
+ end
445
+
446
+ end
447
+ end