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/actions/empty_directory"
1
+ require_relative "empty_directory"
2
2
 
3
3
  class Thor
4
4
  module Actions
@@ -21,9 +21,14 @@ class Thor
21
21
  # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n")
22
22
  # end
23
23
  #
24
+ WARNINGS = { unchanged_no_flag: 'File unchanged! The supplied flag value not found!' }
25
+
24
26
  def insert_into_file(destination, *args, &block)
25
27
  data = block_given? ? block : args.shift
26
- config = args.shift
28
+
29
+ config = args.shift || {}
30
+ config[:after] = /\z/ unless config.key?(:before) || config.key?(:after)
31
+
27
32
  action InjectIntoFile.new(self, destination, data, config)
28
33
  end
29
34
  alias_method :inject_into_file, :insert_into_file
@@ -45,15 +50,23 @@ class Thor
45
50
  end
46
51
 
47
52
  def invoke!
48
- say_status :invoke
49
-
50
53
  content = if @behavior == :after
51
54
  '\0' + replacement
52
55
  else
53
56
  replacement + '\0'
54
57
  end
55
58
 
56
- replace!(/#{flag}/, content, config[:force])
59
+ if exists?
60
+ if replace!(/#{flag}/, content, config[:force])
61
+ say_status(:invoke)
62
+ else
63
+ say_status(:unchanged, warning: WARNINGS[:unchanged_no_flag], color: :red)
64
+ end
65
+ else
66
+ unless pretend?
67
+ raise Thor::Error, "The file #{ destination } does not appear to exist"
68
+ end
69
+ end
57
70
  end
58
71
 
59
72
  def revoke!
@@ -72,7 +85,7 @@ class Thor
72
85
 
73
86
  protected
74
87
 
75
- def say_status(behavior)
88
+ def say_status(behavior, warning: nil, color: nil)
76
89
  status = if behavior == :invoke
77
90
  if flag == /\A/
78
91
  :prepend
@@ -81,21 +94,25 @@ class Thor
81
94
  else
82
95
  :insert
83
96
  end
97
+ elsif warning
98
+ warning
84
99
  else
85
100
  :subtract
86
101
  end
87
102
 
88
- super(status, config[:verbose])
103
+ super(status, (color || config[:verbose]))
89
104
  end
90
105
 
91
106
  # Adds the content to the file.
92
107
  #
93
108
  def replace!(regexp, string, force)
94
- return if base.options[:pretend]
95
- content = File.binread(destination)
109
+ return if pretend?
110
+ content = File.read(destination)
96
111
  if force || !content.include?(replacement)
97
- content.gsub!(regexp, string)
112
+ success = content.gsub!(regexp, string)
113
+
98
114
  File.open(destination, "wb") { |file| file.write(content) }
115
+ success
99
116
  end
100
117
  end
101
118
  end
@@ -1,17 +1,17 @@
1
- require "thor/command"
2
- require "thor/core_ext/hash_with_indifferent_access"
3
- require "thor/core_ext/ordered_hash"
4
- require "thor/error"
5
- require "thor/invocation"
6
- require "thor/parser"
7
- require "thor/shell"
8
- require "thor/line_editor"
9
- require "thor/util"
1
+ require_relative "command"
2
+ require_relative "core_ext/hash_with_indifferent_access"
3
+ require_relative "error"
4
+ require_relative "invocation"
5
+ require_relative "nested_context"
6
+ require_relative "parser"
7
+ require_relative "shell"
8
+ require_relative "line_editor"
9
+ require_relative "util"
10
10
 
11
11
  class Thor
12
- autoload :Actions, "thor/actions"
13
- autoload :RakeCompat, "thor/rake_compat"
14
- autoload :Group, "thor/group"
12
+ autoload :Actions, File.expand_path("actions", __dir__)
13
+ autoload :RakeCompat, File.expand_path("rake_compat", __dir__)
14
+ autoload :Group, File.expand_path("group", __dir__)
15
15
 
16
16
  # Shortcuts for help.
17
17
  HELP_MAPPINGS = %w(-h -? --help -D)
@@ -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
 
@@ -88,6 +89,7 @@ class Thor
88
89
 
89
90
  class << self
90
91
  def included(base) #:nodoc:
92
+ super(base)
91
93
  base.extend ClassMethods
92
94
  base.send :include, Invocation
93
95
  base.send :include, Shell
@@ -112,7 +114,7 @@ class Thor
112
114
  end
113
115
 
114
116
  # 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.
117
+ # class and the file on Thor::Base. This is the method responsible for it.
116
118
  #
117
119
  def register_klass_file(klass) #:nodoc:
118
120
  file = caller[1].match(/(.*):\d+/)[1]
@@ -150,6 +152,24 @@ class Thor
150
152
  !!check_unknown_options
151
153
  end
152
154
 
155
+ # If you want to raise an error when the default value of an option does not match
156
+ # the type call check_default_type!
157
+ # This will be the default; for compatibility a deprecation warning is issued if necessary.
158
+ def check_default_type!
159
+ @check_default_type = true
160
+ end
161
+
162
+ # If you want to use defaults that don't match the type of an option,
163
+ # either specify `check_default_type: false` or call `allow_incompatible_default_type!`
164
+ def allow_incompatible_default_type!
165
+ @check_default_type = false
166
+ end
167
+
168
+ def check_default_type #:nodoc:
169
+ @check_default_type = from_superclass(:check_default_type, nil) unless defined?(@check_default_type)
170
+ @check_default_type
171
+ end
172
+
153
173
  # If true, option parsing is suspended as soon as an unknown option or a
154
174
  # regular argument is encountered. All remaining arguments are passed to
155
175
  # the command as regular arguments.
@@ -157,6 +177,12 @@ class Thor
157
177
  false
158
178
  end
159
179
 
180
+ # If true, option set will not suspend the execution of the command when
181
+ # a required option is not provided.
182
+ def disable_required_check?(command_name) #:nodoc:
183
+ false
184
+ end
185
+
160
186
  # If you want only strict string args (useful when cascading thor classes),
161
187
  # call strict_args_position! This is disabled by default to allow dynamic
162
188
  # invocations.
@@ -331,22 +357,22 @@ class Thor
331
357
  # Returns the commands for this Thor class.
332
358
  #
333
359
  # ==== Returns
334
- # OrderedHash:: An ordered hash with commands names as keys and Thor::Command
335
- # objects as values.
360
+ # Hash:: An ordered hash with commands names as keys and Thor::Command
361
+ # objects as values.
336
362
  #
337
363
  def commands
338
- @commands ||= Thor::CoreExt::OrderedHash.new
364
+ @commands ||= Hash.new
339
365
  end
340
366
  alias_method :tasks, :commands
341
367
 
342
368
  # Returns the commands for this Thor class and all subclasses.
343
369
  #
344
370
  # ==== Returns
345
- # OrderedHash:: An ordered hash with commands names as keys and Thor::Command
346
- # objects as values.
371
+ # Hash:: An ordered hash with commands names as keys and Thor::Command
372
+ # objects as values.
347
373
  #
348
374
  def all_commands
349
- @all_commands ||= from_superclass(:all_commands, Thor::CoreExt::OrderedHash.new)
375
+ @all_commands ||= from_superclass(:all_commands, Hash.new)
350
376
  @all_commands.merge!(commands)
351
377
  end
352
378
  alias_method :all_tasks, :all_commands
@@ -393,14 +419,20 @@ class Thor
393
419
  # remove_command :this_is_not_a_command
394
420
  # end
395
421
  #
396
- def no_commands
397
- @no_commands = true
398
- yield
399
- ensure
400
- @no_commands = false
422
+ def no_commands(&block)
423
+ no_commands_context.enter(&block)
401
424
  end
425
+
402
426
  alias_method :no_tasks, :no_commands
403
427
 
428
+ def no_commands_context
429
+ @no_commands_context ||= NestedContext.new
430
+ end
431
+
432
+ def no_commands?
433
+ no_commands_context.entered?
434
+ end
435
+
404
436
  # Sets the namespace for the Thor or Thor::Group class. By default the
405
437
  # namespace is retrieved from the class name. If your Thor class is named
406
438
  # Scripts::MyScript, the help method, for example, will be called as:
@@ -444,13 +476,13 @@ class Thor
444
476
  dispatch(nil, given_args.dup, nil, config)
445
477
  rescue Thor::Error => e
446
478
  config[:debug] || ENV["THOR_DEBUG"] == "1" ? (raise e) : config[:shell].error(e.message)
447
- exit(1) if exit_on_failure?
479
+ exit(false) if exit_on_failure?
448
480
  rescue Errno::EPIPE
449
481
  # This happens if a thor command is piped to something like `head`,
450
482
  # which closes the pipe when it's done reading. This will also
451
483
  # mean that if the pipe is closed, further unnecessary
452
484
  # computation will not occur.
453
- exit(0)
485
+ exit(true)
454
486
  end
455
487
 
456
488
  # Allows to use private methods from parent in child classes as commands.
@@ -471,19 +503,25 @@ class Thor
471
503
  alias_method :public_task, :public_command
472
504
 
473
505
  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}."
506
+ raise UndefinedCommandError.new(command, all_commands.keys, (namespace if has_namespace))
476
507
  end
477
508
  alias_method :handle_no_task_error, :handle_no_command_error
478
509
 
479
510
  def handle_argument_error(command, error, args, arity) #:nodoc:
480
- msg = "ERROR: \"#{basename} #{command.name}\" was called with "
511
+ name = [command.ancestor_name, command.name].compact.join(" ")
512
+ msg = "ERROR: \"#{basename} #{name}\" was called with ".dup
481
513
  msg << "no arguments" if args.empty?
482
514
  msg << "arguments " << args.inspect unless args.empty?
483
- msg << "\nUsage: #{banner(command).inspect}"
515
+ msg << "\nUsage: \"#{banner(command).split("\n").join("\"\n \"")}\""
484
516
  raise InvocationError, msg
