thor 0.19.4 → 0.20.3

Sign up to get free protection for your applications and to get access to all the features.
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.