wycats-thor 0.11.0 → 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/thor.rb CHANGED
@@ -4,9 +4,7 @@ require 'thor/group'
4
4
  require 'thor/actions'
5
5
 
6
6
  class Thor
7
-
8
7
  class << self
9
-
10
8
  # Sets the default task when thor is executed without an explicit task to be called.
11
9
  #
12
10
  # ==== Parameters
@@ -108,7 +106,7 @@ class Thor
108
106
  # :group - The group for this options. Use by class options to output options in different levels.
109
107
  # :banner - String to show on usage notes.
110
108
  #
111
- def method_option(name, options)
109
+ def method_option(name, options={})
112
110
  scope = if options[:for]
113
111
  find_and_refresh_task(options[:for]).options
114
112
  else
@@ -220,7 +218,6 @@ class Thor
220
218
  meth = mapping || meth || default_task
221
219
  meth.to_s.gsub('-','_') # treat foo-bar > foo_bar
222
220
  end
223
-
224
221
  end
225
222
 
226
223
  include Thor::Base
data/lib/thor/actions.rb CHANGED
@@ -5,17 +5,13 @@ Dir[File.join(File.dirname(__FILE__), "actions", "*.rb")].each do |action|
5
5
  end
6
6
 
7
7
  class Thor
8
- # Some actions require that a class method called source root is defined in
9
- # the class. Remember to always cache the source root value, because Ruby
10
- # __FILE__ always return the relative path, which may lead to mistakes if you
11
- # are calling an action inside the "inside(path)" method.
12
- #
13
8
  module Actions
14
9
  attr_accessor :behavior
15
10
 
16
11
  # On inclusion, add some options to base.
17
12
  #
18
13
  def self.included(base) #:nodoc:
14
+ base.extend ClassMethods
19
15
  return unless base.respond_to?(:class_option)
20
16
 
21
17
  base.class_option :pretend, :type => :boolean, :aliases => "-p", :group => :runtime,
@@ -31,6 +27,32 @@ class Thor
31
27
  :desc => "Supress status output"
32
28
  end
33
29
 
30
+ module ClassMethods
31
+ # Hold source paths for one Thor instance. source_paths_for_search is the
32
+ # method responsible to gather source_paths from this current class,
33
+ # inherited paths and the source root.
34
+ #
35
+ def source_paths
36
+ @source_paths ||= []
37
+ end
38
+
39
+ # Returns the source paths in the following order:
40
+ #
41
+ # 1) This class source paths
42
+ # 2) Source root
43
+ # 3) Parents source paths
44
+ #
45
+ def source_paths_for_search
46
+ @source_paths_for_search ||= begin
47
+ paths = []
48
+ paths += self.source_paths
49
+ paths << self.source_root if self.respond_to?(:source_root)
50
+ paths += from_superclass(:source_paths, [])
51
+ paths
52
+ end
53
+ end
54
+ end
55
+
34
56
  # Extends initializer to add more configuration options.
35
57
  #
36
58
  # ==== Configuration
@@ -38,8 +60,8 @@ class Thor
38
60
  # It also accepts :force, :skip and :pretend to set the behavior
39
61
  # and the respective option.
40
62
  #
41
- # root<String>:: The root directory needed for some actions. It's also known
42
- # as destination root.
63
+ # destination_root<String>:: The root directory needed for some actions. It's also known
64
+ # as destination root.
43
65
  #
44
66
  def initialize(args=[], options={}, config={})
45
67
  self.behavior = case config[:behavior].to_s
@@ -53,7 +75,7 @@ class Thor
53
75
  end
54
76
 
55
77
  super
56
- self.root = config[:root]
78
+ self.destination_root = config[:destination_root]
57
79
  end
58
80
 
59
81
  # Wraps an action object and call it accordingly to the thor class behavior.
@@ -68,44 +90,43 @@ class Thor
68
90
 
69
91
  # Returns the root for this thor class (also aliased as destination root).
70
92
  #
71
- def root
72
- @root_stack.last
93
+ def destination_root
94
+ @destination_stack.last
73
95
  end
74
- alias :destination_root :root
75
96
 
76
97
  # Sets the root for this thor class. Relatives path are added to the
77
98
  # directory where the script was invoked and expanded.
78
99
  #
79
- def root=(root)
80
- @root_stack ||= []
81
- @root_stack[0] = File.expand_path(root || '')
82
- end
83
-
84
- # Gets the current root relative to the absolute root.
85
- #
86
- # inside "foo" do
87
- # relative_root #=> "foo"
88
- # end
89
- #
90
- def relative_root(remove_dot=true)
91
- relative_to_absolute_root(root, remove_dot)
100
+ def destination_root=(root)
101
+ @destination_stack ||= []
102
+ @destination_stack[0] = File.expand_path(root || '')
92
103
  end
93
104
 
94
105
  # Returns the given path relative to the absolute root (ie, root where
95
106
  # the script started).
96
107
  #
97
- def relative_to_absolute_root(path, remove_dot=true)
98
- path = path.gsub(@root_stack[0], '.')
108
+ def relative_to_original_destination_root(path, remove_dot=true)
109
+ path = path.gsub(@destination_stack[0], '.')
99
110
  remove_dot ? (path[2..-1] || '') : path
100
111
  end
101
112
 
102
- # Get the source root in the class. Raises an error if a source root is
103
- # not specified in the thor class.
113
+ # Receives a file or directory and search for it in the source paths.
104
114
  #
105
- def source_root
106
- self.class.source_root
107
- rescue NoMethodError => e
108
- raise NoMethodError, "You have to specify the class method source_root in your thor class."
115
+ def find_in_source_paths(file)
116
+ relative_root = relative_to_original_destination_root(destination_root, false)
117
+ paths = self.class.source_paths_for_search
118
+
119
+ paths.each do |source|
120
+ source_file = File.expand_path(file, File.join(source, relative_root))
121
+ return source_file if File.exists?(source_file)
122
+ end
123
+
124
+ if paths.empty?
125
+ raise Error, "You don't have any source path defined for class #{self.class.name}. To fix this, " <<
126
+ "you can define a source_root in your class."
127
+ else
128
+ raise Error, "Could not find #{file.inspect} in source paths."
129
+ end
109
130
  end
