thor 0.19.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- require "thor/base"
1
+ require_relative "base"
2
2
 
3
3
  # Thor has a special class called Thor::Group. The main difference to Thor class
4
4
  # is that it invokes all commands at once. It also include some methods that allows
@@ -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
@@ -1,6 +1,7 @@
1
1
  class Thor
2
2
  module Invocation
3
3
  def self.included(base) #:nodoc:
4
+ super(base)
4
5
  base.extend ClassMethods
5
6
  end
6
7
 
@@ -1,5 +1,5 @@
1
- require "thor/line_editor/basic"
2
- require "thor/line_editor/readline"
1
+ require_relative "line_editor/basic"
2
+ require_relative "line_editor/readline"
3
3
 
4
4
  class Thor
5
5
  module LineEditor
@@ -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"
26
28
  $stdin.noecho(&:gets)
27
29
  end
28
30
  end
@@ -1,19 +1,19 @@
1
- begin
2
- require "readline"
3
- rescue LoadError
4
- end
5
-
6
1
  class Thor
7
2
  module LineEditor
8
3
  class Readline < Basic
9
4
  def self.available?
5
+ begin
6
+ require "readline"
7
+ rescue LoadError
8
+ end
9
+
10
10
  Object.const_defined?(:Readline)
11
11
  end
12
12
 
13
13
  def readline
14
14
  if echo?
15
15
  ::Readline.completion_append_character = nil
16
- # Ruby 1.8.7 does not allow Readline.completion_proc= to receive nil.
16
+ # rb-readline does not allow Readline.completion_proc= to receive nil.
17
17
  if complete = completion_proc
18
18
  ::Readline.completion_proc = complete
19
19
  end
@@ -0,0 +1,29 @@
1
+ class Thor
2
+ class NestedContext
3
+ def initialize
4
+ @depth = 0
5
+ end
6
+
7
+ def enter
8
+ push
9
+
10
+ yield
11
+ ensure
12
+ pop
13
+ end
14
+
15
+ def entered?
16
+ @depth > 0
17
+ end
18
+
19
+ private
20
+
21
+ def push
22
+ @depth += 1
23
+ end
24
+
25
+ def pop
26
+ @depth -= 1
27
+ end
28
+ end
29
+ end
@@ -1,4 +1,4 @@
1
- require "thor/parser/argument"
2
- require "thor/parser/arguments"
3
- require "thor/parser/option"
4
- require "thor/parser/options"
1
+ require_relative "parser/argument"
2
+ require_relative "parser/arguments"
3
+ require_relative "parser/option"
4
+ require_relative "parser/options"
@@ -9,7 +9,7 @@ class Thor
9
9
  arguments = []
10
10
 
11
11
  args.each do |item|
12
- break if item =~ /^-/
12
+ break if item.is_a?(String) && item =~ /^-/
13
13
  arguments << item
14
14
  end
15
15
 
@@ -82,7 +82,7 @@ class Thor
82
82
  end
83
83
 
84
84
  def current_is_value?
85
- peek && peek.to_s !~ /^-/
85
+ peek && peek.to_s !~ /^-{1,2}\S+/
86
86
  end
87
87
 
88
88
  # Runs through the argument array getting strings that contains ":" and
@@ -1,16 +1,18 @@
1
1
  class Thor
2
2
  class Option < Argument #:nodoc:
3
- attr_reader :aliases, :group, :lazy_default, :hide
3
+ attr_reader :aliases, :group, :lazy_default, :hide, :repeatable
4
4
 
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)
10
+ @repeatable = options.fetch(:repeatable, false)
9
11
  super
10
- @lazy_default = options[:lazy_default]
11
- @group = options[:group].to_s.capitalize if options[:group]
12
- @aliases = Array(options[:aliases])
13
- @hide = options[:hide]
12
+ @lazy_default = options[:lazy_default]
13
+ @group = options[:group].to_s.capitalize if options[:group]
14
+ @aliases = Array(options[:aliases])
15
+ @hide = options[:hide]
14
16
  end
15
17
 
16
18
  # This parse quick options given as method_options. It makes several
@@ -80,12 +82,12 @@ class Thor
80
82
 
81
83
  def usage(padding = 0)
82
84
  sample = if banner && !banner.to_s.empty?
83
- "#{switch_name}=#{banner}"
85
+ "#{switch_name}=#{banner}".dup
84
86
  else
85
87
  switch_name
86
88
  end
87
89
 
88
- sample = "[#{sample}]" unless required?
90
+ sample = "[#{sample}]".dup unless required?
89
91
 
90
92
  if boolean?
