thor 1.2.2 → 1.5.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: 51ab2cb4ae4ac2fe79583c377aa7385d47206d1e47ac0de5090f6a02de60f603
4
- data.tar.gz: 8ec9bc5cb0cb2288d8c0909aff4d05e649e48404d97ea339f9db6cf7498c05da
3
+ metadata.gz: 83a74d97baca896e3f78dfdcb081c118c8599ecbcf79085cbff299f393ccdd7c
4
+ data.tar.gz: c96fd32b0d35ea099f176a8febae1eb8814bf81548c442eb3a43d05d1c58f407
5
5
  SHA512:
6
- metadata.gz: 0ed6c11335f2f33c1fe2f49f4552110b1fad06c59547bd06b92e49fd01fa5b5a261d5298d7fabf5ca8ff9a8708e2f2d4ecb26e330b8aa2a4e20514c2e113cb94
7
- data.tar.gz: 912dfd2214bad858c184ce1a8994e1eab38acb9ccecf6aaf68f257dbfaa586fa7e427ff0e234ee5b99b738dae2539194a2d2c1943aed9bc3bfd2c527e941a12c
6
+ metadata.gz: e81da702b50b15939c310e1f24b1410bfed9d29364aeaa1972e97c1faf34102853531ee55897358a43914b49351d7a1d213d888b302a24c3d1f779b26bf4d310
7
+ data.tar.gz: bf2139f49455edc3a076a4e2eefd7db7a2b8be46038638f1fc7203f378cffb49317044d0ad51af172b74c3fce163c5a8b8d85c5c99ee8227137475c0632b614a
data/CONTRIBUTING.md CHANGED
@@ -13,3 +13,20 @@ Here are some reasons why a pull request may not be merged:
13
13
  If you would like to help in this process, you can start by evaluating open pull requests against the criteria above. For example, if a pull request does not include specs for new functionality, you can add a comment like: “If you would like this feature to be added to Thor, please add specs to ensure that it does not break in the future.” This will help move a pull request closer to being merged.
14
14
 
15
15
  Include this emoji in the top of your ticket to signal to us that you read this file: 🌈
16
+
17
+ Specs
18
+ -----
19
+
20
+ Ensure that all specs and code linting checks pass before submitting a pull request.
21
+
22
+ To execute the specs locally, run:
23
+
24
+ ```bash
25
+ bundle exec rspec
26
+ ```
27
+
28
+ Linting checks are done with RuboCop. To run the linter, use:
29
+
30
+ ```bash
31
+ bundle exec rubocop
32
+ ```
data/README.md CHANGED
@@ -15,7 +15,7 @@ users.
15
15
 
16
16
  Please note: Thor, by design, is a system tool created to allow seamless file and url
17
17
  access, which should not receive application user input. It relies on [open-uri][open-uri],
18
- 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
19
19
  vector.
20
20
 
21
21
  [rake]: https://github.com/ruby/rake
@@ -27,10 +27,9 @@ Installation
27
27
 
28
28
  Usage and documentation
29
29
  -----------------------
30
- 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.
31
31
 
32
32
  [wiki]: https://github.com/rails/thor/wiki
33
- [homepage]: http://whatisthor.com/
34
33
 
35
34
  Contributing
36
35
  ------------
@@ -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.
@@ -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,7 +84,7 @@ 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
90
  File.open(source) { |input| input.binmode.read }
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -2,6 +2,38 @@ require_relative "empty_directory"
2
2
 
3
3
  class Thor
4
4
  module Actions