110
131
 
111
132
  # Do something in the root or on a provided subfolder. If a relative path
@@ -117,16 +138,25 @@ class Thor
117
138
  # dir<String>:: the directory to move to.
118
139
  #
119
140
  def inside(dir='', &block)
120
- @root_stack.push File.expand_path(dir, root)
121
- FileUtils.mkdir_p(root) unless File.exist?(root)
122
- FileUtils.cd(root) { block.arity == 1 ? yield(root) : yield }
123
- @root_stack.pop
141
+ @destination_stack.push File.expand_path(dir, destination_root)
142
+ FileUtils.mkdir_p(destination_root) unless File.exist?(destination_root)
143
+ FileUtils.cd(destination_root) { block.arity == 1 ? yield(destination_root) : yield }
144
+ @destination_stack.pop
145
+ end
146
+
147
+ # Same as inside, but log status and use padding.
148
+ #
149
+ def inside_with_padding(dir='', config={}, &block)
150
+ say_status :inside, dir, config.fetch(:verbose, true)
151
+ shell.padding += 1
152
+ inside(dir, &block)
153
+ shell.padding -= 1
124
154
  end
125
155
 
126
156
  # Goes to the root and execute the given block.
127
157
  #
128
158
  def in_root
129
- inside(@root_stack.first) { yield }
159
+ inside(@destination_stack.first) { yield }
130
160
  end
131
161
 
132
162
  # Changes the mode of the given file or directory.
@@ -134,17 +164,16 @@ class Thor
134
164
  # ==== Parameters
135
165
  # mode<Integer>:: the file mode
136
166
  # path<String>:: the name of the file to change mode
137
- # log_status<Boolean>:: if false, does not log the status. True by default.
138
- # If a symbol is given, uses it as the output color.
167
+ # config<Hash>:: give :verbose => false to not log the status.
139
168
  #
140
169
  # ==== Example
141
170
  #
142
171
  # chmod "script/*", 0755
143
172
  #
144
- def chmod(path, mode, log_status=true)
173
+ def chmod(path, mode, config={})
145
174
  return unless behavior == :invoke
146
- path = File.expand_path(path, root)
147
- say_status :chmod, relative_to_absolute_root(path), log_status
175
+ path = File.expand_path(path, destination_root)
176
+ say_status :chmod, relative_to_original_destination_root(path), config.fetch(:verbose, true)
148
177
  FileUtils.chmod_R(mode, path) unless options[:pretend]
149
178
  end
150
179
 
@@ -152,8 +181,7 @@ class Thor
152
181
  #
153
182
  # ==== Parameters
154
183
  # command<String>:: the command to be executed.
155
- # log_status<Boolean>:: if false, does not log the status. True by default.
156
- # If a symbol is given, uses it as the output color.
184
+ # config<Hash>:: give :verbose => false to not log the status.
157
185
  #
158
186
  # ==== Example
159
187
  #
@@ -161,9 +189,10 @@ class Thor
161
189
  # run('ln -s ~/edge rails')
162
190
  # end
163
191
  #
164
- def run(command, log_status=true)
192
+ def run(command, config={})
165
193
  return unless behavior == :invoke
166
- say_status :run, "\"#{command}\" from #{relative_to_absolute_root(root, false)}", log_status
194
+ description = "#{command.inspect} from #{relative_to_original_destination_root(destination_root, false)}"
195
+ say_status :run, description, config.fetch(:verbose, true)
167
196
  `#{command}` unless options[:pretend]
168
197
  end
169
198
 
@@ -171,12 +200,11 @@ class Thor
171
200
  #
172
201
  # ==== Parameters
173
202
  # command<String>:: the command to be executed.
174
- # log_status<Boolean>:: if false, does not log the status. True by default.
175
- # If a symbol is given, uses it as the output color.
203
+ # config<Hash>:: give :verbose => false to not log the status.
176
204
  #
177
- def run_ruby_script(command, log_status=true)
205
+ def run_ruby_script(command, config={})
178
206
  return unless behavior == :invoke
179
- say_status File.basename(Thor::Util.ruby_command), command, log_status
207
+ say_status File.basename(Thor::Util.ruby_command), command, config.fetch(:verbose, true)
180
208
  `#{Thor::Util.ruby_command} #{command}` unless options[:pretend]
181
209
  end
182
210
 
@@ -186,9 +214,8 @@ class Thor
186
214
  # ==== Parameters
187
215
  # task<String>:: the task to be invoked
188
216
  # args<Array>:: arguments to the task
189
- # options<Hash>:: a hash with options used on invocation
190
- # log_status<Boolean>:: if false, does not log the status. True by default.
191
- # If a symbol is given, uses it as the output color.
217
+ # options<Hash>:: give :verbose => false to not log the status. Other options
218
+ # are given as parameter to Thor.
192
219
  #
193
220
  # ==== Examples
194
221
  #
@@ -199,35 +226,33 @@ class Thor
199
226
  # #=> thor list --all --substring=rails
200
227
  #
201
228
  def thor(task, *args)
202
- log_status = args.last.is_a?(Symbol) || [true, false].include?(args.last) ? args.pop : true
203
- options = args.last.is_a?(Hash) ? args.pop : {}
229
+ config = args.last.is_a?(Hash) ? args.pop : {}
230
+ verbose = config.key?(:verbose) ? config.delete(:verbose) : true
204
231
 
205
232
  args.unshift task
206
- args.push Thor::Options.to_switches(options)
233
+ args.push Thor::Options.to_switches(config)
207
234
  command = args.join(' ').strip
208
235
 
209
- say_status :thor, command, log_status
210
- run "thor #{command}", false
236
+ say_status :thor, command, verbose
237
+ run "thor #{command}", :verbose => false
211
238
  end
212
239
 
213
240
  # Removes a file at the given location.
214
241
  #
215
242
  # ==== Parameters
216
243
  # path<String>:: path of the file to be changed
