thor 1.1.0 → 1.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49e18474eddb5dbf02c13be5b42104385928e460511272e7293cc66110d1c3d7
4
- data.tar.gz: ffea33d8f08051882551af3a444fe68a77ffe0ecfe16404f2ead662364d36b5e
3
+ metadata.gz: 50c78a27ef16cfc96930bc60d8637e86acd69c94b932cc745bb2ed5c3e1605c8
4
+ data.tar.gz: 00b8a88f938047a55fe3ffadb6bd019666d82813ff584209a338bd8e6d3f16a7
5
5
  SHA512:
6
- metadata.gz: bc1a58088c4c7b48166152c1f7cfc0c4dc91e9c3199e583e6085b74a2982f65c0eaa8d8b7595f8f5781c08b0f60818e4a014bb23adbdfb07fd2ded558ceef67e
7
- data.tar.gz: f9a3c7f81a55d75298e3655569a86f04aa2055088058eb0a30ce5b80e05c6be7ed9c0f1840031dd266c453a4ac1454d8749849b47659d2f53f8b1221b9db2b7b
6
+ metadata.gz: 86fb64f2b698f06f5c7a0bd14523338802b374ae3d42879d71f94d542598216595176ce744b9ea84196d87657ec4cdaa9f73954ad15124b89109ee87a304eadc
7
+ data.tar.gz: 81f3b4ce1bca3d43efe2aecf00c88372b6673e3a82706e75ef018cc17badf3b3f2a267fe926b86225b112bbe1bd6e0257115327aa006984c89d8ddc434b88d56
data/README.md CHANGED
@@ -2,14 +2,8 @@ Thor
2
2
  ====
3
3
 