91
93
  sample << ", [#{dasherize('no-' + human_name)}]" unless (name == "force") || name.start_with?("no-")
@@ -127,8 +129,19 @@ class Thor
127
129
  @default.class.name.downcase.to_sym
128
130
  end
129
131
 
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
132
+ expected_type = (@repeatable && @type != :hash) ? :array : @type
133
+
134
+ if default_type != expected_type
135
+ err = "Expected #{expected_type} default value for '#{switch_name}'; got #{@default.inspect} (#{default_type})"
136
+
137
+ if @check_default_type
138
+ raise ArgumentError, err
139
+ elsif @check_default_type == nil
140
+ Thor.deprecation_warning "#{err}.\n" +
141
+ 'This will be rejected in the future unless you explicitly pass the options `check_default_type: false`' +
142
+ ' or call `allow_incompatible_default_type!` in your code'
143
+ end
144
+ end
132
145
  end
133
146
 
134
147
  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
@@ -94,10 +97,12 @@ class Thor
94
97
 
95
98
  switch = normalize_switch(switch)
96
99
  option = switch_option(switch)
97
- @assigns[option.human_name] = parse_peek(switch, option)
100
+ result = parse_peek(switch, option)
101
+ assign_result!(option, result)
98
102
  elsif @stop_on_unknown
99
103
  @parsing_options = false
100
104
  @extra << shifted
105
+ @stopped_parsing_after_extra_index ||= @extra.size
101
106
  @extra << shift while peek
102
107
  break
103
108
  elsif match
@@ -111,7 +116,7 @@ class Thor
111
116
  end
112
117
  end
113
118
 
114
- check_requirement!
119
+ check_requirement! unless @disable_required_check
115
120
 
116
121
  assigns = Thor::CoreExt::HashWithIndifferentAccess.new(@assigns)
117
122
  assigns.freeze
@@ -119,13 +124,24 @@ class Thor
119
124
  end
120
125
 
121
126
  def check_unknown!
127
+ to_check = @stopped_parsing_after_extra_index ? @extra[0...@stopped_parsing_after_extra_index] : @extra
128
+
122
129
  # 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?
130
+ unknown = to_check.select { |str| str =~ /^--?(?:(?!--).)*$/ }
131
+ raise UnknownArgumentError.new(@switches.keys, unknown) unless unknown.empty?
125
132
  end
126
133
 
127
134
  protected
128
135
 
136
+ def assign_result!(option, result)
137
+ if option.repeatable && option.type == :hash
138
+ (@assigns[option.human_name] ||= {}).merge!(result)
139
+ elsif option.repeatable
140
+ (@assigns[option.human_name] ||= []) << result
141
+ else
142
+ @assigns[option.human_name] = result
143
+ end
144
+ end
129
145
  # Check if the current value in peek is a registered switch.
130
146
  #
131
147
  # Two booleans are returned. The first is true if the current value
@@ -155,7 +171,7 @@ class Thor
155
171
  end
156
172
 
157
173
  def switch?(arg)
158
- switch_option(normalize_switch(arg))
174
+ !switch_option(normalize_switch(arg)).nil?
159
175
  end
160
176
 
161
177
  def switch_option(arg)
@@ -188,7 +204,7 @@ class Thor
188
204
  shift
189
205
  false
190
206
  else
191
- true
207
+ @switches.key?(switch) || !no_or_skip?(switch)
192
208
  end
193
209
  else
194
210
  @switches.key?(switch) || !no_or_skip?(switch)
@@ -25,6 +25,7 @@ class Thor
25
25
  end
26
26
 
27
27
  def self.included(base)
28
+ super(base)
28
29
  # Hack. Make rakefile point to invoker, so rdoc task is generated properly.
29
30
  rakefile = File.basename(caller[0].match(/(.*):\d+/)[1])
30
31
  Rake.application.instance_variable_set(:@rakefile, rakefile)
@@ -1,14 +1,13 @@
1
- require "thor"
2
- require "thor/group"
3
- require "thor/core_ext/io_binary_read"
1
+ require_relative "../thor"
2
+ require_relative "group"
4
3
 
5
- require "fileutils"
6
- require "open-uri"
7
4
  require "yaml"
8
5
  require "digest/md5"
9
6
  require "pathname"
10
7
 
11
8
  class Thor::Runner < Thor #:nodoc: # rubocop:disable ClassLength
9
+ autoload :OpenURI, "open-uri"
10
+
12
11
  map "-T" => :list, "-i" => :install, "-u" => :update, "-v" => :version
13
12
 
14
13
  def self.banner(command, all = false, subcommand = false)