217
- # log_status<Boolean>:: if false, does not log the status. True by default.
218
- # If a symbol is given, uses it as the output color.
244
+ # config<Hash>:: give :verbose => false to not log the status.
219
245
  #
220
246
  # ==== Example
221
247
  #
222
248
  # remove_file 'README'
223
249
  # remove_file 'app/controllers/application_controller.rb'
224
250
  #
225
- def remove_file(path, log_status=true)
251
+ def remove_file(path, config={})
226
252
  return unless behavior == :invoke
227
- path = File.expand_path(path, root)
228
- color = log_status.is_a?(Symbol) ? log_status : :red
253
+ path = File.expand_path(path, destination_root)
229
254
 
230
- say_status :remove, relative_to_absolute_root(path), log_status
255
+ say_status :remove, relative_to_original_destination_root(path), config.fetch(:verbose, true)
231
256
  ::FileUtils.rm_rf(path) if !options[:pretend] && File.exists?(path)
232
257
  end
233
258
 
@@ -237,8 +262,7 @@ class Thor
237
262
  # path<String>:: path of the file to be changed
238
263
  # flag<Regexp|String>:: the regexp or string to be replaced
239
264
  # replacement<String>:: the replacement, can be also given as a block
240
- # log_status<Boolean>:: if false, does not log the status. True by default.
241
- # If a symbol is given, uses it as the output color.
265
+ # config<Hash>:: give :verbose => false to not log the status.
242
266
  #
243
267
  # ==== Example
244
268
  #
@@ -250,10 +274,10 @@ class Thor
250
274
  #
251
275
  def gsub_file(path, flag, *args, &block)
252
276
  return unless behavior == :invoke
253
- log_status = args.last.is_a?(Symbol) || [ true, false ].include?(args.last) ? args.pop : true
277
+ config = args.last.is_a?(Hash) ? args.pop : {}
254
278
 
255
- path = File.expand_path(path, root)
256
- say_status :gsub, relative_to_absolute_root(path), log_status
279
+ path = File.expand_path(path, destination_root)
280
+ say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
257
281
 
258
282
  unless options[:pretend]
259
283
  content = File.read(path)
@@ -267,17 +291,16 @@ class Thor
267
291
  # ==== Parameters
268
292
  # path<String>:: path of the file to be changed
269
293
  # data<String>:: the data to append to the file, can be also given as a block.
270
- # log_status<Boolean>:: if false, does not log the status. True by default.
271
- # If a symbol is given, uses it as the output color.
294
+ # config<Hash>:: give :verbose => false to not log the status.
272
295
  #
273
296
  # ==== Example
274
297
  #
275
298
  # append_file 'config/environments/test.rb', 'config.gem "rspec"'
276
299
  #
277
- def append_file(path, data=nil, log_status=true, &block)
300
+ def append_file(path, data=nil, config={}, &block)
278
301
  return unless behavior == :invoke
279
- path = File.expand_path(path, root)
280
- say_status :append, relative_to_absolute_root(path), log_status
302
+ path = File.expand_path(path, destination_root)
303
+ say_status :append, relative_to_original_destination_root(path), config.fetch(:verbose, true)
281
304
  File.open(path, 'ab') { |file| file.write(data || block.call) } unless options[:pretend]
282
305
  end
283
306
 
@@ -286,17 +309,16 @@ class Thor
286
309
  # ==== Parameters
287
310
  # path<String>:: path of the file to be changed
288
311
  # data<String>:: the data to prepend to the file, can be also given as a block.
289
- # log_status<Boolean>:: if false, does not log the status. True by default.
290
- # If a symbol is given, uses it as the output color.
312
+ # config<Hash>:: give :verbose => false to not log the status.
291
313
  #
292
314
  # ==== Example
293
315
  #
294
316
  # prepend_file 'config/environments/test.rb', 'config.gem "rspec"'
295
317
  #
296
- def prepend_file(path, data=nil, log_status=true, &block)
318
+ def prepend_file(path, data=nil, config={}, &block)
297
319
  return unless behavior == :invoke
298
- path = File.expand_path(path, root)
299
- say_status :prepend, relative_to_absolute_root(path), log_status
320
+ path = File.expand_path(path, destination_root)
321
+ say_status :prepend, relative_to_original_destination_root(path), config.fetch(:verbose, true)
300
322
 
301
323
  unless options[:pretend]
302
324
  content = data || block.call
@@ -310,7 +332,7 @@ class Thor
310
332
  # Allow current root to be shared between invocations.
311
333
  #
312
334
  def _shared_configuration #:nodoc:
313
- super.merge!(:root => self.root)
335
+ super.merge!(:destination_root => self.destination_root)
314
336
  end
315
337
 
316
338
  def _cleanup_options_and_set(options, key) #:nodoc:
@@ -7,9 +7,9 @@ class Thor
7
7
  # the destination is not given it's assumed to be equal to the source.
8
8
  #
9
9
  # ==== Parameters
10
- # source<String>:: the relative path to the source root
11
- # destination<String>:: the relative path to the destination root
12
- # log_status<Boolean>:: if false, does not log the status. True by default.
10
+ # source<String>:: the relative path to the source root.
11
+ # destination<String>:: the relative path to the destination root.
12
+ # config<Hash>:: give :verbose => false to not log the status.
13
13
  #
14
14
  # ==== Examples
15
15
  #
@@ -17,8 +17,8 @@ class Thor
17
17
  #
18
18
  # copy_file "doc/README"
19
19
  #
20
- def copy_file(source, destination=nil, log_status=true)
21
- action CopyFile.new(self, source, destination || source, log_status)
20
+ def copy_file(source, destination=nil, config={})
21
+ action CopyFile.new(self, source, destination || source, config)
22
22
  end
23
23
 
24
24
  class CopyFile < Templater #:nodoc:
@@ -9,7 +9,7 @@ class Thor
9
9
  # ==== Parameters
10
10
  # destination<String>:: the relative path to the destination root.
11
11
  # data<String|NilClass>:: the data to append to the file.