4
4
  [![Gem Version](http://img.shields.io/gem/v/thor.svg)][gem]
5
- [![Build Status](http://img.shields.io/travis/erikhuda/thor.svg)][travis]
6
- [![Code Climate](http://img.shields.io/codeclimate/github/erikhuda/thor.svg)][codeclimate]
7
- [![Coverage Status](http://img.shields.io/coveralls/erikhuda/thor.svg)][coveralls]
8
5
 
9
6
  [gem]: https://rubygems.org/gems/thor
10
- [travis]: http://travis-ci.org/erikhuda/thor
11
- [codeclimate]: https://codeclimate.com/github/erikhuda/thor
12
- [coveralls]: https://coveralls.io/r/erikhuda/thor
13
7
 
14
8
  Description
15
9
  -----------
@@ -21,7 +15,7 @@ users.
21
15
 
22
16
  Please note: Thor, by design, is a system tool created to allow seamless file and url
23
17
  access, which should not receive application user input. It relies on [open-uri][open-uri],
24
- which combined with application user input would provide a command injection attack
18
+ which, combined with application user input, would provide a command injection attack
25
19
  vector.
26
20
 
27
21
  [rake]: https://github.com/ruby/rake
@@ -33,9 +27,9 @@ Installation
33
27
 
34
28
  Usage and documentation
35
29
  -----------------------
36
- Please see the [wiki][] for basic usage and other documentation on using Thor. You can also checkout the [official homepage][homepage].
30
+ Please see the [wiki][] for basic usage and other documentation on using Thor. You can also check out the [official homepage][homepage].
37
31
 
38
- [wiki]: https://github.com/erikhuda/thor/wiki
32
+ [wiki]: https://github.com/rails/thor/wiki
39
33
  [homepage]: http://whatisthor.com/
40
34
 
41
35
  Contributing
@@ -43,7 +43,8 @@ class Thor
43
43
  # Boolean:: true if it is identical, false otherwise.
44
44
  #
45
45
  def identical?
46
- exists? && File.binread(destination) == render
46
+ # binread uses ASCII-8BIT, so to avoid false negatives, the string must use the same
47
+ exists? && File.binread(destination) == String.new(render).force_encoding("ASCII-8BIT")
47
48
  end
48
49
 
49
50
  # Holds the content to be added to the file.
@@ -60,7 +61,7 @@ class Thor
60
61
  invoke_with_conflict_check do
61
62
  require "fileutils"
62
63
  FileUtils.mkdir_p(File.dirname(destination))
63
- File.open(destination, "wb") { |f| f.write render }
64
+ File.open(destination, "wb", config[:perm]) { |f| f.write render }
64
65
  end
65
66
  given_destination
66
67
  end
@@ -58,7 +58,7 @@ class Thor
58
58
  def initialize(base, source, destination = nil, config = {}, &block)
59
59
  @source = File.expand_path(Dir[Util.escape_globs(base.find_in_source_paths(source.to_s))].first)
60
60
  @block = block
61
- super(base, destination, {:recursive => true}.merge(config))
61
+ super(base, destination, {recursive: true}.merge(config))
62
62
  end
63
63
 
64
64
  def invoke!
@@ -33,7 +33,7 @@ class Thor
33
33
  #
34
34
  def initialize(base, destination, config = {})
35
35
  @base = base
36
- @config = {:verbose => true}.merge(config)
36
+ @config = {verbose: true}.merge(config)
37
37
  self.destination = destination
38
38
  end
39
39
 
@@ -10,7 +10,6 @@ class Thor
10
10
  # destination<String>:: the relative path to the destination root.
11
11
  # config<Hash>:: give :verbose => false to not log the status, and
12
12
  # :mode => :preserve, to preserve the file mode from the source.
13
-
14
13
  #
15
14
  # ==== Examples
16
15
  #
@@ -66,12 +65,15 @@ class Thor
66
65
  # ==== Parameters
67
66
  # source<String>:: the address of the given content.
68
67
  # destination<String>:: the relative path to the destination root.
69
- # config<Hash>:: give :verbose => false to not log the status.
68
+ # config<Hash>:: give :verbose => false to not log the status, and
69
+ # :http_headers => <Hash> to add headers to an http request.
70
70
  #
71
71
  # ==== Examples
72
72
  #
73
73
  # get "http://gist.github.com/103208", "doc/README"
74
74
  #
75
+ # get "http://gist.github.com/103208", "doc/README", :http_headers => {"Content-Type" => "application/json"}
76
+ #
75
77
  # get "http://gist.github.com/103208" do |content|
76
78
  # content.split("\n").first
77
79
  # end
@@ -82,10 +84,10 @@ class Thor
82
84
 
83
85
  render = if source =~ %r{^https?\://}
84
86
  require "open-uri"
85
- URI.send(:open, source) { |input| input.binmode.read }
87
+ URI.send(:open, source, config.fetch(:http_headers, {})) { |input| input.binmode.read }
86
88
  else
87
89
  source = File.expand_path(find_in_source_paths(source.to_s))
88
- open(source) { |input| input.binmode.read }
90
+ File.open(source) { |input| input.binmode.read }
89
91
  end
90
92
 
91
93
  destination ||= if block_given?
@@ -120,12 +122,7 @@ class Thor
120
122
  context = config.delete(:context) || instance_eval("binding")
121
123
 
122
124
  create_file destination, nil, config do
123
- match = ERB.version.match(/(\d+\.\d+\.\d+)/)
124
- capturable_erb = if match && match[1] >= "2.2.0" # Ruby 2.6+
125
- CapturableERB.new(::File.binread(source), :trim_mode => "-", :eoutvar => "@output_buffer")
126
- else
127
- CapturableERB.new(::File.binread(source), nil, "-", "@output_buffer")
128
- end
125
+ capturable_erb = CapturableERB.new(::File.binread(source), trim_mode: "-", eoutvar: "@output_buffer")
129
126
  content = capturable_erb.tap do |erb|
130
127
  erb.filename = source
131
128
  end.result(context)
@@ -210,9 +207,9 @@ class Thor
210
207
  #
211
208
  # ==== Examples
212
209
  #
213
- # inject_into_class "app/controllers/application_controller.rb", ApplicationController, " filter_parameter :password\n"
210
+ # inject_into_class "app/controllers/application_controller.rb", "ApplicationController", " filter_parameter :password\n"
214
211
  #
215
- # inject_into_class "app/controllers/application_controller.rb", ApplicationController do
212
+ # inject_into_class "app/controllers/application_controller.rb", "ApplicationController" do
216
213
  # " filter_parameter :password\n"
217
214
  # end
218
215
  #
@@ -233,9 +230,9 @@ class Thor
233
230
  #
234
231
  # ==== Examples
235
232
  #
236
- # inject_into_module "app/helpers/application_helper.rb", ApplicationHelper, " def help; 'help'; end\n"
233
+ # inject_into_module "app/helpers/application_helper.rb", "ApplicationHelper", " def help; 'help'; end\n"
237
234
  #
238
- # inject_into_module "app/helpers/application_helper.rb", ApplicationHelper do
235
+ # inject_into_module "app/helpers/application_helper.rb", "ApplicationHelper" do
239
236
  # " def help; 'help'; end\n"
240
237
  # end
241
238
  #
@@ -245,6 +242,35 @@ class Thor
245
242
  insert_into_file(path, *(args << config), &block)
246
243
  end
247
244
 
245
+ # Run a regular expression replacement on a file, raising an error if the
246
+ # contents of the file are not changed.
247
+ #
248
+ # ==== Parameters
249
+ # path<String>:: path of the file to be changed
250
+ # flag<Regexp|String>:: the regexp or string to be replaced
251
+ # replacement<String>:: the replacement, can be also given as a block
252
+ # config<Hash>:: give :verbose => false to not log the status, and
253
+ # :force => true, to force the replacement regardless of runner behavior.
254
+ #
255
+ # ==== Example
256
+ #
257
+ # gsub_file! 'app/controllers/application_controller.rb', /#\s*(filter_parameter_logging :password)/, '\1'
258
+ #
259
+ # gsub_file! 'README', /rake/, :green do |match|
260
+ # match << " no more. Use thor!"
261
+ # end
262
+ #
263
+ def gsub_file!(path, flag, *args, &block)
264
+ config = args.last.is_a?(Hash) ? args.pop : {}
265
+
266
+ return unless behavior == :invoke || config.fetch(:force, false)
267
+
268
+ path = File.expand_path(path, destination_root)
269
+ say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
270
+
271
+ actually_gsub_file(path, flag, args, true, &block) unless options[:pretend]
272
+ end
273
+
248
274
  # Run a regular expression replacement on a file.
249
275
  #
250
276
  # ==== Parameters
@@ -252,7 +278,7 @@ class Thor
252
278
  # flag<Regexp|String>:: the regexp or string to be replaced
253
279
  # replacement<String>:: the replacement, can be also given as a block
254
280
  # config<Hash>:: give :verbose => false to not log the status, and
255
- # :force => true, to force the replacement regardles of runner behavior.
281
+ # :force => true, to force the replacement regardless of runner behavior.
256
282
  #
257
283
  # ==== Example
258
284
  #
@@ -270,16 +296,11 @@ class Thor
270
296
  path = File.expand_path(path, destination_root)
271
297
  say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
272
298
 
273
- unless options[:pretend]
274
- content = File.binread(path)
275
- content.gsub!(flag, *args, &block)
276
- File.open(path, "wb") { |file| file.write(content) }
277
- end
299
+ actually_gsub_file(path, flag, args, false, &block) unless options[:pretend]
278
300
  end
279
301
 
280
- # Uncomment all lines matching a given regex. It will leave the space
281
- # which existed before the comment hash in tact but will remove any spacing
282
- # between the comment hash and the beginning of the line.
302
+ # Uncomment all lines matching a given regex. Preserves indentation before
303
+ # the comment hash and removes the hash and any immediate following space.
283
304
  #
284
305
  # ==== Parameters
285
306
  # path<String>:: path of the file to be changed
@@ -293,7 +314,7 @@ class Thor
293
314
  def uncomment_lines(path, flag, *args)
294
315
  flag = flag.respond_to?(:source) ? flag.source : flag
295
316
 
296
- gsub_file(path, /^(\s*)#[[:blank:]]*(.*#{flag})/, '\1\2', *args)
317
+ gsub_file(path, /^(\s*)#[[:blank:]]?(.*#{flag})/, '\1\2', *args)
297
318
  end
298
319
 
299
320
  # Comment all lines matching a given regex. It will leave the space
@@ -331,7 +352,7 @@ class Thor
331
352
  path = File.expand_path(path, destination_root)
332
353
 
333
354
  say_status :remove, relative_to_original_destination_root(path), config.fetch(:verbose, true)
334
- if !options[:pretend] && File.exist?(path)
355
+ if !options[:pretend] && (File.exist?(path) || File.symlink?(path))
335
356
  require "fileutils"
336
357
  ::FileUtils.rm_rf(path)
337
358
  end
@@ -352,7 +373,7 @@ class Thor
352
373
  end
353
374
 
354
375
  def with_output_buffer(buf = "".dup) #:nodoc:
355
- raise ArgumentError, "Buffer can not be a frozen object" if buf.frozen?
376
+ raise ArgumentError, "Buffer cannot be a frozen object" if buf.frozen?
356
377
  old_buffer = output_buffer
357
378
  self.output_buffer = buf
358
379
  yield
@@ -361,6 +382,17 @@ class Thor
361
382
  self.output_buffer = old_buffer
362
383
  end
363
384
 
385
+ def actually_gsub_file(path, flag, args, error_on_no_change, &block)
386
+ content = File.binread(path)
387
+ success = content.gsub!(flag, *args, &block)
388
+
389
+ if success.nil? && error_on_no_change
390
+ raise Thor::Error, "The content of #{path} did not change"
391
+ end
392
+
393
+ File.open(path, "wb") { |file| file.write(content) }
394
+ end
395
+
364
396
  # Thor::Actions#capture depends on what kind of buffer is used in ERB.
365
397
  # Thus CapturableERB fixes ERB to use String buffer.
366
398
  class CapturableERB < ERB
@@ -21,7 +21,7 @@ class Thor
21
21
  # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n")
22
22
  # end
23
23
  #
24
- WARNINGS = { unchanged_no_flag: 'File unchanged! The supplied flag value not found!' }
24
+ WARNINGS = {unchanged_no_flag: "File unchanged! Either the supplied flag value not found or the content has already been inserted!"}
25
25
 
26
26
  def insert_into_file(destination, *args, &block)
27
27
  data = block_given? ? block : args.shift
@@ -37,7 +37,7 @@ class Thor
37
37
  attr_reader :replacement, :flag, :behavior
38
38
 
39
39
  def initialize(base, destination, data, config)
40
- super(base, destination, {:verbose => true}.merge(config))
40
+ super(base, destination, {verbose: true}.merge(config))
41
41
 
42
42
  @behavior, @flag = if @config.key?(:after)
43
43
  [:after, @config.delete(:after)]
@@ -59,6 +59,8 @@ class Thor
59
59
  if exists?
60
60
  if replace!(/#{flag}/, content, config[:force])
61
61
  say_status(:invoke)
62
+ elsif replacement_present?
63
+ say_status(:unchanged, color: :blue)
62
64
  else
63
65
  say_status(:unchanged, warning: WARNINGS[:unchanged_no_flag], color: :red)
64
66
  end
@@ -96,6 +98,8 @@ class Thor
96
98
  end
97
99
  elsif warning
98
100
  warning
101
+ elsif behavior == :unchanged
102
+ :unchanged
99
103
  else
100
104
  :subtract
101
105
  end
@@ -103,15 +107,21 @@ class Thor
103
107
  super(status, (color || config[:verbose]))
104
108
  end
105
109
 
110
+ def content
111
+ @content ||= File.read(destination)
112
+ end
113
+
114
+ def replacement_present?
115
+ content.include?(replacement)
116
+ end
117
+
106
118
  # Adds the content to the file.
107
119
  #
108
120
  def replace!(regexp, string, force)
109
- return if pretend?
110
- content = File.read(destination)
111
- if force || !content.include?(replacement)
121
+ if force || !replacement_present?
112
122
  success = content.gsub!(regexp, string)
113
123
 
114
- File.open(destination, "wb") { |file| file.write(content) }
124
+ File.open(destination, "wb") { |file| file.write(content) } unless pretend?
115
125
  success
116
126
  end
117
127
  end
data/lib/thor/actions.rb CHANGED
@@ -46,17 +46,17 @@ class Thor
46
46
  # Add runtime options that help actions execution.
47
47
  #
48
48
  def add_runtime_options!
49
- class_option :force, :type => :boolean, :aliases => "-f", :group => :runtime,
50
- :desc => "Overwrite files that already exist"
49
+ class_option :force, type: :boolean, aliases: "-f", group: :runtime,
50
+ desc: "Overwrite files that already exist"
51
51
 
52
- class_option :pretend, :type => :boolean, :aliases => "-p", :group => :runtime,
53
- :desc => "Run but do not make any changes"
52
+ class_option :pretend, type: :boolean, aliases: "-p", group: :runtime,
53
+ desc: "Run but do not make any changes"
54
54
 
55
- class_option :quiet, :type => :boolean, :aliases => "-q", :group => :runtime,
56
- :desc => "Suppress status output"
55
+ class_option :quiet, type: :boolean, aliases: "-q", group: :runtime,
56
+ desc: "Suppress status output"
57
57
 
58
- class_option :skip, :type => :boolean, :aliases => "-s", :group => :runtime,
59
- :desc => "Skip files that already exist"
58
+ class_option :skip, type: :boolean, aliases: "-s", group: :runtime,
59
+ desc: "Skip files that already exist"
60
60
  end
61
61
  end
62
62
 
@@ -113,9 +113,9 @@ class Thor
113
113
  #
114
114
  def relative_to_original_destination_root(path, remove_dot = true)
115
115
  root = @destination_stack[0]
116
- if path.start_with?(root) && [File::SEPARATOR, File::ALT_SEPARATOR, nil, ''].include?(path[root.size..root.size])
116
+ if path.start_with?(root) && [File::SEPARATOR, File::ALT_SEPARATOR, nil, ""].include?(path[root.size..root.size])
117
117
  path = path.dup
118
- path[0...root.size] = '.'
118
+ path[0...root.size] = "."
119
119
  remove_dot ? (path[2..-1] || "") : path
120
120
  else
121
121
  path
@@ -161,6 +161,8 @@ class Thor
161
161
  # to the block you provide. The path is set back to the previous path when
162
162
  # the method exits.
163
163
  #
164
+ # Returns the value yielded by the block.
165
+ #
164
166
  # ==== Parameters
165
167
  # dir<String>:: the directory to move to.
166
168
  # config<Hash>:: give :verbose => true to log and use padding.
@@ -173,22 +175,24 @@ class Thor
173
175
  shell.padding += 1 if verbose
174
176
  @destination_stack.push File.expand_path(dir, destination_root)
175
177
 
176
- # If the directory doesnt exist and we're not pretending
178
+ # If the directory doesn't exist and we're not pretending
177
179
  if !File.exist?(destination_root) && !pretend
178
180
  require "fileutils"
179
181
  FileUtils.mkdir_p(destination_root)
180
182
  end
181
183
 
184
+ result = nil
182
185
  if pretend
183
186
  # In pretend mode, just yield down to the block
184
- block.arity == 1 ? yield(destination_root) : yield
187
+ result = block.arity == 1 ? yield(destination_root) : yield
185
188
  else
186
189
  require "fileutils"
187
- FileUtils.cd(destination_root) { block.arity == 1 ? yield(destination_root) : yield }
190
+ FileUtils.cd(destination_root) { result = block.arity == 1 ? yield(destination_root) : yield }
188
191
  end
189
192
 
190
193
  @destination_stack.pop
191
194
  shell.padding -= 1 if verbose
195
+ result
192
196
  end
193
197
 
194
198
  # Goes to the root and execute the given block.
@@ -221,7 +225,7 @@ class Thor
221
225
  require "open-uri"
222
226
  URI.open(path, "Accept" => "application/x-thor-template", &:read)
223
227
  else
224
- open(path, &:read)
228
+ File.open(path, &:read)
225
229
  end
226
230
 
227
231
  instance_eval(contents, path)
@@ -280,7 +284,7 @@ class Thor
280
284
  #
281
285
  def run_ruby_script(command, config = {})
282
286
  return unless behavior == :invoke
283
- run command, config.merge(:with => Thor::Util.ruby_command)
287
+ run command, config.merge(with: Thor::Util.ruby_command)
284
288
  end
285
289
 
286
290
  # Run a thor command. A hash of options can be given and it's converted to
@@ -311,7 +315,7 @@ class Thor
311
315
  args.push Thor::Options.to_switches(config)
312
316
  command = args.join(" ").strip
313
317
 
314
- run command, :with => :thor, :verbose => verbose, :pretend => pretend, :capture => capture
318
+ run command, with: :thor, verbose: verbose, pretend: pretend, capture: capture
315
319
  end
316
320
 
317
321
  protected
@@ -319,7 +323,7 @@ class Thor
319
323
  # Allow current root to be shared between invocations.
320
324
  #
321
325
  def _shared_configuration #:nodoc:
322
- super.merge!(:destination_root => destination_root)
326
+ super.merge!(destination_root: destination_root)
323
327
  end
324
328
 
325
329
  def _cleanup_options_and_set(options, key) #:nodoc:
data/lib/thor/base.rb CHANGED
@@ -24,9 +24,9 @@ class Thor
24
24
 
25
25
  class << self
26
26
  def deprecation_warning(message) #:nodoc:
27
- unless ENV['THOR_SILENCE_DEPRECATION']
27
+ unless ENV["THOR_SILENCE_DEPRECATION"]
28
28
  warn "Deprecation warning: #{message}\n" +
29
- 'You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION.'
29
+ "You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION."
30
30
  end
31
31
  end
32
32
  end
@@ -60,6 +60,7 @@ class Thor
60
60
 
61
61
  command_options = config.delete(:command_options) # hook for start
62
62
  parse_options = parse_options.merge(command_options) if command_options
63
+
63
64
  if local_options.is_a?(Array)
64
65
  array_options = local_options
65
66
  hash_options = {}
@@ -73,9 +74,24 @@ class Thor
73
74
  # Let Thor::Options parse the options first, so it can remove
74
75
  # declared options from the array. This will leave us with
75
76
  # a list of arguments that weren't declared.
76
- stop_on_unknown = self.class.stop_on_unknown_option? config[:current_command]
77
- disable_required_check = self.class.disable_required_check? config[:current_command]
78
- opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check)
77
+ current_command = config[:current_command]
78
+ stop_on_unknown = self.class.stop_on_unknown_option? current_command
79
+
80
+ # Give a relation of options.
81
+ # After parsing, Thor::Options check whether right relations are kept
82
+ relations = if current_command.nil?
83
+ {exclusive_option_names: [], at_least_one_option_names: []}
84
+ else
85
+ current_command.options_relation
86
+ end
87
+
88
+ self.class.class_exclusive_option_names.map { |n| relations[:exclusive_option_names] << n }
89
+ self.class.class_at_least_one_option_names.map { |n| relations[:at_least_one_option_names] << n }
90
+
91
+ disable_required_check = self.class.disable_required_check? current_command
92
+
93
+ opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check, relations)
94
+
79
95
  self.options = opts.parse(array_options)
80
96
  self.options = config[:class_options].merge(options) if config[:class_options]
81
97
 
@@ -310,9 +326,92 @@ class Thor
310
326
  # :hide:: -- If you want to hide this option from the help.
311
327
  #
312
328
  def class_option(name, options = {})
329
+ unless [ Symbol, String ].any? { |klass| name.is_a?(klass) }
330
+ raise ArgumentError, "Expected a Symbol or String, got #{name.inspect}"
331
+ end
313
332
  build_option(name, options, class_options)
314
333
  end
315
334
 
335
+ # Adds and declares option group for exclusive options in the
336
+ # block and arguments. You can declare options as the outside of the block.
337
+ #
338
+ # ==== Parameters
339
+ # Array[Thor::Option.name]
340
+ #
341
+ # ==== Examples
342
+ #
343
+ # class_exclusive do
344
+ # class_option :one
345
+ # class_option :two
346
+ # end
347
+ #
348
+ # Or
349
+ #
350
+ # class_option :one
351
+ # class_option :two
352
+ # class_exclusive :one, :two
353
+ #
354
+ # If you give "--one" and "--two" at the same time ExclusiveArgumentsError
355
+ # will be raised.
356
+ #
357
+ def class_exclusive(*args, &block)
358
+ register_options_relation_for(:class_options,
359
+ :class_exclusive_option_names, *args, &block)
360
+ end
361
+
362
+ # Adds and declares option group for required at least one of options in the
363
+ # block and arguments. You can declare options as the outside of the block.
364
+ #
365
+ # ==== Examples
366
+ #
367
+ # class_at_least_one do
368
+ # class_option :one
369
+ # class_option :two
370
+ # end
371
+ #
372
+ # Or
373
+ #
374
+ # class_option :one
375
+ # class_option :two
376
+ # class_at_least_one :one, :two
377
+ #
378
+ # If you do not give "--one" and "--two" AtLeastOneRequiredArgumentError
379
+ # will be raised.
380
+ #
381
+ # You can use class_at_least_one and class_exclusive at the same time.
382
+ #
383
+ # class_exclusive do
384
+ # class_at_least_one do
385
+ # class_option :one
386
+ # class_option :two
387
+ # end
388
+ # end
389
+ #
390
+ # Then it is required either only one of "--one" or "--two".
391
+ #
392
+ def class_at_least_one(*args, &block)
393
+ register_options_relation_for(:class_options,
394
+ :class_at_least_one_option_names, *args, &block)
395
+ end
396
+
397
+ # Returns this class exclusive options array set, looking up in the ancestors chain.
398
+ #
399
+ # ==== Returns
400
+ # Array[Array[Thor::Option.name]]
401
+ #
402
+ def class_exclusive_option_names
403
+ @class_exclusive_option_names ||= from_superclass(:class_exclusive_option_names, [])
404
+ end
405
+
406
+ # Returns this class at least one of required options array set, looking up in the ancestors chain.
407
+ #
408
+ # ==== Returns
409
+ # Array[Array[Thor::Option.name]]
410
+ #
411
+ def class_at_least_one_option_names
412
+ @class_at_least_one_option_names ||= from_superclass(:class_at_least_one_option_names, [])
413
+ end
414
+
316
415
  # Removes a previous defined argument. If :undefine is given, undefine
317
416
  # accessors as well.
318
417
  #
@@ -506,7 +605,7 @@ class Thor
506
605
  #
507
606
  def public_command(*names)
508
607
  names.each do |name|
509
- class_eval "def #{name}(*); super end"
608
+ class_eval "def #{name}(*); super end", __FILE__, __LINE__
510
609
  end
511
610
  end
512
611
  alias_method :public_task, :public_command
@@ -558,20 +657,19 @@ class Thor
558
657
  return if options.empty?
559
658
 
560
659
  list = []
561
- padding = options.map { |o| o.aliases.size }.max.to_i * 4
562
-
660
+ padding = options.map { |o| o.aliases_for_usage.size }.max.to_i
563
661
  options.each do |option|
564
662
  next if option.hide
565
663
  item = [option.usage(padding)]
566
664
  item.push(option.description ? "# #{option.description}" : "")
567
665
 
568
666
  list << item
569
- list << ["", "# Default: #{option.default}"] if option.show_default?
570
- list << ["", "# Possible values: #{option.enum.join(', ')}"] if option.enum
667
+ list << ["", "# Default: #{option.print_default}"] if option.show_default?
668
+ list << ["", "# Possible values: #{option.enum_to_s}"] if option.enum
571
669
  end
572
670
 
573
671
  shell.say(group_name ? "#{group_name} options:" : "Options:")
574
- shell.print_table(list, :indent => 2)
672
+ shell.print_table(list, indent: 2)
575
673
  shell.say ""
576
674
  end
577
675
 
@@ -588,7 +686,7 @@ class Thor
588
686
  # options<Hash>:: Described in both class_option and method_option.
589
687
  # scope<Hash>:: Options hash that is being built up
590
688
  def build_option(name, options, scope) #:nodoc:
591
- scope[name] = Thor::Option.new(name, {:check_default_type => check_default_type}.merge!(options))
689
+ scope[name] = Thor::Option.new(name, {check_default_type: check_default_type}.merge!(options))
592
690
  end
593
691
 
594
692
  # Receives a hash of options, parse them and add to the scope. This is a
@@ -610,7 +708,7 @@ class Thor
610
708
  def find_and_refresh_command(name) #:nodoc:
611
709
  if commands[name.to_s]
612
710
  commands[name.to_s]
613
- elsif command = all_commands[name.to_s] # rubocop:disable AssignmentInCondition
711
+ elsif command = all_commands[name.to_s] # rubocop:disable Lint/AssignmentInCondition
614
712
  commands[name.to_s] = command.clone
615
713
  else
616
714
  raise ArgumentError, "You supplied :for => #{name.inspect}, but the command #{name.inspect} could not be found."
@@ -618,7 +716,7 @@ class Thor
618
716
  end
619
717
  alias_method :find_and_refresh_task, :find_and_refresh_command
620
718
 
621
- # Everytime someone inherits from a Thor class, register the klass
719
+ # Every time someone inherits from a Thor class, register the klass
622
720
  # and file into baseclass.
623
721
  def inherited(klass)
624
722
  super(klass)
@@ -694,6 +792,34 @@ class Thor
694
792
  def dispatch(command, given_args, given_opts, config) #:nodoc:
695
793
  raise NotImplementedError
696
794
  end
795
+
796
+ # Register a relation of options for target(method_option/class_option)
797
+ # by args and block.
798
+ def register_options_relation_for(target, relation, *args, &block) # :nodoc:
799
+ opt = args.pop if args.last.is_a? Hash
800
+ opt ||= {}
801
+ names = args.map{ |arg| arg.to_s }
802
+ names += built_option_names(target, opt, &block) if block_given?
803
+ command_scope_member(relation, opt) << names
804
+ end
805
+
806
+ # Get target(method_options or class_options) options
807
+ # of before and after by block evaluation.
808
+ def built_option_names(target, opt = {}, &block) # :nodoc:
809
+ before = command_scope_member(target, opt).map{ |k,v| v.name }
810
+ instance_eval(&block)
811
+ after = command_scope_member(target, opt).map{ |k,v| v.name }
812
+ after - before
813
+ end
814
+
815
+ # Get command scope member by name.
816
+ def command_scope_member(name, options = {}) # :nodoc:
817
+ if options[:for]
818
+ find_and_refresh_command(options[:for]).send(name)
819
+ else
820
+ send(name)
821
+ end
822
+ end
697
823
  end
698
824
  end
699
825
  end