5
+ WARNINGS = {unchanged_no_flag: "File unchanged! Either the supplied flag value not found or the content has already been inserted!"}
6
+
7
+ # Injects the given content into a file, raising an error if the contents of
8
+ # the file are not changed. Different from gsub_file, this method is reversible.
9
+ #
10
+ # ==== Parameters
11
+ # destination<String>:: Relative path to the destination root
12
+ # data<String>:: Data to add to the file. Can be given as a block.
13
+ # config<Hash>:: give :verbose => false to not log the status and the flag
14
+ # for injection (:after or :before) or :force => true for
15
+ # insert two or more times the same content.
16
+ #
17
+ # ==== Examples
18
+ #
19
+ # insert_into_file "config/environment.rb", "config.gem :thor", :after => "Rails::Initializer.run do |config|\n"
20
+ #
21
+ # insert_into_file "config/environment.rb", :after => "Rails::Initializer.run do |config|\n" do
22
+ # gems = ask "Which gems would you like to add?"
23
+ # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n")
24
+ # end
25
+ #
26
+ def insert_into_file!(destination, *args, &block)
27
+ data = block_given? ? block : args.shift
28
+
29
+ config = args.shift || {}
30
+ config[:after] = /\z/ unless config.key?(:before) || config.key?(:after)
31
+ config = config.merge({error_on_no_change: true})
32
+
33
+ action InjectIntoFile.new(self, destination, data, config)
34
+ end
35
+ alias_method :inject_into_file!, :insert_into_file!
36
+
5
37
  # Injects the given content into a file. Different from gsub_file, this
6
38
  # method is reversible.
7
39
  #
@@ -21,8 +53,6 @@ class Thor
21
53
  # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n")
22
54
  # end
23
55
  #
24
- WARNINGS = { unchanged_no_flag: 'File unchanged! Either the supplied flag value not found or the content has already been inserted!' }
25
-
26
56
  def insert_into_file(destination, *args, &block)
27
57
  data = block_given? ? block : args.shift
28
58
 
@@ -37,7 +67,7 @@ class Thor
37
67
  attr_reader :replacement, :flag, :behavior
38
68
 
39
69
  def initialize(base, destination, data, config)
40
- super(base, destination, {:verbose => true}.merge(config))
70
+ super(base, destination, {verbose: true}.merge(config))
41
71
 
42
72
  @behavior, @flag = if @config.key?(:after)
43
73
  [:after, @config.delete(:after)]
@@ -47,6 +77,7 @@ class Thor
47
77
 
48
78
  @replacement = data.is_a?(Proc) ? data.call : data
49
79
  @flag = Regexp.escape(@flag) unless @flag.is_a?(Regexp)
80
+ @error_on_no_change = @config.fetch(:error_on_no_change, false)
50
81
  end
51
82
 
52
83
  def invoke!
@@ -59,6 +90,10 @@ class Thor
59
90
  if exists?