12
- # log_status<Boolean>:: if false, does not log the status. True by default.
12
+ # config<Hash>:: give :verbose => false to not log the status.
13
13
  #
14
14
  # ==== Examples
15
15
  #
@@ -20,8 +20,8 @@ class Thor
20
20
  #
21
21
  # create_file "config/apach.conf", "your apache config"
22
22
  #
23
- def create_file(destination, data=nil, log_status=true, &block)
24
- action CreateFile.new(self, destination, block || data.to_s, log_status)
23
+ def create_file(destination, data=nil, config={}, &block)
24
+ action CreateFile.new(self, destination, block || data.to_s, config)
25
25
  end
26
26
  alias :add_file :create_file
27
27
 
@@ -31,8 +31,8 @@ class Thor
31
31
  class CreateFile < Templater #:nodoc:
32
32
  attr_reader :data
33
33
 
34
- def initialize(base, destination, data, log_status)
35
- super(base, nil, destination, log_status)
34
+ def initialize(base, destination, data, config={})
35
+ super(base, nil, destination, config)
36
36
  @data = data
37
37
  end
38
38
 
@@ -30,31 +30,27 @@ class Thor
30
30
  # blog.rb
31
31
  #
32
32
  # ==== Parameters
33
- # source<String>:: the relative path to the source root
34
- # destination<String>:: the relative path to the destination root
35
- # recursive<Boolean>:: if the directory must be copied recursively, true by default
36
- # log_status<Boolean>:: if false, does not log the status. True by default.
33
+ # source<String>:: the relative path to the source root.
34
+ # destination<String>:: the relative path to the destination root.
35
+ # config<Hash>:: give :verbose => false to not log the status.
36
+ # If :recursive => false, does not look for paths recursively.
37
37
  #
38
38
  # ==== Examples
39
39
  #
40
40
  # directory "doc"
41
- # directory "doc", "docs", false
41
+ # directory "doc", "docs", :recursive => false
42
42
  #
43
- def directory(source, destination=nil, recursive=true, log_status=true)
44
- action Directory.new(self, source, destination || source, recursive, log_status)
43
+ def directory(source, destination=nil, config={})
44
+ action Directory.new(self, source, destination || source, config)
45
45
  end
46
46
 
47
47
  class Directory < Templater #:nodoc:
48
- attr_reader :recursive
49
-
50
- def initialize(base, source, destination=nil, recursive=true, log_status=true)
51
- @recursive = recursive
52
- super(base, source, destination, log_status)
48
+ def initialize(base, source, destination=nil, config={})
49
+ super(base, source, destination, { :recursive => true }.merge(config))
53
50
  end
54
51
 
55
52
  def invoke!
56
- raise "Source #{source.inspect} does not exist" unless File.exists?(source)
57
- base.empty_directory given_destination, @log_status
53
+ base.empty_directory given_destination, config
58
54
  execute!
59
55
  end
60
56
 
@@ -65,20 +61,20 @@ class Thor
65
61
  protected
66
62
 
67
63
  def execute!
68
- lookup = recursive ? File.join(source, '**') : source
64
+ lookup = config[:recursive] ? File.join(source, '**') : source
69
65
  lookup = File.join(lookup, '{*,.[a-z]*}')
70
66
 
71
67
  Dir[lookup].each do |file_source|
72
- file_destination = File.join(given_destination, file_source.gsub(source, '.'))
73
68
  next if File.directory?(file_source)
69
+ file_destination = File.join(given_destination, file_source.gsub(source, '.'))
74
70
 
75
71
  case file_source
76
72
  when /\.empty_directory$/
77
- base.empty_directory(File.dirname(file_destination), @log_status)
73
+ base.empty_directory(File.dirname(file_destination), config)
78
74
  when /\.tt$/
79
- base.template(file_source, file_destination[0..-4], @log_status)
75
+ base.template(file_source, file_destination[0..-4], config)
80
76
  else
81
- base.copy_file(file_source, file_destination, @log_status)
77
+ base.copy_file(file_source, file_destination, config)
82
78
  end
83
79
  end
84
80
  end
@@ -6,21 +6,21 @@ class Thor
6
6
  # Creates an empty directory.
7
7
  #
8
8
  # ==== Parameters
9
- # destination<String>:: the relative path to the destination root
10
- # log_status<Boolean>:: if false, does not log the status. True by default.
9
+ # destination<String>:: the relative path to the destination root.
10
+ # config<Hash>:: give :verbose => false to not log the status.
11
11
  #
12
12
  # ==== Examples
13
13
  #
14
14
  # empty_directory "doc"
15
15
  #
16
- def empty_directory(destination, log_status=true)
17
- action EmptyDirectory.new(self, nil, destination, log_status)
16
+ def empty_directory(destination, config={})
17
+ action EmptyDirectory.new(self, nil, destination, config)
18
18
  end
19
19
 
20
20
  class EmptyDirectory < Templater #:nodoc:
21
21
 
22
22
  def invoke!
23
- invoke_with_options!(base.options) do
23
+ invoke_with_options!(base.options.merge(config)) do
24
24
  ::FileUtils.mkdir_p(destination)
25
25
  end
26
26
  end
@@ -9,9 +9,9 @@ class Thor
9
9
  # the url is yielded and used as location.
10
10
  #
11
11
  # ==== Parameters
12
- # source<String>:: the address of the given content
13
- # destination<String>:: the relative path to the destination root
14
- # log_status<Boolean>:: if false, does not log the status. True by default.
12
+ # source<String>:: the address of the given content.
13
+ # destination<String>:: the relative path to the destination root.
14
+ # config<Hash>:: give :verbose => false to not log the status.
15
15
  #
16
16
  # ==== Examples
17
17
  #
@@ -21,8 +21,8 @@ class Thor
21
21
  # content.split("\n").first
22
22
  # end
23
23
  #
24
- def get(source, destination=nil, log_status=true, &block)
25
- action Get.new(self, source, block || destination, log_status)
24
+ def get(source, destination=nil, config={}, &block)
25
+ action Get.new(self, source, block || destination, config)
26
26
  end
