thor 0.19.4 → 1.0.1

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)
@@ -22,6 +22,15 @@ class Thor
22
22
 
23
23
  TEMPLATE_EXTNAME = ".tt"
24
24
 
25
+ class << self
26
+ def deprecation_warning(message) #:nodoc:
27
+ unless ENV['THOR_SILENCE_DEPRECATION']
28
+ warn "Deprecation warning: #{message}\n" +
29
+ 'You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION.'
30
+ end
31
+ end
32
+ end
33
+
25
34
  module Base
26
35
  attr_accessor :options, :parent_options, :args
27
36
 
@@ -42,7 +51,7 @@ class Thor
42
51
  # config<Hash>:: Configuration for this Thor class.
43
52
  #
44
53
  def initialize(args = [], local_options = {}, config = {})
45
- parse_options = config[:current_command] && config[:current_command].disable_class_options ? {} : self.class.class_options
54
+ parse_options = self.class.class_options
46
55
 
47
56
  # The start method splits inbound arguments at the first argument
48
57
  # that looks like an option (starts with - or --). It then calls
@@ -65,7 +74,8 @@ class Thor
65
74
  # declared options from the array. This will leave us with
66
75
  # a list of arguments that weren't declared.
67
76
  stop_on_unknown = self.class.stop_on_unknown_option? config[:current_command]
68
- opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown)
77
+ disable_required_check = self.class.disable_required_check? config[:current_command]
78
+ opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check)
69
79
  self.options = opts.parse(array_options)
70
80
  self.options = config[:class_options].merge(options) if config[:class_options]
71
81
 
@@ -88,6 +98,7 @@ class Thor
88
98
 
89
99
  class << self
90
100
  def included(base) #:nodoc:
101
+ super(base)
91
102
  base.extend ClassMethods
92
103
  base.send :include, Invocation
93
104
  base.send :include, Shell
@@ -112,7 +123,7 @@ class Thor
112
123
  end
113
124
 
114
125
  # 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.
126
+ # class and the file on Thor::Base. This is the method responsible for it.
116
127
  #
117
128
  def register_klass_file(klass) #:nodoc:
118
129
  file = caller[1].match(/(.*):\d+/)[1]
@@ -150,6 +161,24 @@ class Thor
150
161
  !!check_unknown_options
151
162
  end
152
163
 
164
+ # If you want to raise an error when the default value of an option does not match
165
+ # the type call check_default_type!
166
+ # This will be the default; for compatibility a deprecation warning is issued if necessary.
167
+ def check_default_type!
168
+ @check_default_type = true
169
+ end
170
+
171
+ # If you want to use defaults that don't match the type of an option,
172
+ # either specify `check_default_type: false` or call `allow_incompatible_default_type!`
173
+ def allow_incompatible_default_type!
174
+ @check_default_type = false
175
+ end
176
+
177
+ def check_default_type #:nodoc:
178
+ @check_default_type = from_superclass(:check_default_type, nil) unless defined?(@check_default_type)
179
+ @check_default_type
180
+ end
181
+
153
182
  # If true, option parsing is suspended as soon as an unknown option or a
154
183
  # regular argument is encountered. All remaining arguments are passed to
155
184
  # the command as regular arguments.
@@ -157,6 +186,12 @@ class Thor
157
186
  false
158
187
  end
159
188
 
189
+ # If true, option set will not suspend the execution of the command when
190
+ # a required option is not provided.
191
+ def disable_required_check?(command_name) #:nodoc:
192
+ false
193
+ end
194
+
160
195
  # If you want only strict string args (useful when cascading thor classes),
161
196
  # call strict_args_position! This is disabled by default to allow dynamic
162
197
  # invocations.
@@ -331,22 +366,22 @@ class Thor
331
366
  # Returns the commands for this Thor class.
332
367
  #
333
368
  # ==== Returns
334
- # OrderedHash:: An ordered hash with commands names as keys and Thor::Command
335
- # objects as values.
369
+ # Hash:: An ordered hash with commands names as keys and Thor::Command
370
+ # objects as values.
336
371
  #
337
372
  def commands
338
- @commands ||= Thor::CoreExt::OrderedHash.new
373
+ @commands ||= Hash.new
339
374
  end
340
375
  alias_method :tasks, :commands
341
376
 
342
377
  # Returns the commands for this Thor class and all subclasses.
343
378
  #
344
379
  # ==== Returns
345
- # OrderedHash:: An ordered hash with commands names as keys and Thor::Command
346
- # objects as values.
380
+ # Hash:: An ordered hash with commands names as keys and Thor::Command
381
+ # objects as values.
347
382
  #
348
383
  def all_commands
349
- @all_commands ||= from_superclass(:all_commands, Thor::CoreExt::OrderedHash.new)
384
+ @all_commands ||= from_superclass(:all_commands, Hash.new)
350
385
  @all_commands.merge!(commands)
351
386
  end
352
387
  alias_method :all_tasks, :all_commands
