thor 0.19.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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