27
27
 
28
28
  class Get < Templater #:nodoc:
@@ -9,9 +9,8 @@ class Thor
9
9
  # ==== Parameters
10
10
  # destination<String>:: Relative path to the destination root
11
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.
12
+ # config<Hash>:: give :verbose => false to not log the status and the flag
13
+ # for injection (:after or :before).
15
14
  #
16
15
  # ==== Examples
17
16
  #
@@ -24,28 +23,29 @@ class Thor
24
23
  #
25
24
  def inject_into_file(destination, *args, &block)
26
25
  if block_given?
27
- data, flag = block, args.shift
26
+ data, config = block, args.shift
28
27
  else
29
- data, flag = args.shift, args.shift
28
+ data, config = args.shift, args.shift
30
29
  end
31
30
 
32
31
  log_status = args.empty? || args.pop
33
- action InjectIntoFile.new(self, destination, data, flag, log_status)
32
+ action InjectIntoFile.new(self, destination, data, config)
34
33
  end
35
34
 
36
35
  class InjectIntoFile #:nodoc:
37
- attr_reader :base, :destination, :relative_destination, :flag, :replacement
36
+ attr_reader :base, :destination, :relative_destination, :flag, :replacement, :config
38
37
 
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
38
+ def initialize(base, destination, data, config)
39
+ @base, @config = base, { :verbose => true }.merge(config)
42
40
 
43
41
  self.destination = destination
44
42
  data = data.call if data.is_a?(Proc)
45
43
 
46
- @replacement = if behavior == :after
44
+ @replacement = if @config.key?(:after)
45
+ @flag = @config.delete(:after)
47
46
  @flag + data
48
47
  else
48
+ @flag = @config.delete(:before)
49
49
  data + @flag
50
50
  end
51
51
  end
@@ -68,14 +68,14 @@ class Thor
68
68
  def destination=(destination)
69
69
  if destination
70
70
  @destination = ::File.expand_path(destination.to_s, base.destination_root)
71
- @relative_destination = base.relative_to_absolute_root(@destination)
71
+ @relative_destination = base.relative_to_original_destination_root(@destination)
72
72
  end
73
73
  end
74
74
 
75
75
  # Shortcut to say_status shell method.
76
76
  #
77
77
  def say_status(status)
78
- base.shell.say_status status, relative_destination, @log_status
78
+ base.shell.say_status status, relative_destination, config[:verbose]
79
79
  end
80
80
 
81
81
  # Adds the content to the file.
@@ -9,9 +9,9 @@ class Thor
9
9
  # to be equal to the source removing .tt from the filename.
10
10
  #
11
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.
12
+ # source<String>:: the relative path to the source root.
13
+ # destination<String>:: the relative path to the destination root.
14
+ # config<Hash>:: give :verbose => false to not log the status.
15
15
  #
16
16
  # ==== Examples
17
17
  #
@@ -19,9 +19,9 @@ class Thor
19
19
  #
20
20
  # template "doc/README"
21
21
  #
22
- def template(source, destination=nil, log_status=true)
22
+ def template(source, destination=nil, config={})
23
23
  destination ||= source.gsub(/.tt$/, '')
24
- action Template.new(self, source, destination, log_status)
24
+ action Template.new(self, source, destination, config)
25
25
  end
26
26
 
27
27
  class Template < Templater #:nodoc:
@@ -8,7 +8,7 @@ class Thor
8
8
  # by Jonas Nicklas and Michael S. Klishin under MIT LICENSE.
9
9
  #
10
10
  class Templater #:nodoc:
11
- attr_reader :base, :source, :destination, :given_destination, :relative_destination
11
+ attr_reader :base, :source, :destination, :given_destination, :relative_destination, :config
12
12
 
13
13
  # Initializes given the source and destination.
14
14
  #
@@ -16,11 +16,10 @@ class Thor
16
16
  # base<Thor::Base>:: A Thor::Base instance
17
17
  # source<String>:: Relative path to the source of this file
18
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.
19
+ # config<Hash>:: give :verbose => false to not log the status.
21
20
  #
22
- def initialize(base, source, destination, log_status=true)
23
- @base, @log_status = base, log_status
21
+ def initialize(base, source, destination, config={})
22
+ @base, @config = base, { :verbose => true }.merge(config)
24
23
  self.source = source
25
24
  self.destination = destination
26
25
  end
@@ -56,7 +55,7 @@ class Thor
56
55
  # but you can modify in the subclass.
57
56
  #
58
57
  def invoke!
59
- invoke_with_options!(base.options) do
58
+ invoke_with_options!(base.options.merge(config)) do
60
59
  ::FileUtils.mkdir_p(::File.dirname(destination))
61
60
  ::File.open(destination, 'w'){ |f| f.write render }
62
61
  end
@@ -101,7 +100,7 @@ class Thor
101
100
  #
102
101
  def source=(source)
103
102
  if source
104
- @source = ::File.expand_path(source.to_s, File.join(base.source_root, base.relative_root))
103
+ @source = ::File.expand_path(base.find_in_source_paths(source.to_s))
105
104
  end
106
105
  end
107
106
 
@@ -123,7 +122,7 @@ class Thor
123
122
  if destination
124
123
  @given_destination = convert_encoded_instructions(destination.to_s)
125
124
  @destination = ::File.expand_path(@given_destination, base.destination_root)
126
- @relative_destination = base.relative_to_absolute_root(@destination)
125
+ @relative_destination = base.relative_to_original_destination_root(@destination)
127
126
  end
128
127
  end
129
128
 
@@ -187,7 +186,7 @@ class Thor
187
186
  # Shortcut to say_status shell method.
188
187
  #
189
188
  def say_status(status, color)
190
- base.shell.say_status status, relative_destination, color if @log_status
189
+ base.shell.say_status status, relative_destination, color if config[:verbose]
191
190
  end
192
191
 
193
192
  end
data/lib/thor/base.rb CHANGED
@@ -9,7 +9,7 @@ require 'thor/util'
9
9
 
10
10
  class Thor