485
517
  end
486
518
 
519
+ # A flag that makes the process exit with status 1 if any error happens.
520
+ def exit_on_failure?
521
+ Thor.deprecation_warning "Thor exit with status 0 on errors. To keep this behavior, you must define `exit_on_failure?` in `#{self.name}`"
522
+ false
523
+ end
524
+
487
525
  protected
488
526
 
489
527
  # Prints the class options per group. If an option does not belong to
@@ -541,7 +579,7 @@ class Thor
541
579
  # options<Hash>:: Described in both class_option and method_option.
542
580
  # scope<Hash>:: Options hash that is being built up
543
581
  def build_option(name, options, scope) #:nodoc:
544
- scope[name] = Thor::Option.new(name, options)
582
+ scope[name] = Thor::Option.new(name, {:check_default_type => check_default_type}.merge!(options))
545
583
  end
546
584
 
547
585
  # Receives a hash of options, parse them and add to the scope. This is a
@@ -574,13 +612,15 @@ class Thor
574
612
  # Everytime someone inherits from a Thor class, register the klass
575
613
  # and file into baseclass.
576
614
  def inherited(klass)
615
+ super(klass)
577
616
  Thor::Base.register_klass_file(klass)
578
- klass.instance_variable_set(:@no_commands, false)
617
+ klass.instance_variable_set(:@no_commands, 0)
579
618
  end