60
91
  if replace!(/#{flag}/, content, config[:force])
61
92
  say_status(:invoke)
93
+ elsif @error_on_no_change
94
+ raise Thor::Error, "The content of #{destination} did not change"
95
+ elsif replacement_present?
96
+ say_status(:unchanged, color: :blue)
62
97
  else
63
98
  say_status(:unchanged, warning: WARNINGS[:unchanged_no_flag], color: :red)
64
99
  end
@@ -96,6 +131,8 @@ class Thor
96
131
  end
97
132
  elsif warning
98
133
  warning
134
+ elsif behavior == :unchanged
135
+ :unchanged
99
136
  else
100
137
  :subtract
101
138
  end
@@ -103,11 +140,18 @@ class Thor
103
140
  super(status, (color || config[:verbose]))
104
141
  end
105
142
 
143
+ def content
144
+ @content ||= File.read(destination)
145
+ end
146
+
147
+ def replacement_present?
148
+ content.include?(replacement)
149
+ end
150
+
106
151
  # Adds the content to the file.
107
152
  #
108
153
  def replace!(regexp, string, force)
109
- content = File.read(destination)
110
- if force || !content.include?(replacement)
154
+ if force || !replacement_present?
111
155
  success = content.gsub!(regexp, string)
112
156
 
113
157
  File.open(destination, "wb") { |file| file.write(content) } unless pretend?
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
@@ -223,8 +223,7 @@ class Thor
223
223
 
224
224
  contents = if is_uri
225
225
  require "open-uri"
226
- # for ruby 2.1-2.4
227
- URI.send(:open, path, "Accept" => "application/x-thor-template", &:read)
226
+ URI.open(path, "Accept" => "application/x-thor-template", &:read)
228
227
  else
229
228
  File.open(path, &:read)
230
229
  end
@@ -285,7 +284,7 @@ class Thor
285
284
  #
286
285
  def run_ruby_script(command, config = {})
287
286
  return unless behavior == :invoke
288
- run command, config.merge(:with => Thor::Util.ruby_command)
287
+ run command, config.merge(with: Thor::Util.ruby_command)
289
288
  end
290
289
 
291
290
  # Run a thor command. A hash of options can be given and it's converted to
@@ -316,7 +315,7 @@ class Thor
316
315
  args.push Thor::Options.to_switches(config)
317
316
  command = args.join(" ").strip
318
317
 
319
- run command, :with => :thor, :verbose => verbose, :pretend => pretend, :capture => capture
318
+ run command, with: :thor, verbose: verbose, pretend: pretend, capture: capture
320
319
  end
321
320
 
322
321
  protected
@@ -324,7 +323,7 @@ class Thor
324
323
  # Allow current root to be shared between invocations.
325
324
  #
326
325
  def _shared_configuration #:nodoc:
327
- super.merge!(:destination_root => destination_root)
326
+ super.merge!(destination_root: destination_root)
328
327
  end
329
328
 
330
329
  def _cleanup_options_and_set(options, key) #:nodoc:
data/lib/thor/base.rb CHANGED
@@ -13,8 +13,9 @@ class Thor
13
13
  autoload :RakeCompat, File.expand_path("rake_compat", __dir__)
14
14
  autoload :Group, File.expand_path("group", __dir__)
15
15
 
16
- # Shortcuts for help.
16
+ # Shortcuts for help and tree commands.
17
17
  HELP_MAPPINGS = %w(-h -? --help -D)
18
+ TREE_MAPPINGS = %w(-t --tree)
18
19
 
19
20
  # Thor methods that should not be overwritten by the user.
20
21
  THOR_RESERVED_WORDS = %w(invoke shell options behavior root destination_root relative_root
@@ -24,9 +25,9 @@ class Thor
24
25
 
25
26
  class << self
26
27
  def deprecation_warning(message) #:nodoc:
27
- unless ENV['THOR_SILENCE_DEPRECATION']
28
+ unless ENV["THOR_SILENCE_DEPRECATION"]
28
29
  warn "Deprecation warning: #{message}\n" +
29
- 'You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION.'
30
+ "You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION."
30
31
  end
31
32
  end
32
33
  end
@@ -60,6 +61,7 @@ class Thor
60
61
 
61
62
  command_options = config.delete(:command_options) # hook for start
62
63
  parse_options = parse_options.merge(command_options) if command_options
64
+
63
65
  if local_options.is_a?(Array)
64
66
  array_options = local_options
65
67
  hash_options = {}
@@ -73,9 +75,24 @@ class Thor
73
75
  # Let Thor::Options parse the options first, so it can remove
74
76
  # declared options from the array. This will leave us with
75
77
  # 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)
78
+ current_command = config[:current_command]
79
+ stop_on_unknown = self.class.stop_on_unknown_option? current_command
80
+
81
+ # Give a relation of options.
82
+ # After parsing, Thor::Options check whether right relations are kept
83
+ relations = if current_command.nil?
84
+ {exclusive_option_names: [], at_least_one_option_names: []}
85
+ else
86
+ current_command.options_relation
87
+ end
88
+
89
+ self.class.class_exclusive_option_names.map { |n| relations[:exclusive_option_names] << n }
90
+ self.class.class_at_least_one_option_names.map { |n| relations[:at_least_one_option_names] << n }
91
+
92
+ disable_required_check = self.class.disable_required_check? current_command
93
+
94
+ opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check, relations)
95
+
79
96
  self.options = opts.parse(array_options)
80
97
  self.options = config[:class_options].merge(options) if config[:class_options]
81
98
 
@@ -310,9 +327,92 @@ class Thor
310
327
  # :hide:: -- If you want to hide this option from the help.
311
328
  #
312
329
  def class_option(name, options = {})
330
+ unless [ Symbol, String ].any? { |klass| name.is_a?(klass) }
331
+ raise ArgumentError, "Expected a Symbol or String, got #{name.inspect}"
332
+ end
313
333
  build_option(name, options, class_options)
314
334
  end
315
335
 
336
+ # Adds and declares option group for exclusive options in the
337
+ # block and arguments. You can declare options as the outside of the block.
338
+ #
339
+ # ==== Parameters
340
+ # Array[Thor::Option.name]
341
+ #
342
+ # ==== Examples
343
+ #
344
+ # class_exclusive do
345
+ # class_option :one
346
+ # class_option :two
347
+ # end
348
+ #
349
+ # Or
350
+ #
351
+ # class_option :one
352
+ # class_option :two
353
+ # class_exclusive :one, :two
354
+ #
355
+ # If you give "--one" and "--two" at the same time ExclusiveArgumentsError
356
+ # will be raised.
357
+ #
358
+ def class_exclusive(*args, &block)
359
+ register_options_relation_for(:class_options,
360
+ :class_exclusive_option_names, *args, &block)
361
+ end
362
+
363
+ # Adds and declares option group for required at least one of options in the
364
+ # block and arguments. You can declare options as the outside of the block.
365
+ #
366
+ # ==== Examples
367
+ #
368
+ # class_at_least_one do
369
+ # class_option :one
370
+ # class_option :two
371
+ # end
372
+ #
373
+ # Or
374
+ #
375
+ # class_option :one
376
+ # class_option :two
377
+ # class_at_least_one :one, :two
378
+ #
379
+ # If you do not give "--one" and "--two" AtLeastOneRequiredArgumentError
380
+ # will be raised.
381
+ #
382
+ # You can use class_at_least_one and class_exclusive at the same time.
383
+ #
384
+ # class_exclusive do
385
+ # class_at_least_one do
386
+ # class_option :one
387
+ # class_option :two
388
+ # end
389
+ # end
390
+ #
391
+ # Then it is required either only one of "--one" or "--two".
392
+ #
393
+ def class_at_least_one(*args, &block)
394
+ register_options_relation_for(:class_options,
395
+ :class_at_least_one_option_names, *args, &block)
396
+ end
397
+
398
+ # Returns this class exclusive options array set, looking up in the ancestors chain.
399
+ #
400
+ # ==== Returns
401
+ # Array[Array[Thor::Option.name]]
402
+ #
403
+ def class_exclusive_option_names
404
+ @class_exclusive_option_names ||= from_superclass(:class_exclusive_option_names, [])
405
+ end
406
+
407
+ # Returns this class at least one of required options array set, looking up in the ancestors chain.
408
+ #
409
+ # ==== Returns
410
+ # Array[Array[Thor::Option.name]]
411
+ #
412
+ def class_at_least_one_option_names
413
+ @class_at_least_one_option_names ||= from_superclass(:class_at_least_one_option_names, [])
414
+ end
415
+
316
416
  # Removes a previous defined argument. If :undefine is given, undefine
317
417
  # accessors as well.
318
418
  #
@@ -565,12 +665,12 @@ class Thor
565
665
  item.push(option.description ? "# #{option.description}" : "")
566
666
 
567
667
  list << item
568
- list << ["", "# Default: #{option.default}"] if option.show_default?
569
- list << ["", "# Possible values: #{option.enum.join(', ')}"] if option.enum
668
+ list << ["", "# Default: #{option.print_default}"] if option.show_default?
669
+ list << ["", "# Possible values: #{option.enum_to_s}"] if option.enum
570
670
  end
571
671
 
572
672
  shell.say(group_name ? "#{group_name} options:" : "Options:")
573
- shell.print_table(list, :indent => 2)
673
+ shell.print_table(list, indent: 2)
574
674
  shell.say ""
575
675
  end
576
676
 
@@ -587,7 +687,7 @@ class Thor
587
687
  # options<Hash>:: Described in both class_option and method_option.
588
688
  # scope<Hash>:: Options hash that is being built up
589
689
  def build_option(name, options, scope) #:nodoc:
590
- scope[name] = Thor::Option.new(name, {:check_default_type => check_default_type}.merge!(options))
690
+ scope[name] = Thor::Option.new(name, {check_default_type: check_default_type}.merge!(options))
591
691
  end
592
692
 
593
693
  # Receives a hash of options, parse them and add to the scope. This is a
@@ -693,6 +793,34 @@ class Thor
693
793
  def dispatch(command, given_args, given_opts, config) #:nodoc:
694
794
  raise NotImplementedError
695
795
  end
796
+
797
+ # Register a relation of options for target(method_option/class_option)
798
+ # by args and block.
799
+ def register_options_relation_for(target, relation, *args, &block) # :nodoc:
800
+ opt = args.pop if args.last.is_a? Hash
801
+ opt ||= {}
802
+ names = args.map{ |arg| arg.to_s }
803
+ names += built_option_names(target, opt, &block) if block_given?
804
+ command_scope_member(relation, opt) << names
805
+ end
806
+
807
+ # Get target(method_options or class_options) options
808
+ # of before and after by block evaluation.
809
+ def built_option_names(target, opt = {}, &block) # :nodoc:
810
+ before = command_scope_member(target, opt).map{ |k,v| v.name }
811
+ instance_eval(&block)
812
+ after = command_scope_member(target, opt).map{ |k,v| v.name }
813
+ after - before
814
+ end
815
+
816
+ # Get command scope member by name.
817
+ def command_scope_member(name, options = {}) # :nodoc:
818
+ if options[:for]
819
+ find_and_refresh_command(options[:for]).send(name)
820
+ else
821
+ send(name)
822
+ end
823
+ end
696
824
  end
697
825
  end
698
826
  end
data/lib/thor/command.rb CHANGED
@@ -1,14 +1,15 @@
1
1
  class Thor
2
- class Command < Struct.new(:name, :description, :long_description, :usage, :options, :ancestor_name)
2
+ class Command < Struct.new(:name, :description, :long_description, :wrap_long_description, :usage, :options, :options_relation, :ancestor_name)
3
3
  FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/
4
4
 
5
- def initialize(name, description, long_description, usage, options = nil)
6
- super(name.to_s, description, long_description, usage, options || {})
5
+ def initialize(name, description, long_description, wrap_long_description, usage, options = nil, options_relation = nil)
6
+ super(name.to_s, description, long_description, wrap_long_description, usage, options || {}, options_relation || {})
7
7
  end
8
8
 
9
9
  def initialize_copy(other) #:nodoc:
10
10
  super(other)
11
11
  self.options = other.options.dup if other.options
12
+ self.options_relation = other.options_relation.dup if other.options_relation
12
13
  end
13
14
 
14
15
  def hidden?
@@ -62,6 +63,14 @@ class Thor
62
63
  end.join("\n")
63
64
  end
64
65
 
66
+ def method_exclusive_option_names #:nodoc:
67
+ self.options_relation[:exclusive_option_names] || []
68
+ end
69
+
70
+ def method_at_least_one_option_names #:nodoc:
71
+ self.options_relation[:at_least_one_option_names] || []
72
+ end
73
+
65
74
  protected
66
75
 
67
76
  # Add usage with required arguments
@@ -127,7 +136,7 @@ class Thor
127
136
  # A dynamic command that handles method missing scenarios.
128
137
  class DynamicCommand < Command
129
138
  def initialize(name, options = nil)
130
- super(name.to_s, "A dynamically-generated command", name.to_s, name.to_s, options)
139
+ super(name.to_s, "A dynamically-generated command", name.to_s, nil, name.to_s, options)
131
140
  end
132
141
 
133
142
  def run(instance, args = [])
@@ -38,6 +38,10 @@ class Thor
38
38
  super(convert_key(key), *args)
39
39
  end
40
40
 
41
+ def slice(*keys)
42
+ super(*keys.map{ |key| convert_key(key) })
43
+ end
44
+
41
45
  def key?(key)
42
46
  super(convert_key(key))
43
47
  end