11
11
  HELP_MAPPINGS = %w(-h -? --help -D)
12
- THOR_RESERVED_WORDS = %w(invoke shell options behavior root destination_root relative_root source_root)
12
+ THOR_RESERVED_WORDS = %w(invoke shell options behavior root destination_root relative_root)
13
13
 
14
14
  module Base
15
15
  attr_accessor :options
@@ -37,15 +37,17 @@ class Thor
37
37
 
38
38
  parse_options = self.class.class_options
39
39
 
40
- options = if options.is_a?(Array)
40
+ if options.is_a?(Array)
41
41
  task_options = config.delete(:task_options) # hook for start
42
42
  parse_options = parse_options.merge(task_options) if task_options
43
- Thor::Options.parse(parse_options, options)
43
+ array_options, hash_options = options, {}
44
44
  else
45
- Thor::Options.parse(parse_options, []).merge(options)
45
+ array_options, hash_options = [], options
46
46
  end
47
47
 
48
- self.options = Thor::CoreExt::HashWithIndifferentAccess.new(options).freeze
48
+ options = Thor::Options.parse(parse_options, array_options)
49
+ self.options = Thor::CoreExt::HashWithIndifferentAccess.new(options).merge!(hash_options)
50
+ self.options.freeze
49
51
  end
50
52
 
51
53
  class << self
@@ -75,14 +77,10 @@ class Thor
75
77
 
76
78
  # Whenever a class inherits from Thor or Thor::Group, we should track the
77
79
  # class and the file on Thor::Base. This is the method responsable for it.
78
- # Also invoke the source_root if the klass respond to it. This is needed
79
- # to ensure that the source_root does not change after FileUtils#cd is
80
- # called.
80
+ # Also adds the source root to the source paths if the klass respond to it.
81
81
  #
82
82
  def register_klass_file(klass) #:nodoc:
83
83
  file = caller[1].match(/(.*):\d+/)[1]
84
-
85
- klass.source_root if klass.respond_to?(:source_root)
86
84
  Thor::Base.subclasses << klass unless Thor::Base.subclasses.include?(klass)
87
85
 
88
86
  file_subclasses = Thor::Base.subclass_files[File.expand_path(file)]
@@ -189,7 +187,7 @@ class Thor
189
187
  # :type - The type of the argument, can be :string, :hash, :array, :numeric or :boolean.
190
188
  # :banner - String to show on usage notes.
191
189
  #
192
- def class_option(name, options)
190
+ def class_option(name, options={})
193
191
  build_option(name, options, class_options)
194
192
  end
195
193
 
@@ -359,51 +357,48 @@ class Thor
359
357
  # Prints the class options per group. If an option does not belong to
360
358
  # any group, it uses the ungrouped name value. This method provide to
361
359
  # hooks to add extra options, one of them if the third argument called
362
- # extra_group that should be a Thor::CoreExt::OrderedHash in the format
363
- # :group => Array[Options].
360
+ # extra_group that should be a hash in the format :group => Array[Options].
364
361
  #
365
362
  # The second is by returning a lamda used to print values. The lambda
366
363
  # requires two options: the group name and the array of options.
367
364
  #
368
365
  def class_options_help(shell, ungrouped_name=nil, extra_group=nil) #:nodoc:
369
- unless self.class_options.empty?
370
- groups = {}
371
-
372
- class_options.each do |_, value|
373
- groups[value.group] ||= []
374
- groups[value.group] << value
375
- end
366
+ groups = {}
376
367
 
377
- printer = proc do |group_name, options|
378
- list = []
379
- padding = options.collect{ |o| o.aliases.size }.max.to_i * 4
368
+ class_options.each do |_, value|
369
+ groups[value.group] ||= []
370
+ groups[value.group] << value
371
+ end
380
372
 
381
- options.each do |option|
382
- list << [ option.usage(padding), option.description || "" ]
383
- list << [ "", "Default: #{option.default}" ] if option.show_default?
384
- end
373
+ printer = proc do |group_name, options|
374
+ list = []
375
+ padding = options.collect{ |o| o.aliases.size }.max.to_i * 4
385
376
 
386
- unless list.empty?
387
- if group_name
388
- shell.say "#{group_name} options:"
389
- else
390
- shell.say "Options:"
391
- end
377
+ options.each do |option|
378
+ list << [ option.usage(padding), option.description || "" ]
379
+ list << [ "", "Default: #{option.default}" ] if option.show_default?
380
+ end
392
381
 
393
- shell.print_table(list, :emphasize_last => true, :ident => 2)
394
- shell.say ""
382
+ unless list.empty?
383
+ if group_name
384
+ shell.say "#{group_name} options:"
385
+ else
386
+ shell.say "Options:"
395
387
  end
388
+
389
+ shell.print_table(list, :emphasize_last => true, :ident => 2)
390
+ shell.say ""
396
391
  end
392
+ end
397
393
 
398
- # Deal with default group
399
- global_options = groups.delete(nil) || []
400
- printer.call(ungrouped_name, global_options) if global_options
394
+ # Deal with default group
395
+ global_options = groups.delete(nil) || []
396
+ printer.call(ungrouped_name, global_options) if global_options
401
397
 
402
- # Print all others
403
- groups = extra_group.merge(groups) if extra_group
404
- groups.each(&printer)
405
- printer
406
- end
398
+ # Print all others
399
+ groups = extra_group.merge(groups) if extra_group
400
+ groups.each(&printer)
401
+ printer
407
402
  end
408
403
 
409
404
  # Raises an error if the word given is a Thor reserved word.
data/lib/thor/group.rb CHANGED
@@ -1,7 +1,10 @@
1
+ # Thor has a special class called Thor::Group. The main difference to Thor class
2
+ # is that it invokes all tasks at once. It also include some methods that allows
3
+ # invocations to be done at the class method, which are not available to Thor
4
+ # tasks.
5
+ #
1
6
  class Thor::Group
2
-
3
7
  class << self
4
-
5
8
  # The descrition for this Thor::Group. If none is provided, but a source root
6
9
  # exists, tries to find the USAGE one folder above it, otherwise searches