@@ -393,14 +428,20 @@ class Thor
393
428
  # remove_command :this_is_not_a_command
394
429
  # end
395
430
  #
396
- def no_commands
397
- @no_commands = true
398
- yield
399
- ensure
400
- @no_commands = false
431
+ def no_commands(&block)
432
+ no_commands_context.enter(&block)
401
433
  end
434
+
402
435
  alias_method :no_tasks, :no_commands
403
436
 
437
+ def no_commands_context
438
+ @no_commands_context ||= NestedContext.new
439
+ end
440
+
441
+ def no_commands?
442
+ no_commands_context.entered?
443
+ end
444
+
404
445
  # Sets the namespace for the Thor or Thor::Group class. By default the
405
446
  # namespace is retrieved from the class name. If your Thor class is named
406
447
  # Scripts::MyScript, the help method, for example, will be called as:
@@ -444,13 +485,13 @@ class Thor
444
485
  dispatch(nil, given_args.dup, nil, config)
445
486
  rescue Thor::Error => e
446
487
  config[:debug] || ENV["THOR_DEBUG"] == "1" ? (raise e) : config[:shell].error(e.message)
447
- exit(1) if exit_on_failure?
488
+ exit(false) if exit_on_failure?
448
489
  rescue Errno::EPIPE
449
490
  # This happens if a thor command is piped to something like `head`,
450
491
  # which closes the pipe when it's done reading. This will also
451
492
  # mean that if the pipe is closed, further unnecessary
452
493
  # computation will not occur.
453
- exit(0)
494
+ exit(true)
454
495
  end
455
496
 
456
497
  # Allows to use private methods from parent in child classes as commands.
@@ -471,19 +512,25 @@ class Thor
471
512
  alias_method :public_task, :public_command
472
513
 
473
514
  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}."
515
+ raise UndefinedCommandError.new(command, all_commands.keys, (namespace if has_namespace))
476
516
  end
477
517
  alias_method :handle_no_task_error, :handle_no_command_error
478
518
 
479
519
  def handle_argument_error(command, error, args, arity) #:nodoc:
480
- msg = "ERROR: \"#{basename} #{command.name}\" was called with "
520
+ name = [command.ancestor_name, command.name].compact.join(" ")
521
+ msg = "ERROR: \"#{basename} #{name}\" was called with ".dup
481
522
  msg << "no arguments" if args.empty?
482
523
  msg << "arguments " << args.inspect unless args.empty?
483
- msg << "\nUsage: #{banner(command).inspect}"
524
+ msg << "\nUsage: \"#{banner(command).split("\n").join("\"\n \"")}\""
484
525
  raise InvocationError, msg
485
526
  end
486
527
 
528
+ # A flag that makes the process exit with status 1 if any error happens.
529
+ def exit_on_failure?
530
+ Thor.deprecation_warning "Thor exit with status 0 on errors. To keep this behavior, you must define `exit_on_failure?` in `#{self.name}`"
531
+ false
532
+ end
533
+
487
534
  protected
488
535
 
489
536
  # Prints the class options per group. If an option does not belong to
@@ -541,7 +588,7 @@ class Thor
541
588
  # options<Hash>:: Described in both class_option and method_option.
542
589
  # scope<Hash>:: Options hash that is being built up
543
590
  def build_option(name, options, scope) #:nodoc:
544
- scope[name] = Thor::Option.new(name, options)
591
+ scope[name] = Thor::Option.new(name, {:check_default_type => check_default_type}.merge!(options))
545
592
  end
546
593
 
547
594
  # Receives a hash of options, parse them and add to the scope. This is a
@@ -574,13 +621,15 @@ class Thor
574
621
  # Everytime someone inherits from a Thor class, register the klass
575
622
  # and file into baseclass.
576
623
  def inherited(klass)
624
+ super(klass)
577
625
  Thor::Base.register_klass_file(klass)
578
- klass.instance_variable_set(:@no_commands, false)
626
+ klass.instance_variable_set(:@no_commands, 0)
579
627
  end
580
628
 
581
629
  # Fire this callback whenever a method is added. Added methods are
582
630
  # tracked as commands by invoking the create_command method.
583
631
  def method_added(meth)
632
+ super(meth)
584
633
  meth = meth.to_s
585
634
 
586
635
  if meth == "initialize"
@@ -591,8 +640,7 @@ class Thor
591
640
  # Return if it's not a public instance method
592
641
  return unless public_method_defined?(meth.to_sym)
593
642
 
594
- @no_commands ||= false
595
- return if @no_commands || !create_command(meth)
643
+ return if no_commands? || !create_command(meth)
596
644
 
597
645
  is_thor_reserved_word?(meth, :command)
598
646
  Thor::Base.register_klass_file(self)
@@ -619,11 +667,6 @@ class Thor
619
667
  end
620
668
  end
621
669
 
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
670
  #
628
671
  # The basename of the program invoking the thor class.
629
672
  #
@@ -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