580
619
 
581
620
  # Fire this callback whenever a method is added. Added methods are
582
621
  # tracked as commands by invoking the create_command method.
583
622
  def method_added(meth)
623
+ super(meth)
584
624
  meth = meth.to_s
585
625
 
586
626
  if meth == "initialize"
@@ -591,8 +631,7 @@ class Thor
591
631
  # Return if it's not a public instance method
592
632
  return unless public_method_defined?(meth.to_sym)
593
633
 
594
- @no_commands ||= false
595
- return if @no_commands || !create_command(meth)
634
+ return if no_commands? || !create_command(meth)
596
635
 
597
636
  is_thor_reserved_word?(meth, :command)
598
637
  Thor::Base.register_klass_file(self)
@@ -619,11 +658,6 @@ class Thor
619
658
  end
620
659
  end
621
660
 
622
- # A flag that makes the process exit with status 1 if any error happens.
623
- def exit_on_failure?
624
- false
625
- end
626
-
627
661
  #
628
662
  # The basename of the program invoking the thor class.
629
663
  #
@@ -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,32 +39,42 @@ 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
- # Add usage with required arguments
51
- formatted << if klass && !klass.arguments.empty?
52
- usage.to_s.gsub(/^#{name}/) do |match|
53
- match << " " << klass.arguments.map(&:usage).compact.join(" ")
54
- end
55
- else
56
- usage.to_s
57
- end
52
+ Array(usage).map do |specific_usage|
53
+ formatted_specific_usage = formatted
58
54
 
59
- # Add required options
60
- formatted << " #{required_options}"
55
+ formatted_specific_usage += required_arguments_for(klass, specific_usage)
61
56
 
62
- # Strip and go!
63
- formatted.strip
57
+ # Add required options
58
+ formatted_specific_usage += " #{required_options}"
59
+
60
+ # Strip and go!
61
+ formatted_specific_usage.strip
62
+ end.join("\n")
64
63
  end
65
64
 
66
65
  protected
67
66
 
67
+ # Add usage with required arguments
68
+ def required_arguments_for(klass, usage)
69
+ if klass && !klass.arguments.empty?
70
+ usage.to_s.gsub(/^#{name}/) do |match|
71
+ match << " " << klass.arguments.map(&:usage).compact.join(" ")
72
+ end
73
+ else
74
+ usage.to_s
75
+ end
76
+ end
77
+
68
78
  def not_debugging?(instance)
69
79
  !(instance.class.respond_to?(:debugging) && instance.class.debugging)
70
80
  end
@@ -95,8 +105,7 @@ class Thor
95
105
  def handle_argument_error?(instance, error, caller)
96
106
  not_debugging?(instance) && (error.message =~ /wrong number of arguments/ || error.message =~ /given \d*, expected \d*/) && begin
97
107
  saned = sans_backtrace(error.backtrace, caller)
98
- # Ruby 1.9 always include the called method in the backtrace
99
- saned.empty? || (saned.size == 1 && RUBY_VERSION >= "1.9")
108
+ saned.empty? || saned.size == 1
100
109
  end
101
110
  end
102
111
 
@@ -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,19 @@
1
1
  class Thor
2
+ Correctable = if defined?(DidYouMean::SpellChecker) && defined?(DidYouMean::Correctable)
3
+ # In order to support versions of Ruby that don't have keyword
4
+ # arguments, we need our own spell checker class that doesn't take key
5
+ # words. Even though this code wouldn't be hit because of the check
6
+ # above, it's still necessary because the interpreter would otherwise be
7
+ # unable to parse the file.
8
+ class NoKwargSpellChecker < DidYouMean::SpellChecker # :nodoc:
9
+ def initialize(dictionary)
10
+ @dictionary = dictionary
11
+ end
12
+ end
13
+
14
+ DidYouMean::Correctable
15
+ end
16
+
2
17
  # Thor::Error is raised when it's caused by wrong usage of thor classes. Those
3
18
  # errors have their backtrace suppressed and are nicely shown to the user.
4
19
  #
@@ -10,6 +25,35 @@ class Thor
10
25
 
11
26
  # Raised when a command was not found.
12
27
  class UndefinedCommandError < Error
28
+ class SpellChecker
29
+ attr_reader :error
30
+
31
+ def initialize(error)
32
+ @error = error
33
+ end
34
+
35
+ def corrections
36
+ @corrections ||= spell_checker.correct(error.command).map(&:inspect)
37
+ end
38
+
39
+ def spell_checker
40
+ NoKwargSpellChecker.new(error.all_commands)
41
+ end
42
+ end
43
+
44
+ attr_reader :command, :all_commands
45
+
46
+ def initialize(command, all_commands, namespace)
47
+ @command = command
48
+ @all_commands = all_commands
49
+
50
+ message = "Could not find command #{command.inspect}"
51
+ message = namespace ? "#{message} in #{namespace.inspect} namespace." : "#{message}."
52
+
53
+ super(message)
54
+ end
55
+
56
+ prepend Correctable if Correctable
13
57
  end
14
58
  UndefinedTaskError = UndefinedCommandError
15
59
 
@@ -22,6 +66,33 @@ class Thor
22
66
  end
23
67
 
24
68
  class UnknownArgumentError < Error
69
+ class SpellChecker
70
+ attr_reader :error
71
+
72
+ def initialize(error)
73
+ @error = error
74
+ end
75
+
76
+ def corrections
77
+ @corrections ||=
78
+ error.unknown.flat_map { |unknown| spell_checker.correct(unknown) }.uniq.map(&:inspect)
79
+ end
80
+
81
+ def spell_checker
82
+ @spell_checker ||= NoKwargSpellChecker.new(error.switches)
83
+ end
84
+ end
85
+
86
+ attr_reader :switches, :unknown
87
+
88
+ def initialize(switches, unknown)
89
+ @switches = switches
90
+ @unknown = unknown
91
+
92
+ super("Unknown switches #{unknown.map(&:inspect).join(', ')}")
93
+ end
94
+
95
+ prepend Correctable if Correctable
25
96
  end
26
97
 
27
98
  class RequiredArgumentMissingError < InvocationError
@@ -29,4 +100,11 @@ class Thor
29
100
 
30
101
  class MalformattedArgumentError < InvocationError
31
102
  end
103
+
104
+ if Correctable
105
+ DidYouMean::SPELL_CHECKERS.merge!(
106
+ 'Thor::UndefinedCommandError' => UndefinedCommandError::SpellChecker,
107
+ 'Thor::UnknownArgumentError' => UnknownArgumentError::SpellChecker
108
+ )
109
+ end
32
110
  end