7
10
  # in the superclass.
@@ -50,6 +53,171 @@ class Thor::Group
50
53
  end
51
54
  end
52
55
 
56
+ # Stores invocations for this class merging with superclass values.
57
+ #
58
+ def invocations #:nodoc:
59
+ @invocations ||= from_superclass(:invocations, {})
60
+ end
61
+
62
+ # Stores invocation blocks used on invoke_from_option.
63
+ #
64
+ def invocation_blocks #:nodoc:
65
+ @invocation_blocks ||= from_superclass(:invocation_blocks, {})
66
+ end
67
+
68
+ # Invoke the given namespace or class given. It adds an instance
69
+ # method that will invoke the klass and task. You can give a block to
70
+ # configure how it will be invoked.
71
+ #
72
+ # The namespace/class given will have its options showed on the help
73
+ # usage. Check invoke_from_option for more information.
74
+ #
75
+ def invoke(*names, &block)
76
+ options = names.last.is_a?(Hash) ? names.pop : {}
77
+ verbose = options.fetch(:verbose, :white)
78
+
79
+ names.each do |name|
80
+ invocations[name] = false
81
+ invocation_blocks[name] = block if block_given?
82
+
83
+ class_eval <<-METHOD, __FILE__, __LINE__
84
+ def _invoke_#{name.to_s.gsub(/\W/, '_')}
85
+ klass, task = self.class.prepare_for_invocation(nil, #{name.inspect})
86
+
87
+ if klass
88
+ say_status :invoke, #{name.inspect}, #{verbose.inspect}
89
+ block = self.class.invocation_blocks[#{name.inspect}]
90
+ invoke_with_padding klass, task, &block
91
+ else
92
+ say_status :error, %(#{name.inspect} [not found]), :red
93
+ end
94
+ end
95
+ METHOD
96
+ end
97
+ end
98
+
99
+ # Invoke a thor class based on the value supplied by the user to the
100
+ # given option named "name". A class option must be created before this
101
+ # method is invoked for each name given.
102
+ #
103
+ # ==== Examples
104
+ #
105
+ # class GemGenerator < Thor::Group
106
+ # class_option :test_framework, :type => :string
107
+ # invoke_from_option :test_framework
108
+ # end
109
+ #
110
+ # ==== Boolean options
111
+ #
112
+ # In some cases, you want to invoke a thor class if some option is true or
113
+ # false. This is automatically handled by invoke_from_option. Then the
114
+ # option name is used to invoke the generator.
115
+ #
116
+ # ==== Preparing for invocation
117
+ #
118
+ # In some cases you want to customize how a specified hook is going to be
119
+ # invoked. You can do that by overwriting the class method
120
+ # prepare_for_invocation. The class method must necessarily return a klass
121
+ # and an optional task.
122
+ #
123
+ # ==== Custom invocations
124
+ #
125
+ # You can also supply a block to customize how the option is giong to be
126
+ # invoked. The block receives two parameters, an instance of the current
127
+ # class and the klass to be invoked.
128
+ #
129
+ def invoke_from_option(*names, &block)
130
+ options = names.last.is_a?(Hash) ? names.pop : {}
131
+ verbose = options.fetch(:verbose, :white)
132
+
133
+ names.each do |name|
134
+ unless class_options.key?(name)
135
+ raise ArgumentError, "You have to define the option #{name.inspect} " <<
136
+ "before setting invoke_from_option."
137
+ end
138
+
139
+ invocations[name] = true
140
+ invocation_blocks[name] = block if block_given?
141
+
142
+ class_eval <<-METHOD, __FILE__, __LINE__
143
+ def _invoke_from_option_#{name.to_s.gsub(/\W/, '_')}
144
+ return unless options[#{name.inspect}]
145
+
146
+ value = options[#{name.inspect}]
147
+ value = #{name.inspect} if TrueClass === value
148
+ klass, task = self.class.prepare_for_invocation(#{name.inspect}, value)
149
+
150
+ if klass
151
+ say_status :invoke, value, #{verbose.inspect}
152
+ block = self.class.invocation_blocks[#{name.inspect}]
153
+ invoke_with_padding klass, task, &block
154
+ else
155
+ say_status :error, %(\#{value} [not found]), :red
156
+ end
157
+ end
158
+ METHOD
159
+ end
160
+ end
161
+
162
+ # Remove a previously added invocation.
163
+ #
164
+ # ==== Examples
165
+ #
166
+ # remove_invocation :test_framework
167
+ #
168
+ def remove_invocation(*names)
169
+ names.each do |name|
170
+ remove_task(name)
171
+ remove_class_option(name)
172
+ invocations.delete(name)
173
+ invocation_blocks.delete(name)
174
+ end
175
+ end
176
+
177
+ # Overwrite class options help to allow invoked generators options to be
178
+ # shown recursively when invoking a generator.
179
+ #
180
+ def class_options_help(shell, ungrouped_name=nil, extra_group=nil) #:nodoc:
181
+ group_options = {}
182
+
183
+ get_options_from_invocations(group_options, class_options) do |klass|
184
+ klass.send(:get_options_from_invocations, group_options, class_options)
185
+ end
186
+
187
+ group_options.merge!(extra_group) if extra_group
188
+ super(shell, ungrouped_name, group_options)
189
+ end
190
+
191
+ # Get invocations array and merge options from invocations. Those
192
+ # options are added to group_options hash. Options that already exists
193
+ # in base_options are not added twice.
194
+ #
195
+ def get_options_from_invocations(group_options, base_options) #:nodoc:
196
+ invocations.each do |name, from_option|
197
+ value = if from_option
198
+ option = class_options[name]
199
+ option.type == :boolean ? name : option.default
200
+ else
201
+ name
202
+ end
203
+ next unless value
204
+
205
+ klass, task = prepare_for_invocation(name, value)
206
+ next unless klass && klass.respond_to?(:class_options)
207
+
208
+ value = value.to_s
209
+ human_name = value.respond_to?(:classify) ? value.classify : value
210
+
211
+ group_options[human_name] ||= []
212
+ group_options[human_name] += klass.class_options.values.select do |option|
213
+ base_options[option.name.to_sym].nil? && option.group.nil? &&
214
+ !group_options.values.flatten.any? { |i| i.name == option.name }
215
+ end
216
+
217
+ yield klass if block_given?
218
+ end
219
+ end
220
+
53
221
  protected
54
222
 
55
223
  # The banner for this class. You can customize it if you are invoking the
@@ -1,5 +1,22 @@
1
1
  class Thor
2
2
  module Invocation
3
+ def self.included(base) #:nodoc:
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ # Prepare for class methods invocations. This method must return a klass to
9
+ # have the invoked class options showed in help messages in generators.
10
+ #
11
+ def prepare_for_invocation(key, name) #:nodoc:
12
+ case name
13
+ when Symbol, String
14
+ Thor::Util.namespace_to_thor_class(name.to_s, false)
15
+ else
16
+ name
17
+ end
18
+ end
19
+ end
3
20
 
4
21
  # Make initializer aware of invocations and the initializer proc.
5
22
  #
@@ -79,7 +96,7 @@ class Thor
79
96
  task, args, opts, config = nil, task, args, opts if task.nil? || task.is_a?(Array)
80
97
  args, opts, config = nil, args, opts if args.is_a?(Hash)
81
98
 
82
- object, task = _setup_for_invoke(name, task)
99
+ object, task = _prepare_for_invocation(name, task)
83
100
  if object.is_a?(Class)
84
101
  klass = object
85
102
 
@@ -113,6 +130,26 @@ class Thor
113
130
  end
114
131
  end
115
132
 
133
+ # Shortcut for invoke with padding and status handling. Used internally by
134
+ # class options invoke and invoke_from_option.
135
+ #
136
+ def invoke_with_padding(klass, task=nil, *args, &block)
137
+ shell.padding += 1
138
+
139
+ result = if block_given?
140
+ if block.arity == 2
141
+ block.call(self, klass)
142
+ else
143
+ block.call(self, klass, task)
144
+ end
145
+ else
146
+ invoke klass, task, *args
147
+ end
148
+
149
+ shell.padding -= 1
150
+ result
151
+ end
152
+
116
153
  protected
117
154
 
118
155
  # Configuration values that are shared between invocations.
@@ -121,23 +158,18 @@ class Thor
121
158
  { :invocations => @_invocations }
122
159
  end
123
160
 
124
- # This is the method responsable for retrieving and setting up an
125
- # instance to be used in invoke.
161
+ # Prepare for invocation in the instance level. In this case, we have to
162
+ # take into account that a just a task name from the current class was
163
+ # given or even a Thor::Task object.
126
164
  #
127
- def _setup_for_invoke(name, sent_task=nil) #:nodoc:
128
- case name
129
- when Thor::Task
130
- task = name
131
- when Symbol, String
132
- name = name.to_s
133
-
134
- # If is not one of this class tasks, do a lookup.
135
- unless task = self.class.all_tasks[name]
136
- object, task = Thor::Util.namespace_to_thor_class(name, false)
137
- task ||= sent_task
138
- end
139
- else
140
- object, task = name, sent_task
165
+ def _prepare_for_invocation(name, sent_task=nil) #:nodoc:
166
+ if name.is_a?(Thor::Task)
167
+ task = name
168
+ elsif task = self.class.all_tasks[name.to_s]
169
+ object = self
170
+ else
171
+ object, task = self.class.prepare_for_invocation(nil, name)
172
+ task ||= sent_task
141
173
  end
142
174
 
143
175
  # If the object was not set, use self and use the name as task.
@@ -148,7 +180,7 @@ class Thor
148
180
  # Check if the object given is a Thor class object and get a task object
149
181
  # for it.
150
182
  #
151
- def _validate_klass_and_task(object, task)
183
+ def _validate_klass_and_task(object, task) #:nodoc:
152
184
  klass = object.is_a?(Class) ? object : object.class
153
185
  raise "Expected Thor class, got #{klass}" unless klass <= Thor::Base
154
186
 
@@ -156,6 +188,5 @@ class Thor
156
188
  task = klass.all_tasks[task.to_s] || Task.dynamic(task) if task && !task.is_a?(Thor::Task)
157
189
  task
158
190
  end
159
-
160
191
  end
161
192
  end
@@ -95,14 +95,21 @@ class Thor
95
95
  end
96
96
  end
97
97
 
98
- def input_required?
99
- type != :boolean
98
+ # Allow some type predicates as: boolean?, string? and etc.
99
+ #
100
+ def method_missing(method, *args, &block)
101
+ given = method.to_s.sub(/\?$/, '').to_sym
102
+ if valid_type?(given)
103
+ self.type == given
104
+ else
105
+ super
106
+ end
100
107
  end
101
108
 
102
109
  protected
103
110
 
104
111
  def validate!
105
- raise ArgumentError, "An option cannot be boolean and required." if type == :boolean && required?
112
+ raise ArgumentError, "An option cannot be boolean and required." if boolean? && required?
106
113
  end
107
114
 
108
115
  def valid_type?(type)
@@ -122,9 +122,16 @@ class Thor
122
122
  # Parse the value at the peek analyzing if it requires an input or not.
123
123
  #
124
124
  def parse_peek(switch, option)
125
- if option.input_required?
126
- return nil if no_or_skip?(switch)
127
- raise MalformattedArgumentError, "no value provided for option '#{switch}'" unless current_is_value?
125
+ unless current_is_value?
126
+ if option.boolean?
127
+ # No problem for boolean types
128
+ elsif no_or_skip?(switch)
129
+ return nil # User set value to nil
130
+ elsif option.string? && !option.required?
131
+ return option.human_name # Return the option name
132
+ else
133
+ raise MalformattedArgumentError, "no value provided for option '#{switch}'"
134
+ end
128
135
  end
129
136
 
130
137
  @non_assigned_required.delete(option)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wycats-thor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.11.1
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-07-13 00:00:00 -07:00
12
+ date: 2009-07-16 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15