thor 0.20.0 → 1.2.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.
data/lib/thor/command.rb CHANGED
@@ -49,24 +49,32 @@ class Thor
49
49
 
50
50
  formatted ||= "".dup
51
51
 
52
- # Add usage with required arguments
53
- formatted << if klass && !klass.arguments.empty?
54
- usage.to_s.gsub(/^#{name}/) do |match|
55
- match << " " << klass.arguments.map(&:usage).compact.join(" ")
56
- end
57
- else
58
- usage.to_s
59
- end
52
+ Array(usage).map do |specific_usage|
53
+ formatted_specific_usage = formatted
60
54
 
61
- # Add required options
62
- formatted << " #{required_options}"
55
+ formatted_specific_usage += required_arguments_for(klass, specific_usage)
63
56
 
64
- # Strip and go!
65
- 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")
66
63
  end
67
64
 
68
65
  protected
69
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
+
70
78
  def not_debugging?(instance)
71
79
  !(instance.class.respond_to?(:debugging) && instance.class.debugging)
72
80
  end
@@ -97,8 +105,7 @@ class Thor
97
105
  def handle_argument_error?(instance, error, caller)
98
106
  not_debugging?(instance) && (error.message =~ /wrong number of arguments/ || error.message =~ /given \d*, expected \d*/) && begin
99
107
  saned = sans_backtrace(error.backtrace, caller)
100
- # Ruby 1.9 always include the called method in the backtrace
101
- saned.empty? || (saned.size == 1 && RUBY_VERSION >= "1.9")
108
+ saned.empty? || saned.size == 1
102
109
  end
103
110
  end
104
111
 
@@ -28,6 +28,12 @@ class Thor
28
28
  super(convert_key(key))
29
29
  end
30
30
 
31
+ def except(*keys)
32
+ dup.tap do |hash|
33
+ keys.each { |key| hash.delete(convert_key(key)) }
34
+ end
35
+ end
36
+
31
37
  def fetch(key, *args)
32
38
  super(convert_key(key), *args)
33
39
  end
data/lib/thor/error.rb CHANGED
@@ -1,4 +1,19 @@
1
1
  class Thor
2
+ Correctable = if defined?(DidYouMean::SpellChecker) && defined?(DidYouMean::Correctable) # rubocop:disable Naming/ConstantName
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,16 @@ class Thor
29
100
 
30
101
  class MalformattedArgumentError < InvocationError
31
102
  end
103
+
104
+ if Correctable
105
+ if DidYouMean.respond_to?(:correct_error)
106
+ DidYouMean.correct_error(Thor::UndefinedCommandError, UndefinedCommandError::SpellChecker)
107
+ DidYouMean.correct_error(Thor::UnknownArgumentError, UnknownArgumentError::SpellChecker)
108
+ else
109
+ DidYouMean::SPELL_CHECKERS.merge!(
110
+ 'Thor::UndefinedCommandError' => UndefinedCommandError::SpellChecker,
111
+ 'Thor::UnknownArgumentError' => UnknownArgumentError::SpellChecker
112
+ )
113
+ end
114
+ end
32
115
  end
data/lib/thor/group.rb CHANGED
@@ -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
 
@@ -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
 
@@ -24,7 +24,7 @@ class Thor
24
24
  $stdin.gets
25
25
  else
26
26
  # Lazy-load io/console since it is gem-ified as of 2.3
27
- require "io/console" if RUBY_VERSION > "1.9.2"
27
+ require "io/console"
28
28
  $stdin.noecho(&:gets)
29
29
  end
30
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
@@ -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
@@ -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
@@ -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
 
@@ -30,7 +30,11 @@ class Thor
30
30
 
31
31
  arguments.each do |argument|
32
32
  if !argument.default.nil?
33
- @assigns[argument.human_name] = argument.default
33
+ begin
34
+ @assigns[argument.human_name] = argument.default.dup
35
+ rescue TypeError # Compatibility shim for un-dup-able Fixnum in Ruby < 2.4
36
+ @assigns[argument.human_name] = argument.default
37
+ end
34
38
  elsif argument.required?
35
39
  @non_assigned_required << argument
36
40
  end
@@ -82,7 +86,7 @@ class Thor
82
86
  end
83
87
 
84
88
  def current_is_value?
85
- peek && peek.to_s !~ /^-/
89
+ peek && peek.to_s !~ /^-{1,2}\S+/
86
90
  end
87
91
 
88
92
  # Runs through the argument array getting strings that contains ":" and
@@ -1,17 +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
8
  @check_default_type = options[:check_default_type]
9
9
  options[:required] = false unless options.key?(:required)
10
+ @repeatable = options.fetch(:repeatable, false)
10
11
  super
11
- @lazy_default = options[:lazy_default]
12
- @group = options[:group].to_s.capitalize if options[:group]
13
- @aliases = Array(options[:aliases])
14
- @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]
15
16
  end
16
17
 
17
18
  # This parse quick options given as method_options. It makes several
@@ -111,7 +112,7 @@ class Thor
111
112
 
112
113
  def validate!
113
114
  raise ArgumentError, "An option cannot be boolean and required." if boolean? && required?
114
- validate_default_type! if @check_default_type
115
+ validate_default_type!
115
116
  end
116
117
 
117
118
  def validate_default_type!
@@ -128,7 +129,19 @@ class Thor
128
129
  @default.class.name.downcase.to_sym
129
130
  end
130
131
 
131
- raise ArgumentError, "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?
@@ -44,6 +44,8 @@ class Thor
44
44
  @shorts = {}
45
45
  @switches = {}