@@ -104,6 +103,7 @@ class Thor::Runner < Thor #:nodoc: # rubocop:disable ClassLength
104
103
  if package == :file
105
104
  File.open(destination, "w") { |f| f.puts contents }
106
105
  else
106
+ require "fileutils"
107
107
  FileUtils.cp_r(name, destination)
108
108
  end
109
109
 
@@ -112,7 +112,7 @@ class Thor::Runner < Thor #:nodoc: # rubocop:disable ClassLength
112
112
 
113
113
  desc "version", "Show Thor version"
114
114
  def version
115
- require "thor/version"
115
+ require_relative "version"
116
116
  say "Thor #{Thor::VERSION}"
117
117
  end
118
118
 
@@ -120,6 +120,7 @@ class Thor::Runner < Thor #:nodoc: # rubocop:disable ClassLength
120
120
  def uninstall(name)
121
121
  raise Error, "Can't find module '#{name}'" unless thor_yaml[name]
122
122
  say "Uninstalling #{name}."
123
+ require "fileutils"
123
124
  FileUtils.rm_rf(File.join(thor_root, (thor_yaml[name][:filename]).to_s))
124
125
 
125
126
  thor_yaml.delete(name)
@@ -138,6 +139,7 @@ class Thor::Runner < Thor #:nodoc: # rubocop:disable ClassLength
138
139
  self.options = options.merge("as" => name)
139
140
 
140
141
  if File.directory? File.expand_path(name)
142
+ require "fileutils"
141
143
  FileUtils.rm_rf(File.join(thor_root, old_filename))
142
144
 
143
145
  thor_yaml.delete(old_filename)
@@ -194,6 +196,7 @@ private
194
196
  yaml_file = File.join(thor_root, "thor.yml")
195
197
 
196
198
  unless File.exist?(yaml_file)
199
+ require "fileutils"
197
200
  FileUtils.mkdir_p(thor_root)
198
201
  yaml_file = File.join(thor_root, "thor.yml")
199
202
  FileUtils.touch(yaml_file)
@@ -24,9 +24,9 @@ class Thor
24
24
  SHELL_DELEGATED_METHODS = [:ask, :error, :set_color, :yes?, :no?, :say, :say_status, :print_in_columns, :print_table, :print_wrapped, :file_collision, :terminal_width]
25
25
  attr_writer :shell
26
26
 
27
- autoload :Basic, "thor/shell/basic"
28
- autoload :Color, "thor/shell/color"
29
- autoload :HTML, "thor/shell/html"
27
+ autoload :Basic, File.expand_path("shell/basic", __dir__)
28
+ autoload :Color, File.expand_path("shell/color", __dir__)
29
+ autoload :HTML, File.expand_path("shell/html", __dir__)
30
30
 
31
31
  # Add shell to initialize config values.
32
32
  #
@@ -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
@@ -422,15 +451,41 @@ class Thor
422
451
 
423
452
  def ask_filtered(statement, color, options)
424
453
  answer_set = options[:limited_to]
454
+ case_insensitive = options.fetch(:case_insensitive, false)
425
455
  correct_answer = nil
426
456
  until correct_answer
427
457
  answers = answer_set.join(", ")
428
458
  answer = ask_simply("#{statement} [#{answers}]", color, options)
429
- correct_answer = answer_set.include?(answer) ? answer : nil
459
+ correct_answer = answer_match(answer_set, answer, case_insensitive)
430
460
  say("Your response must be one of: [#{answers}]. Please try again.") unless correct_answer
431
461
  end
432
462
  correct_answer
433
463
  end
464
+
465
+ def answer_match(possibilities, answer, case_insensitive)
466
+ if case_insensitive
467
+ possibilities.detect{ |possibility| possibility.downcase == answer.downcase }
468
+ else
469
+ possibilities.detect{ |possibility| possibility == answer }
470
+ end
471
+ end
472
+
473
+ def merge(destination, content) #:nodoc:
474
+ require "tempfile"
475
+ Tempfile.open([File.basename(destination), File.extname(destination)], File.dirname(destination)) do |temp|
476
+ temp.write content
477
+ temp.rewind
478
+ system %(#{merge_tool} "#{temp.path}" "#{destination}")
479
+ end
480
+ end
481
+
482
+ def merge_tool #:nodoc:
483
+ @merge_tool ||= ENV["THOR_MERGE"] || git_merge_tool
484
+ end
485
+
486
+ def git_merge_tool #:nodoc:
487
+ `git config merge.tool`.rstrip rescue ""
488
+ end
434
489
  end
435
490
  end
436
491
  end