thor 0.19.4 → 0.20.3

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
- SHA1:
3
- metadata.gz: 5fd71663b46487af27e6f0b6a0206f5140d0d196
4
- data.tar.gz: d67e482d506417552648630f8a7bc4b1886fc06c
2
+ SHA256:
3
+ metadata.gz: b35ad01aa15321a80d1e8c579b3c979c3ff6985a98cca96ffc3e21beba4dd7b5
4
+ data.tar.gz: f6aa82269a71a418fa99f82ed56672a32da5ce7ebaa4f6f9e2bdb8a16459ec95
5
5
  SHA512:
6
- metadata.gz: f15702a93adea15d623fd708962193b42bc38bf2b86dff66fe43f1078971fc38f5dedff0d6a78bd1bf9241315e83ede0c216ffedea8c452c55b57aff17d0d051
7
- data.tar.gz: 1e7093648f0913e9c7e32c796ea795e819ca59331588885059bfcae9867172ea50144033c342d22c4d5c8ab628f135821bb9a0ab8645f7784c396482370b98c0
6
+ metadata.gz: 3a405c6ac1be920be0d5f1dfe2f4d01ddae7fd68f229d6968c619e243abedfe6adb6b5b58d935b5a8528b4561c6f16fbd9c9ca58befd993ea0bb43950333983e
7
+ data.tar.gz: 659d3e822725f0bc9161feb69fc2449dfd4b355279a0c59406abf909f23435f157e97b353deec047cd3dbce38a5214d3cd6010844ffd86f50e797a0ab21eac55
@@ -1,3 +1,44 @@
1
+ # 0.20.3
2
+ * Support old versions of `did_you_mean`.
3
+
4
+ # 0.20.2
5
+ * Fix `did_you_mean` support.
6
+
7
+ # 0.20.1
8
+ * Support new versions fo ERB.
9
+ * Fix `check_unknown_options!` to not check the content that was not parsed, i.e. after a `--` or after the first unknown with `stop_on_unknown_option!`
10
+ * Add `did_you_mean` support.
11
+
12
+ ## 0.20.0
13
+ * Add `check_default_type!` to check if the default value of an option matches the defined type.
14
+ It removes the warning on usage and gives the command authors the possibility to check for programming errors.
15
+
16
+ * Add `disable_required_check!` to disable check for required options in some commands.
17
+ It is a substitute of `disable_class_options` that was not working as intended.
18
+
19
+ * Add `inject_into_module`.
20
+
21
+ ## 0.19.4, release 2016-11-28
22
+ * Rename `Thor::Base#thor_reserved_word?` to `#is_thor_reserved_word?`
23
+
24
+ ## 0.19.3, release 2016-11-27
25
+ * Output a warning instead of raising an exception when a default option value doesn't match its specified type
26
+
27
+ ## 0.19.2, release 2016-11-26
28
+ * Fix bug with handling of colors passed to `ask` (and methods like `yes?` and `no?` which it underpins)
29
+ * Allow numeric arguments to be negative
30
+ * Ensure that default option values are of the specified type (e.g. you can't specify `"foo"` as the default for a numeric option), but make symbols and strings interchangeable
31
+ * Add `Thor::Shell::Basic#indent` method for intending output
32
+ * Fix `remove_command` for an inherited command (see #451)
33
+ * Allow hash arguments to only have each key provided once (see #455)
34
+ * Allow commands to disable class options, for instance for "help" commands (see #363)
35
+ * Do not generate a negative option (`--no-no-foo`) for already negative boolean options (`--no-foo`)
36
+ * Improve compatibility of `Thor::CoreExt::HashWithIndifferentAccess` with Ruby standard library `Hash`
37
+ * Allow specifying a custom binding for template evaluation (e.g. `#key?` and `#fetch`)
38
+ * Fix support for subcommand-specific "help"s
39
+ * Use a string buffer when handling ERB for Ruby 2.3 compatibility
40
+ * Update dependencies
41
+
1
42
  ## 0.19.1, release 2014-03-24
2
43
  * Fix `say` non-String break regression
3
44
 
data/README.md CHANGED
@@ -3,13 +3,11 @@ Thor
3
3
 
4
4
  [![Gem Version](http://img.shields.io/gem/v/thor.svg)][gem]
5
5
  [![Build Status](http://img.shields.io/travis/erikhuda/thor.svg)][travis]
6
- [![Dependency Status](http://img.shields.io/gemnasium/erikhuda/thor.svg)][gemnasium]
7
6
  [![Code Climate](http://img.shields.io/codeclimate/github/erikhuda/thor.svg)][codeclimate]
8
7
  [![Coverage Status](http://img.shields.io/coveralls/erikhuda/thor.svg)][coveralls]
9
8
 
10
9
  [gem]: https://rubygems.org/gems/thor
11
10
  [travis]: http://travis-ci.org/erikhuda/thor
12
- [gemnasium]: https://gemnasium.com/erikhuda/thor
13
11
  [codeclimate]: https://codeclimate.com/github/erikhuda/thor
14
12
  [coveralls]: https://coveralls.io/r/erikhuda/thor
15
13
 
@@ -21,7 +19,13 @@ utilities. It removes the pain of parsing command line options, writing
21
19
  build tool. The syntax is Rake-like, so it should be familiar to most Rake
22
20
  users.
23
21
 
22
+ Please note: Thor, by design, is a system tool created to allow seamless file and url
23
+ 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
25
+ vector.
26
+
24
27
  [rake]: https://github.com/ruby/rake
28
+ [open-uri]: https://ruby-doc.org/stdlib-2.5.1/libdoc/open-uri/rdoc/index.html
25
29
 
26
30
  Installation
27
31
  ------------
@@ -158,10 +158,6 @@ class Thor
158
158
  end
159
159
  alias_method :option, :method_option
160
160
 
161
- def disable_class_options
162
- @disable_class_options = true
163
- end
164
-
165
161
  # Prints help information for the given command.
166
162
  #
167
163
  # ==== Parameters
@@ -241,6 +237,9 @@ class Thor
241
237
  invoke_args.unshift "help" if opts.delete("--help") || opts.delete("-h")
242
238
  invoke subcommand_class, *invoke_args
243
239
  end
240
+ subcommand_class.commands.each do |_meth, command|
241
+ command.ancestor_name = subcommand
242
+ end
244
243
  end
245
244
  alias_method :subtask, :subcommand
246
245
 
@@ -326,12 +325,31 @@ class Thor
326
325
  command && stop_on_unknown_option.include?(command.name.to_sym)
327
326
  end
328
327
 
328
+ # Disable the check for required options for the given commands.
329
+ # This is useful if you have a command that does not need the required options
330
+ # to work, like help.
331
+ #
332
+ # ==== Parameters
333
+ # Symbol ...:: A list of commands that should be affected.
334
+ def disable_required_check!(*command_names)
335
+ disable_required_check.merge(command_names)
336
+ end
337
+
338
+ def disable_required_check?(command) #:nodoc:
339
+ command && disable_required_check.include?(command.name.to_sym)
340
+ end
341
+
329
342
  protected
330
343
 
331
344
  def stop_on_unknown_option #:nodoc:
332
345
  @stop_on_unknown_option ||= Set.new
333
346
  end
334
347
 
348
+ # help command has the required check disabled by default.
349
+ def disable_required_check #:nodoc:
350
+ @disable_required_check ||= Set.new([:help])
351
+ end
352
+
335
353
  # The method responsible for dispatching given the args.
336
354
  def dispatch(meth, given_args, given_opts, config) #:nodoc: # rubocop:disable MethodLength
337
355
  meth ||= retrieve_command_name(given_args)
@@ -390,12 +408,12 @@ class Thor
390
408
  @usage ||= nil
391
409
  @desc ||= nil
392
410
  @long_desc ||= nil
393
- @disable_class_options ||= nil
411
+ @hide ||= nil
394
412
 
395
413
  if @usage && @desc
396
414
  base_class = @hide ? Thor::HiddenCommand : Thor::Command
397
- commands[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options, @disable_class_options)
398
- @usage, @desc, @long_desc, @method_options, @hide, @disable_class_options = nil
415
+ commands[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options)
416
+ @usage, @desc, @long_desc, @method_options, @hide = nil
399
417
  true
400
418
  elsif all_commands[meth] || meth == "method_missing"
401
419
  true
@@ -477,7 +495,6 @@ class Thor
477
495
  map HELP_MAPPINGS => :help
478
496
 
479
497
  desc "help [COMMAND]", "Describe available commands or one specific command"
480
- disable_class_options
481
498
  def help(command = nil, subcommand = false)
482
499
  if command
483
500
  if self.class.subcommands.include? command
@@ -1,4 +1,3 @@
1
- require "fileutils"
2
1
  require "uri"
3
2
  require "thor/core_ext/io_binary_read"
4
3
  require "thor/actions/create_file"
@@ -114,8 +113,10 @@ class Thor
114
113
  # the script started).
115
114
  #
116
115
  def relative_to_original_destination_root(path, remove_dot = true)
117
- path = path.dup
118
- if path.gsub!(@destination_stack[0], ".")
116
+ root = @destination_stack[0]
117
+ if path.start_with?(root) && [File::SEPARATOR, File::ALT_SEPARATOR, nil, ''].include?(path[root.size..root.size])
118
+ path = path.dup
119
+ path[0...root.size] = '.'
119
120
  remove_dot ? (path[2..-1] || "") : path
120
121
  else
121
122
  path
@@ -141,7 +142,7 @@ class Thor
141
142
  end
142
143
  end
143
144
 
144
- message = "Could not find #{file.inspect} in any of your source paths. "
145
+ message = "Could not find #{file.inspect} in any of your source paths. ".dup
145
146
 
146
147
  unless self.class.source_root
147
148
  message << "Please invoke #{self.class.name}.source_root(PATH) with the PATH containing your templates. "
@@ -175,6 +176,7 @@ class Thor
175
176
 
176
177
  # If the directory doesnt exist and we're not pretending
177
178
  if !File.exist?(destination_root) && !pretend
179
+ require "fileutils"
178
180
  FileUtils.mkdir_p(destination_root)
179
181
  end
180
182
 
@@ -182,6 +184,7 @@ class Thor
182
184
  # In pretend mode, just yield down to the block
183
185
  block.arity == 1 ? yield(destination_root) : yield
184
186
  else
187
+ require "fileutils"
185
188
  FileUtils.cd(destination_root) { block.arity == 1 ? yield(destination_root) : yield }
186
189
  end
187
190
 
@@ -216,6 +219,7 @@ class Thor
216
219
  shell.padding += 1 if verbose
217
220
 
218
221
  contents = if is_uri
222
+ require "open-uri"
219
223
  open(path, "Accept" => "application/x-thor-template", &:read)
220
224
  else
221
225
  open(path, &:read)
@@ -251,7 +255,16 @@ class Thor
251
255
 
252
256
  say_status :run, desc, config.fetch(:verbose, true)
253
257
 
254
- !options[:pretend] && config[:capture] ? `#{command}` : system(command.to_s)
258
+ return if options[:pretend]
259
+
260
+ result = config[:capture] ? `#{command}` : system(command.to_s)
261
+
262
+ if config[:abort_on_failure]
263
+ success = config[:capture] ? $?.success? : result
264
+ abort unless success
265
+ end
266
+
267
+ result
255
268
  end
256
269
 
257
270
  # Executes a ruby script (taking into account WIN32 platform quirks).
@@ -58,6 +58,7 @@ class Thor
58
58
 
59
59
  def invoke!
60
60
  invoke_with_conflict_check do
61
+ require "fileutils"
61
62
  FileUtils.mkdir_p(File.dirname(destination))
62
63
  File.open(destination, "wb") { |f| f.write render }
63
64
  end
@@ -38,6 +38,7 @@ class Thor
38
38
 
39
39
  def invoke!
40
40
  invoke_with_conflict_check do
41
+ require "fileutils"
41
42
  FileUtils.mkdir_p(File.dirname(destination))
42
43
  # Create a symlink by default
43
44
  config[:symbolic] = true if config[:symbolic].nil?
@@ -48,12 +48,14 @@ class Thor
48
48
 
49
49
  def invoke!
50
50
  invoke_with_conflict_check do
51
+ require "fileutils"
51
52
  ::FileUtils.mkdir_p(destination)
52
53
  end
53
54
  end
54
55
 
55
56
  def revoke!
56
57
  say_status :remove, :red
58
+ require "fileutils"
57
59
  ::FileUtils.rm_rf(destination) if !pretend? && exists?
58
60
  given_destination
59
61
  end
@@ -112,11 +114,17 @@ class Thor
112
114
  if exists?
113
115
  on_conflict_behavior(&block)
114
116
  else
115
- say_status :create, :green
116
117
  yield unless pretend?
118
+ say_status :create, :green
117
119
  end
118
120
 
119
121
  destination
122
+ rescue Errno::EISDIR, Errno::EEXIST
123
+ on_file_clash_behavior
124
+ end
125
+
126
+ def on_file_clash_behavior
127
+ say_status :file_clash, :red
120
128
  end
121
129
 
122
130
  # What to do when the destination file already exists.
@@ -1,5 +1,4 @@
1
1
  require "erb"
2
- require "open-uri"
3
2
 
4
3
  class Thor
5
4
  module Actions
@@ -61,6 +60,9 @@ class Thor
61
60
  # destination. If a block is given instead of destination, the content of
62
61
  # the url is yielded and used as location.
63
62
  #
63
+ # +get+ relies on open-uri, so passing application user input would provide
64
+ # a command injection attack vector.
65
+ #
64
66
  # ==== Parameters
65
67
  # source<String>:: the address of the given content.
66
68
  # destination<String>:: the relative path to the destination root.
@@ -78,7 +80,12 @@ class Thor
78
80
  config = args.last.is_a?(Hash) ? args.pop : {}
79
81
  destination = args.first
80
82
 
81
- source = File.expand_path(find_in_source_paths(source.to_s)) unless source =~ %r{^https?\://}
83
+ if source =~ %r{^https?\://}
84
+ require "open-uri"
85
+ else
86
+ source = File.expand_path(find_in_source_paths(source.to_s))
87
+ end
88
+
82
89
  render = open(source) { |input| input.binmode.read }
83
90
 
84
91
  destination ||= if block_given?
@@ -113,7 +120,15 @@ class Thor
113
120
  context = config.delete(:context) || instance_eval("binding")
114
121
 
115
122
  create_file destination, nil, config do
116
- content = CapturableERB.new(::File.binread(source), nil, "-", "@output_buffer").result(context)
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
129
+ content = capturable_erb.tap do |erb|
130
+ erb.filename = source
131
+ end.result(context)
117
132
  content = yield(content) if block
118
133
  content
119
134
  end
@@ -134,7 +149,10 @@ class Thor
134
149
  return unless behavior == :invoke
135
150
  path = File.expand_path(path, destination_root)
136
151
  say_status :chmod, relative_to_original_destination_root(path), config.fetch(:verbose, true)
137
- FileUtils.chmod_R(mode, path) unless options[:pretend]
152
+ unless options[:pretend]
153
+ require "fileutils"
154
+ FileUtils.chmod_R(mode, path)
155
+ end
138
156
  end
139
157
 
140
158
  # Prepend text to a file. Since it depends on insert_into_file, it's reversible.
@@ -204,6 +222,29 @@ class Thor
204
222
  insert_into_file(path, *(args << config), &block)
205
223
  end
206
224
 
225
+ # Injects text right after the module definition. Since it depends on
226
+ # insert_into_file, it's reversible.
227
+ #
228
+ # ==== Parameters
229
+ # path<String>:: path of the file to be changed
230
+ # module_name<String|Class>:: the module to be manipulated
231
+ # data<String>:: the data to append to the class, can be also given as a block.
232
+ # config<Hash>:: give :verbose => false to not log the status.
233
+ #
234
+ # ==== Examples
235
+ #
236
+ # inject_into_module "app/helpers/application_helper.rb", ApplicationHelper, " def help; 'help'; end\n"
237
+ #
238
+ # inject_into_module "app/helpers/application_helper.rb", ApplicationHelper do
239
+ # " def help; 'help'; end\n"
240
+ # end
241
+ #
242
+ def inject_into_module(path, module_name, *args, &block)
243
+ config = args.last.is_a?(Hash) ? args.pop : {}
244
+ config[:after] = /module #{module_name}\n|module #{module_name} .*\n/
245
+ insert_into_file(path, *(args << config), &block)
246
+ end
247
+
207
248
  # Run a regular expression replacement on a file.
208
249
  #
209
250
  # ==== Parameters
@@ -269,7 +310,7 @@ class Thor
269
310
  def comment_lines(path, flag, *args)
270
311
  flag = flag.respond_to?(:source) ? flag.source : flag
271
312
 
272
- gsub_file(path, /^(\s*)([^#|\n]*#{flag})/, '\1# \2', *args)
313
+ gsub_file(path, /^(\s*)([^#\n]*#{flag})/, '\1# \2', *args)
273
314
  end
274
315
 
275
316
  # Removes a file at the given location.
@@ -288,7 +329,10 @@ class Thor
288
329
  path = File.expand_path(path, destination_root)
289
330
 
290
331
  say_status :remove, relative_to_original_destination_root(path), config.fetch(:verbose, true)
291
- ::FileUtils.rm_rf(path) if !options[:pretend] && File.exist?(path)
332
+ if !options[:pretend] && File.exist?(path)
333
+ require "fileutils"
334
+ ::FileUtils.rm_rf(path)
335
+ end
292
336
  end
293
337
  alias_method :remove_dir, :remove_file
294
338
 
@@ -305,8 +349,10 @@ class Thor
305
349
  with_output_buffer { yield(*args) }
306
350
  end
307
351
 
308
- def with_output_buffer(buf = "") #:nodoc:
309
- self.output_buffer, old_buffer = buf, output_buffer
352
+ def with_output_buffer(buf = "".dup) #:nodoc:
353
+ raise ArgumentError, "Buffer can not be a frozen object" if buf.frozen?
354
+ old_buffer = output_buffer
355
+ self.output_buffer = buf
310
356
  yield
311
357
  output_buffer
312
358
  ensure
@@ -319,7 +365,7 @@ class Thor
319
365
  def set_eoutvar(compiler, eoutvar = "_erbout")
320
366
  compiler.put_cmd = "#{eoutvar}.concat"
321
367
  compiler.insert_cmd = "#{eoutvar}.concat"
322
- compiler.pre_cmd = ["#{eoutvar} = ''"]
368
+ compiler.pre_cmd = ["#{eoutvar} = ''.dup"]
323
369
  compiler.post_cmd = [eoutvar]
324
370
  end
325
371
  end
@@ -53,7 +53,13 @@ class Thor
53
53
  replacement + '\0'
54
54
  end
55
55
 
56
- replace!(/#{flag}/, content, config[:force])
56
+ if exists?
57
+ replace!(/#{flag}/, content, config[:force])
58
+ else
59
+ unless pretend?
60
+ raise Thor::Error, "The file #{ destination } does not appear to exist"
61
+ end
62
+ end
57
63
  end
58
64
 
59
65
  def revoke!
@@ -91,8 +97,8 @@ class Thor
91
97
  # Adds the content to the file.
92
98
  #
93
99
  def replace!(regexp, string, force)
94
- return if base.options[:pretend]
95
- content = File.binread(destination)
100
+ return if pretend?
101
+ content = File.read(destination)
96
102
  if force || !content.include?(replacement)
97
103
  content.gsub!(regexp, string)
98
104
  File.open(destination, "wb") { |file| file.write(content) }
@@ -42,7 +42,7 @@ class Thor
42
42
  # config<Hash>:: Configuration for this Thor class.
43
43
  #
44
44
  def initialize(args = [], local_options = {}, config = {})
45
- parse_options = config[:current_command] && config[:current_command].disable_class_options ? {} : self.class.class_options
45
+ parse_options = self.class.class_options
46
46
 
47
47
  # The start method splits inbound arguments at the first argument
48
48
  # that looks like an option (starts with - or --). It then calls
@@ -65,7 +65,8 @@ class Thor
65
65
  # declared options from the array. This will leave us with
66
66
  # a list of arguments that weren't declared.
67
67
  stop_on_unknown = self.class.stop_on_unknown_option? config[:current_command]
68
- opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown)
68
+ disable_required_check = self.class.disable_required_check? config[:current_command]
69
+ opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check)
69
70
  self.options = opts.parse(array_options)
70
71
  self.options = config[:class_options].merge(options) if config[:class_options]
71
72
 
@@ -112,7 +113,7 @@ class Thor
112
113
  end
113
114
 
114
115
  # Whenever a class inherits from Thor or Thor::Group, we should track the
115
- # class and the file on Thor::Base. This is the method responsable for it.
116
+ # class and the file on Thor::Base. This is the method responsible for it.
116
117
  #
117
118
  def register_klass_file(klass) #:nodoc:
118
119
  file = caller[1].match(/(.*):\d+/)[1]
@@ -150,6 +151,21 @@ class Thor
150
151
  !!check_unknown_options
151
152
  end
152
153
 
154
+ # If you want to raise an error when the default value of an option does not match
155
+ # the type call check_default_type!
156
+ # This is disabled by default for compatibility.
157
+ def check_default_type!
158
+ @check_default_type = true
159
+ end
160
+
161
+ def check_default_type #:nodoc:
162
+ @check_default_type ||= from_superclass(:check_default_type, false)
163
+ end
164
+
165
+ def check_default_type? #:nodoc:
166
+ !!check_default_type
167
+ end
168
+
153
169
  # If true, option parsing is suspended as soon as an unknown option or a
154
170
  # regular argument is encountered. All remaining arguments are passed to
155
171
  # the command as regular arguments.
@@ -157,6 +173,12 @@ class Thor
157
173
  false
158
174
  end
159
175
 
176
+ # If true, option set will not suspend the execution of the command when
177
+ # a required option is not provided.
178
+ def disable_required_check?(command_name) #:nodoc:
179
+ false
180
+ end
181
+
160
182
  # If you want only strict string args (useful when cascading thor classes),
161
183
  # call strict_args_position! This is disabled by default to allow dynamic
162
184
  # invocations.
@@ -444,13 +466,13 @@ class Thor
444
466
  dispatch(nil, given_args.dup, nil, config)
445
467
  rescue Thor::Error => e
446
468
  config[:debug] || ENV["THOR_DEBUG"] == "1" ? (raise e) : config[:shell].error(e.message)
447
- exit(1) if exit_on_failure?
469
+ exit(false) if exit_on_failure?
448
470
  rescue Errno::EPIPE
449
471
  # This happens if a thor command is piped to something like `head`,
450
472
  # which closes the pipe when it's done reading. This will also
451
473
  # mean that if the pipe is closed, further unnecessary
452
474
  # computation will not occur.
453
- exit(0)
475
+ exit(true)
454
476
  end
455
477
 
456
478
  # Allows to use private methods from parent in child classes as commands.
@@ -471,13 +493,13 @@ class Thor
471
493
  alias_method :public_task, :public_command
472
494
 
473
495
  def handle_no_command_error(command, has_namespace = $thor_runner) #:nodoc:
474
- raise UndefinedCommandError, "Could not find command #{command.inspect} in #{namespace.inspect} namespace." if has_namespace
475
- raise UndefinedCommandError, "Could not find command #{command.inspect}."
496
+ raise UndefinedCommandError.new(command, all_commands.keys, (namespace if has_namespace))
476
497
  end
477
498
  alias_method :handle_no_task_error, :handle_no_command_error
478
499
 
479
500
  def handle_argument_error(command, error, args, arity) #:nodoc:
480
- msg = "ERROR: \"#{basename} #{command.name}\" was called with "
501
+ name = [command.ancestor_name, command.name].compact.join(" ")
502
+ msg = "ERROR: \"#{basename} #{name}\" was called with ".dup
481
503
  msg << "no arguments" if args.empty?
482
504
  msg << "arguments " << args.inspect unless args.empty?
483
505
  msg << "\nUsage: #{banner(command).inspect}"
@@ -541,7 +563,7 @@ class Thor
541
563
  # options<Hash>:: Described in both class_option and method_option.
542
564
  # scope<Hash>:: Options hash that is being built up
543
565
  def build_option(name, options, scope) #:nodoc:
544
- scope[name] = Thor::Option.new(name, options)
566
+ scope[name] = Thor::Option.new(name, options.merge(:check_default_type => check_default_type?))
545
567
  end
546
568
 
547
569
  # Receives a hash of options, parse them and add to the scope. This is a
@@ -1,9 +1,9 @@
1
1
  class Thor
2
- class Command < Struct.new(:name, :description, :long_description, :usage, :options, :disable_class_options)
2
+ class Command < Struct.new(:name, :description, :long_description, :usage, :options, :ancestor_name)
3
3
  FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/
4
4
 
5
- def initialize(name, description, long_description, usage, options = nil, disable_class_options = false)
6
- super(name.to_s, description, long_description, usage, options || {}, disable_class_options)
5
+ def initialize(name, description, long_description, usage, options = nil)
6
+ super(name.to_s, description, long_description, usage, options || {})
7
7
  end
8
8
 
9
9
  def initialize_copy(other) #:nodoc:
@@ -39,13 +39,15 @@ class Thor
39
39
  # Returns the formatted usage by injecting given required arguments
40
40
  # and required options into the given usage.
41
41
  def formatted_usage(klass, namespace = true, subcommand = false)
42
- if namespace
42
+ if ancestor_name
43
+ formatted = "#{ancestor_name} ".dup # add space
44
+ elsif namespace
43
45
  namespace = klass.namespace
44
- formatted = "#{namespace.gsub(/^(default)/, '')}:"
46
+ formatted = "#{namespace.gsub(/^(default)/, '')}:".dup
45
47
  end
46
- formatted = "#{klass.namespace.split(':').last} " if subcommand
48
+ formatted ||= "#{klass.namespace.split(':').last} ".dup if subcommand
47
49
 
48
- formatted ||= ""
50
+ formatted ||= "".dup
49
51
 
50
52
  # Add usage with required arguments
51
53
  formatted << if klass && !klass.arguments.empty?
@@ -51,6 +51,18 @@ class Thor
51
51
  self
52
52
  end
53
53
 
54
+ def reverse_merge(other)
55
+ self.class.new(other).merge(self)
56
+ end
57
+
58
+ def reverse_merge!(other_hash)
59
+ replace(reverse_merge(other_hash))
60
+ end
61
+
62
+ def replace(other_hash)
63
+ super(other_hash)
64
+ end
65
+
54
66
  # Convert to a Hash with String keys.
55
67
  def to_hash
56
68
  Hash.new(default).merge!(self)
@@ -1,4 +1,23 @@
1
1
  class Thor
2
+ Correctable =
3
+ begin
4
+ require 'did_you_mean'
5
+
6
+ # In order to support versions of Ruby that don't have keyword
7
+ # arguments, we need our own spell checker class that doesn't take key
8
+ # words. Even though this code wouldn't be hit because of the check
9
+ # above, it's still necessary because the interpreter would otherwise be
10
+ # unable to parse the file.
11
+ class NoKwargSpellChecker < DidYouMean::SpellChecker # :nodoc:
12
+ def initialize(dictionary)
13
+ @dictionary = dictionary
14
+ end
15
+ end
16
+
17
+ DidYouMean::Correctable
18
+ rescue LoadError, NameError
19
+ end
20
+
2
21
  # Thor::Error is raised when it's caused by wrong usage of thor classes. Those
3
22
  # errors have their backtrace suppressed and are nicely shown to the user.
4
23
  #
@@ -10,6 +29,35 @@ class Thor
10
29
 
11
30
  # Raised when a command was not found.
12
31
  class UndefinedCommandError < Error
32
+ class SpellChecker
33
+ attr_reader :error
34
+
35
+ def initialize(error)
36
+ @error = error
37
+ end
38
+
39
+ def corrections
40
+ @corrections ||= spell_checker.correct(error.command).map(&:inspect)
41
+ end
42
+
43
+ def spell_checker
44
+ NoKwargSpellChecker.new(error.all_commands)
45
+ end
46
+ end
47
+
48
+ attr_reader :command, :all_commands
49
+
50
+ def initialize(command, all_commands, namespace)
51
+ @command = command
52
+ @all_commands = all_commands
53
+
54
+ message = "Could not find command #{command.inspect}"
55
+ message = namespace ? "#{message} in #{namespace.inspect} namespace." : "#{message}."
56
+
57
+ super(message)
58
+ end
59
+
60
+ prepend Correctable if Correctable
13
61
  end
14
62
  UndefinedTaskError = UndefinedCommandError
15
63
 
@@ -22,6 +70,33 @@ class Thor
22
70
  end
23
71
 
24
72
  class UnknownArgumentError < Error
73
+ class SpellChecker
74
+ attr_reader :error
75
+
76
+ def initialize(error)
77
+ @error = error
78
+ end
79
+
80
+ def corrections
81
+ @corrections ||=
82
+ error.unknown.flat_map { |unknown| spell_checker.correct(unknown) }.uniq.map(&:inspect)
83
+ end
84
+
85
+ def spell_checker
86
+ @spell_checker ||= NoKwargSpellChecker.new(error.switches)
87
+ end
88
+ end
89
+
90
+ attr_reader :switches, :unknown
91
+
92
+ def initialize(switches, unknown)
93
+ @switches = switches
94
+ @unknown = unknown
95
+
96
+ super("Unknown switches #{unknown.map(&:inspect).join(', ')}")
97
+ end
98
+
99
+ prepend Correctable if Correctable
25
100
  end
26
101
 
27
102
  class RequiredArgumentMissingError < InvocationError
@@ -29,4 +104,11 @@ class Thor
29
104
 
30
105
  class MalformattedArgumentError < InvocationError
31
106
  end
107
+
108
+ if Correctable
109
+ DidYouMean::SPELL_CHECKERS.merge!(
110
+ 'Thor::UndefinedCommandError' => UndefinedCommandError::SpellChecker,
111
+ 'Thor::UnknownArgumentError' => UnknownArgumentError::SpellChecker
112
+ )
113
+ end
32
114
  end
@@ -61,7 +61,7 @@ class Thor::Group
61
61
  invocations[name] = false
62
62
  invocation_blocks[name] = block if block_given?
63
63
 
64
- class_eval <<-METHOD, __FILE__, __LINE__
64
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
65
65
  def _invoke_#{name.to_s.gsub(/\W/, '_')}
66
66
  klass, command = self.class.prepare_for_invocation(nil, #{name.inspect})
67
67
 
@@ -120,7 +120,7 @@ class Thor::Group
120
120
  invocations[name] = true
121
121
  invocation_blocks[name] = block if block_given?
122
122
 
123
- class_eval <<-METHOD, __FILE__, __LINE__
123
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
124
124
  def _invoke_from_option_#{name.to_s.gsub(/\W/, '_')}
125
125
  return unless options[#{name.inspect}]
126
126
 
@@ -205,7 +205,7 @@ class Thor::Group
205
205
  alias_method :printable_tasks, :printable_commands
206
206
 
207
207
  def handle_argument_error(command, error, _args, arity) #:nodoc:
208
- msg = "#{basename} #{command.name} takes #{arity} argument"
208
+ msg = "#{basename} #{command.name} takes #{arity} argument".dup
209
209
  msg << "s" if arity > 1
210
210
  msg << ", but it should not."
211
211
  raise error, msg
@@ -23,6 +23,8 @@ class Thor
23
23
  if echo?
24
24
  $stdin.gets
25
25
  else
26
+ # Lazy-load io/console since it is gem-ified as of 2.3
27
+ require "io/console" if RUBY_VERSION > "1.9.2"
26
28
  $stdin.noecho(&:gets)
27
29
  end
28
30
  end
@@ -5,6 +5,7 @@ class Thor
5
5
  VALID_TYPES = [:boolean, :numeric, :hash, :array, :string]
6
6
 
7
7
  def initialize(name, options = {})
8
+ @check_default_type = options[:check_default_type]
8
9
  options[:required] = false unless options.key?(:required)
9
10
  super
10
11
  @lazy_default = options[:lazy_default]
@@ -80,12 +81,12 @@ class Thor
80
81
 
81
82
  def usage(padding = 0)
82
83
  sample = if banner && !banner.to_s.empty?
83
- "#{switch_name}=#{banner}"
84
+ "#{switch_name}=#{banner}".dup
84
85
  else
85
86
  switch_name
86
87
  end
87
88
 
88
- sample = "[#{sample}]" unless required?
89
+ sample = "[#{sample}]".dup unless required?
89
90
 
90
91
  if boolean?
91
92
  sample << ", [#{dasherize('no-' + human_name)}]" unless (name == "force") || name.start_with?("no-")
@@ -110,7 +111,7 @@ class Thor
110
111
 
111
112
  def validate!
112
113
  raise ArgumentError, "An option cannot be boolean and required." if boolean? && required?
113
- validate_default_type!
114
+ validate_default_type! if @check_default_type
114
115
  end
115
116
 
116
117
  def validate_default_type!
@@ -127,8 +128,7 @@ class Thor
127
128
  @default.class.name.downcase.to_sym
128
129
  end
129
130
 
130
- # TODO: This should raise an ArgumentError in a future version of Thor
131
- warn "Expected #{@type} default value for '#{switch_name}'; got #{@default.inspect} (#{default_type})" unless default_type == @type
131
+ raise ArgumentError, "Expected #{@type} default value for '#{switch_name}'; got #{@default.inspect} (#{default_type})" unless default_type == @type
132
132
  end
133
133
 
134
134
  def dasherized?
@@ -18,19 +18,20 @@ class Thor
18
18
  when Hash
19
19
  "--#{key} #{value.map { |k, v| "#{k}:#{v}" }.join(' ')}"
20
20
  when nil, false
21
- ""
21
+ nil
22
22
  else
23
23
  "--#{key} #{value.inspect}"
24
24
  end
25
- end.join(" ")
25
+ end.compact.join(" ")
26
26
  end
27
27
 
28
28
  # Takes a hash of Thor::Option and a hash with defaults.
29
29
  #
30
30
  # If +stop_on_unknown+ is true, #parse will stop as soon as it encounters
31
31
  # an unknown option or a regular argument.
32
- def initialize(hash_options = {}, defaults = {}, stop_on_unknown = false)
32
+ def initialize(hash_options = {}, defaults = {}, stop_on_unknown = false, disable_required_check = false)
33
33
  @stop_on_unknown = stop_on_unknown
34
+ @disable_required_check = disable_required_check
34
35
  options = hash_options.values
35
36
  super(options)
36
37
 
@@ -43,6 +44,7 @@ class Thor
43
44
  @shorts = {}
44
45
  @switches = {}
45
46
  @extra = []
47
+ @stopped_parsing_after_extra_index = nil
46
48
 
47
49
  options.each do |option|
48
50
  @switches[option.switch_name] = option
@@ -65,6 +67,7 @@ class Thor
65
67
  if result == OPTS_END
66
68
  shift
67
69
  @parsing_options = false
70
+ @stopped_parsing_after_extra_index ||= @extra.size
68
71
  super
69
72
  else
70
73
  result
@@ -98,6 +101,7 @@ class Thor
98
101
  elsif @stop_on_unknown
99
102
  @parsing_options = false
100
103
  @extra << shifted
104
+ @stopped_parsing_after_extra_index ||= @extra.size
101
105
  @extra << shift while peek
102
106
  break
103
107
  elsif match
@@ -111,7 +115,7 @@ class Thor
111
115
  end
112
116
  end
113
117
 
114
- check_requirement!
118
+ check_requirement! unless @disable_required_check
115
119
 
116
120
  assigns = Thor::CoreExt::HashWithIndifferentAccess.new(@assigns)
117
121
  assigns.freeze
@@ -119,9 +123,11 @@ class Thor
119
123
  end
120
124
 
121
125
  def check_unknown!
126
+ to_check = @stopped_parsing_after_extra_index ? @extra[0...@stopped_parsing_after_extra_index] : @extra
127
+
122
128
  # an unknown option starts with - or -- and has no more --'s afterward.
123
- unknown = @extra.select { |str| str =~ /^--?(?:(?!--).)*$/ }
124
- raise UnknownArgumentError, "Unknown switches '#{unknown.join(', ')}'" unless unknown.empty?
129
+ unknown = to_check.select { |str| str =~ /^--?(?:(?!--).)*$/ }
130
+ raise UnknownArgumentError.new(@switches.keys, unknown) unless unknown.empty?
125
131
  end
126
132
 
127
133
  protected
@@ -188,7 +194,7 @@ class Thor
188
194
  shift
189
195
  false
190
196
  else
191
- true
197
+ !no_or_skip?(switch)
192
198
  end
193
199
  else
194
200
  @switches.key?(switch) || !no_or_skip?(switch)
@@ -2,8 +2,6 @@ require "thor"
2
2
  require "thor/group"
3
3
  require "thor/core_ext/io_binary_read"
4
4
 
5
- require "fileutils"
6
- require "open-uri"
7
5
  require "yaml"
8
6
  require "digest/md5"
9
7
  require "pathname"
@@ -104,6 +102,7 @@ class Thor::Runner < Thor #:nodoc: # rubocop:disable ClassLength
104
102
  if package == :file
105
103
  File.open(destination, "w") { |f| f.puts contents }
106
104
  else
105
+ require "fileutils"
107
106
  FileUtils.cp_r(name, destination)
108
107
  end
109
108
 
@@ -120,6 +119,7 @@ class Thor::Runner < Thor #:nodoc: # rubocop:disable ClassLength
120
119
  def uninstall(name)
121
120
  raise Error, "Can't find module '#{name}'" unless thor_yaml[name]
122
121
  say "Uninstalling #{name}."
122
+ require "fileutils"
123
123
  FileUtils.rm_rf(File.join(thor_root, (thor_yaml[name][:filename]).to_s))
124
124
 
125
125
  thor_yaml.delete(name)
@@ -138,6 +138,7 @@ class Thor::Runner < Thor #:nodoc: # rubocop:disable ClassLength
138
138
  self.options = options.merge("as" => name)
139
139
 
140
140
  if File.directory? File.expand_path(name)
141
+ require "fileutils"
141
142
  FileUtils.rm_rf(File.join(thor_root, old_filename))
142
143
 
143
144
  thor_yaml.delete(old_filename)
@@ -194,6 +195,7 @@ private
194
195
  yaml_file = File.join(thor_root, "thor.yml")
195
196
 
196
197
  unless File.exist?(yaml_file)
198
+ require "fileutils"
197
199
  FileUtils.mkdir_p(thor_root)
198
200
  yaml_file = File.join(thor_root, "thor.yml")
199
201
  FileUtils.touch(yaml_file)
@@ -55,7 +55,7 @@ class Thor
55
55
 
56
56
  # Common methods that are delegated to the shell.
57
57
  SHELL_DELEGATED_METHODS.each do |method|
58
- module_eval <<-METHOD, __FILE__, __LINE__
58
+ module_eval <<-METHOD, __FILE__, __LINE__ + 1
59
59
  def #{method}(*args,&block)
60
60
  shell.#{method}(*args,&block)
61
61
  end
@@ -1,9 +1,8 @@
1
- require "tempfile"
2
- require "io/console" if RUBY_VERSION > "1.9.2"
3
-
4
1
  class Thor
5
2
  module Shell
6
3
  class Basic
4
+ DEFAULT_TERMINAL_WIDTH = 80
5
+
7
6
  attr_accessor :base
8
7
  attr_reader :padding
9
8
 
@@ -48,6 +47,10 @@ class Thor
48
47
 
49
48
  # Asks something to the user and receives a response.
50
49
  #
50
+ # If a default value is specified it will be presented to the user
51
+ # and allows them to select that value with an empty response. This
52
+ # option is ignored when limited answers are supplied.
53
+ #
51
54
  # If asked to limit the correct responses, you can pass in an
52
55
  # array of acceptable answers. If one of those is not supplied,
53
56
  # they will be shown a message stating that one of those answers
@@ -64,6 +67,8 @@ class Thor
64
67
  # ==== Example
65
68
  # ask("What is your name?")
66
69
  #
70
+ # ask("What is the planet furthest from the sun?", :default => "Pluto")
71
+ #
67
72
  # ask("What is your favorite Neopolitan flavor?", :limited_to => ["strawberry", "chocolate", "vanilla"])
68
73
  #
69
74
  # ask("What is your password?", :echo => false)
@@ -110,7 +115,7 @@ class Thor
110
115
  status = set_color status, color, true if color
111
116
 
112
117
  buffer = "#{status}#{spaces}#{message}"
113
- buffer << "\n" unless buffer.end_with?("\n")
118
+ buffer = "#{buffer}\n" unless buffer.end_with?("\n")
114
119
 
115
120
  stdout.print(buffer)
116
121
  stdout.flush
@@ -165,7 +170,7 @@ class Thor
165
170
  colwidth = options[:colwidth]
166
171
  options[:truncate] = terminal_width if options[:truncate] == true
167
172
 
168
- formats << "%-#{colwidth + 2}s" if colwidth
173
+ formats << "%-#{colwidth + 2}s".dup if colwidth
169
174
  start = colwidth ? 1 : 0
170
175
 
171
176
  colcount = array.max { |a, b| a.size <=> b.size }.size
@@ -177,9 +182,9 @@ class Thor
177
182
  maximas << maxima
178
183
  formats << if index == colcount - 1
179
184
  # Don't output 2 trailing spaces when printing the last column
180
- "%-s"
185
+ "%-s".dup
181
186
  else
182
- "%-#{maxima + 2}s"
187
+ "%-#{maxima + 2}s".dup
183
188
  end
184
189
  end
185
190
 
@@ -187,7 +192,7 @@ class Thor
187
192
  formats << "%s"
188
193
 
189
194
  array.each do |row|
190
- sentence = ""
195
+ sentence = "".dup
191
196
 
192
197
  row.each_with_index do |column, index|
193
198
  maxima = maximas[index]
@@ -225,8 +230,20 @@ class Thor
225
230
  paras = message.split("\n\n")
226
231
 
227
232
  paras.map! do |unwrapped|
228
- unwrapped.strip.tr("\n", " ").squeeze(" ").gsub(/.{1,#{width}}(?:\s|\Z)/) { ($& + 5.chr).gsub(/\n\005/, "\n").gsub(/\005/, "\n") }
229
- end
233
+ counter = 0
234
+ unwrapped.split(" ").inject do |memo, word|
235
+ word = word.gsub(/\n\005/, "\n").gsub(/\005/, "\n")
236
+ counter = 0 if word.include? "\n"
237
+ if (counter + word.length + 1) < width
238
+ memo = "#{memo} #{word}"
239
+ counter += (word.length + 1)
240
+ else
241
+ memo = "#{memo}\n#{word}"
242
+ counter = word.length
243
+ end
244
+ memo
245
+ end
246
+ end.compact!
230
247
 
231
248
  paras.each do |para|
232
249
  para.split("\n").each do |line|
@@ -242,11 +259,11 @@ class Thor
242
259
  #
243
260
  # ==== Parameters
244
261
  # destination<String>:: the destination file to solve conflicts
245
- # block<Proc>:: an optional block that returns the value to be used in diff
262
+ # block<Proc>:: an optional block that returns the value to be used in diff and merge
246
263
  #
247
264
  def file_collision(destination)
248
265
  return true if @always_force
249
- options = block_given? ? "[Ynaqdh]" : "[Ynaqh]"
266
+ options = block_given? ? "[Ynaqdhm]" : "[Ynaqh]"
250
267
 
251
268
  loop do
252
269
  answer = ask(
@@ -255,6 +272,9 @@ class Thor
255
272
  )
256
273
 
257
274
  case answer
275
+ when nil
276
+ say ""
277
+ return true
258
278
  when is?(:yes), is?(:force), ""
259
279
  return true
260
280
  when is?(:no), is?(:skip)
@@ -267,6 +287,13 @@ class Thor
267
287
  when is?(:diff)
268
288
  show_diff(destination, yield) if block_given?
269
289
  say "Retrying..."
290
+ when is?(:merge)
291
+ if block_given? && !merge_tool.empty?
292
+ merge(destination, yield)
293
+ return nil
294
+ end
295
+
296
+ say "Please specify merge tool to `THOR_MERGE` env."
270
297
  else
271
298
  say file_collision_help
272
299
  end
@@ -279,11 +306,11 @@ class Thor
279
306
  result = if ENV["THOR_COLUMNS"]
280
307
  ENV["THOR_COLUMNS"].to_i
281
308
  else
282
- unix? ? dynamic_width : 80
309
+ unix? ? dynamic_width : DEFAULT_TERMINAL_WIDTH
283
310
  end
284
- result < 10 ? 80 : result
311
+ result < 10 ? DEFAULT_TERMINAL_WIDTH : result
285
312
  rescue
286
- 80
313
+ DEFAULT_TERMINAL_WIDTH
287
314
  end
288
315
 
289
316
  # Called if something goes wrong during the execution. This is used by Thor
@@ -344,12 +371,14 @@ class Thor
344
371
  q - quit, abort
345
372
  d - diff, show the differences between the old and the new
346
373
  h - help, show this help
374
+ m - merge, run merge tool
347
375
  HELP
348
376
  end
349
377
 
350
378
  def show_diff(destination, content) #:nodoc:
351
379
  diff_cmd = ENV["THOR_DIFF"] || ENV["RAILS_DIFF"] || "diff -u"
352
380
 
381
+ require "tempfile"
353
382
  Tempfile.open(File.basename(destination), File.dirname(destination)) do |temp|
354
383
  temp.write content
355
384
  temp.rewind
@@ -411,7 +440,7 @@ class Thor
411
440
 
412
441
  return unless result
413
442
 
414
- result.strip!
443
+ result = result.strip
415
444
 
416
445
  if default && result == ""
417
446
  default
@@ -431,6 +460,23 @@ class Thor
431
460
  end
432
461
  correct_answer
433
462
  end
463
+
464
+ def merge(destination, content) #:nodoc:
465
+ require "tempfile"
466
+ Tempfile.open([File.basename(destination), File.extname(destination)], File.dirname(destination)) do |temp|
467
+ temp.write content
468
+ temp.rewind
469
+ system %(#{merge_tool} "#{temp.path}" "#{destination}")
470
+ end
471
+ end
472
+
473
+ def merge_tool #:nodoc:
474
+ @merge_tool ||= ENV["THOR_MERGE"] || git_merge_tool
475
+ end
476
+
477
+ def git_merge_tool #:nodoc:
478
+ `git config merge.tool`.rstrip rescue ""
479
+ end
434
480
  end
435
481
  end
436
482
  end
@@ -27,7 +27,7 @@ class Thor
27
27
  end
28
28
 
29
29
  # Receives a constant and converts it to a Thor namespace. Since Thor
30
- # commands can be added to a sandbox, this method is also responsable for
30
+ # commands can be added to a sandbox, this method is also responsible for
31
31
  # removing the sandbox namespace.
32
32
  #
33
33
  # This method should not be used in general because it's used to deal with
@@ -1,3 +1,3 @@
1
1
  class Thor
2
- VERSION = "0.19.4"
2
+ VERSION = "0.20.3"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.4
4
+ version: 0.20.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yehuda Katz
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-11-28 00:00:00.000000000 Z
12
+ date: 2018-11-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -91,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
91
  version: 1.3.5
92
92
  requirements: []
93
93
  rubyforge_project:
94
- rubygems_version: 2.5.2
94
+ rubygems_version: 2.7.6
95
95
  signing_key:
96
96
  specification_version: 4
97
97
  summary: Thor is a toolkit for building powerful command-line interfaces.