46
46
  @extra = []
47
+ @stopped_parsing_after_extra_index = nil
48
+ @is_treated_as_value = false
47
49
 
48
50
  options.each do |option|
49
51
  @switches[option.switch_name] = option
@@ -66,14 +68,26 @@ class Thor
66
68
  if result == OPTS_END
67
69
  shift
68
70
  @parsing_options = false
71
+ @stopped_parsing_after_extra_index ||= @extra.size
69
72
  super
70
73
  else
71
74
  result
72
75
  end
73
76
  end
74
77
 
78
+ def shift
79
+ @is_treated_as_value = false
80
+ super
81
+ end
82
+
83
+ def unshift(arg, is_value: false)
84
+ @is_treated_as_value = is_value
85
+ super(arg)
86
+ end
87
+
75
88
  def parse(args) # rubocop:disable MethodLength
76
89
  @pile = args.dup
90
+ @is_treated_as_value = false
77
91
  @parsing_options = true
78
92
 
79
93
  while peek
@@ -86,7 +100,10 @@ class Thor
86
100
  when SHORT_SQ_RE
87
101
  unshift($1.split("").map { |f| "-#{f}" })
88
102
  next
89
- when EQ_RE, SHORT_NUM
103
+ when EQ_RE
104
+ unshift($2, is_value: true)
105
+ switch = $1
106
+ when SHORT_NUM
90
107
  unshift($2)
91
108
  switch = $1
92
109
  when LONG_RE, SHORT_RE
@@ -95,10 +112,12 @@ class Thor
95
112
 
96
113
  switch = normalize_switch(switch)
97
114
  option = switch_option(switch)
98
- @assigns[option.human_name] = parse_peek(switch, option)
115
+ result = parse_peek(switch, option)
116
+ assign_result!(option, result)
99
117
  elsif @stop_on_unknown
100
118
  @parsing_options = false
101
119
  @extra << shifted
120
+ @stopped_parsing_after_extra_index ||= @extra.size
102
121
  @extra << shift while peek
103
122
  break
104
123
  elsif match
@@ -120,18 +139,31 @@ class Thor
120
139
  end
121
140
 
122
141
  def check_unknown!
142
+ to_check = @stopped_parsing_after_extra_index ? @extra[0...@stopped_parsing_after_extra_index] : @extra
143
+
123
144
  # an unknown option starts with - or -- and has no more --'s afterward.
124
- unknown = @extra.select { |str| str =~ /^--?(?:(?!--).)*$/ }
125
- raise UnknownArgumentError, "Unknown switches '#{unknown.join(', ')}'" unless unknown.empty?
145
+ unknown = to_check.select { |str| str =~ /^--?(?:(?!--).)*$/ }
146
+ raise UnknownArgumentError.new(@switches.keys, unknown) unless unknown.empty?
126
147
  end
127
148
 
128
149
  protected
129
150
 
151
+ def assign_result!(option, result)
152
+ if option.repeatable && option.type == :hash
153
+ (@assigns[option.human_name] ||= {}).merge!(result)
154
+ elsif option.repeatable
155
+ (@assigns[option.human_name] ||= []) << result
156
+ else
157
+ @assigns[option.human_name] = result
158
+ end
159
+ end
160
+
130
161
  # Check if the current value in peek is a registered switch.
131
162
  #
132
163
  # Two booleans are returned. The first is true if the current value
133
164
  # starts with a hyphen; the second is true if it is a registered switch.
134
165
  def current_is_switch?
166
+ return [false, false] if @is_treated_as_value
135
167
  case peek
136
168
  when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
137
169
  [true, switch?($1)]
@@ -143,6 +175,7 @@ class Thor
143
175
  end
144
176
 
145
177
  def current_is_switch_formatted?
178
+ return false if @is_treated_as_value
146
179
  case peek
147
180
  when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM, SHORT_SQ_RE
148
181
  true
@@ -152,11 +185,12 @@ class Thor
152
185
  end
153
186
 
154
187
  def current_is_value?
188
+ return true if @is_treated_as_value
155
189
  peek && (!parsing_options? || super)
156
190
  end
157
191
 
158
192
  def switch?(arg)
159
- switch_option(normalize_switch(arg))
193
+ !switch_option(normalize_switch(arg)).nil?
160
194
  end
161
195
 
162
196
  def switch_option(arg)
@@ -189,7 +223,7 @@ class Thor
189
223
  shift
190
224
  false
191
225
  else
192
- !no_or_skip?(switch)
226
+ @switches.key?(switch) || !no_or_skip?(switch)
193
227
  end
194
228
  else
195
229
  @switches.key?(switch) || !no_or_skip?(switch)
data/lib/thor/parser.rb CHANGED
@@ -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"
@@ -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)
data/lib/thor/runner.rb CHANGED
@@ -1,12 +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
4
  require "yaml"
6
5
  require "digest/md5"
7
6
  require "pathname"
8
7
 
9
8
  class Thor::Runner < Thor #:nodoc: # rubocop:disable ClassLength
9
+ autoload :OpenURI, "open-uri"
10
+
10
11
  map "-T" => :list, "-i" => :install, "-u" => :update, "-v" => :version
11
12
 
12
13
  def self.banner(command, all = false, subcommand = false)
@@ -111,7 +112,7 @@ class Thor::Runner < Thor #:nodoc: # rubocop:disable ClassLength
111
112
 
112
113
  desc "version", "Show Thor version"
113
114
  def version
114
- require "thor/version"
115
+ require_relative "version"
115
116
  say "Thor #{Thor::VERSION}"
116
117
  end
117
118