thor 1.1.0 → 1.4.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.
data/lib/thor/command.rb CHANGED
@@ -1,14 +1,15 @@
1
1
  class Thor
2
- class Command < Struct.new(:name, :description, :long_description, :usage, :options, :ancestor_name)
2
+ class Command < Struct.new(:name, :description, :long_description, :wrap_long_description, :usage, :options, :options_relation, :ancestor_name)
3
3
  FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/
4
4
 
5
- def initialize(name, description, long_description, usage, options = nil)
6
- super(name.to_s, description, long_description, usage, options || {})
5
+ def initialize(name, description, long_description, wrap_long_description, usage, options = nil, options_relation = nil)
6
+ super(name.to_s, description, long_description, wrap_long_description, usage, options || {}, options_relation || {})
7
7
  end
8
8
 
9
9
  def initialize_copy(other) #:nodoc:
10
10
  super(other)
11
11
  self.options = other.options.dup if other.options
12
+ self.options_relation = other.options_relation.dup if other.options_relation
12
13
  end
13
14
 
14
15
  def hidden?
@@ -62,6 +63,14 @@ class Thor
62
63
  end.join("\n")
63
64
  end
64
65
 
66
+ def method_exclusive_option_names #:nodoc:
67
+ self.options_relation[:exclusive_option_names] || []
68
+ end
69
+
70
+ def method_at_least_one_option_names #:nodoc:
71
+ self.options_relation[:at_least_one_option_names] || []
72
+ end
73
+
65
74
  protected
66
75
 
67
76
  # Add usage with required arguments
@@ -127,7 +136,7 @@ class Thor
127
136
  # A dynamic command that handles method missing scenarios.
128
137
  class DynamicCommand < Command
129
138
  def initialize(name, options = nil)
130
- super(name.to_s, "A dynamically-generated command", name.to_s, name.to_s, options)
139
+ super(name.to_s, "A dynamically-generated command", name.to_s, nil, name.to_s, options)
131
140
  end
132
141
 
133
142
  def run(instance, args = [])
@@ -28,10 +28,20 @@ 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
34
40
 
41
+ def slice(*keys)
42
+ super(*keys.map{ |key| convert_key(key) })
43
+ end
44
+
35
45
  def key?(key)
36
46
  super(convert_key(key))
37
47
  end
data/lib/thor/error.rb CHANGED
@@ -1,18 +1,15 @@
1
1
  class Thor
2
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
3
+ Module.new do
4
+ def to_s
5
+ super + DidYouMean.formatter.message_for(corrections)
6
+ end
7
+
8
+ def corrections
9
+ @corrections ||= self.class.const_get(:SpellChecker).new(self).corrections
10
+ end
11
+ end
12
+ end
16
13
 
17
14
  # Thor::Error is raised when it's caused by wrong usage of thor classes. Those
18
15
  # errors have their backtrace suppressed and are nicely shown to the user.
@@ -37,7 +34,7 @@ class Thor
37
34
  end
38
35
 
39
36
  def spell_checker
40
- NoKwargSpellChecker.new(error.all_commands)
37
+ DidYouMean::SpellChecker.new(dictionary: error.all_commands)
41
38
  end
42
39
  end
43
40
 
@@ -79,7 +76,7 @@ class Thor
79
76
  end
80
77
 
81
78
  def spell_checker
82
- @spell_checker ||= NoKwargSpellChecker.new(error.switches)
79
+ @spell_checker ||= DidYouMean::SpellChecker.new(dictionary: error.switches)
83
80
  end
84
81
  end
85
82
 
@@ -101,10 +98,9 @@ class Thor
101
98
  class MalformattedArgumentError < InvocationError
102
99
  end
103
100
 
104
- if Correctable
105
- DidYouMean::SPELL_CHECKERS.merge!(
106
- 'Thor::UndefinedCommandError' => UndefinedCommandError::SpellChecker,
107
- 'Thor::UnknownArgumentError' => UnknownArgumentError::SpellChecker
108
- )
101
+ class ExclusiveArgumentError < InvocationError
102
+ end
103
+
104
+ class AtLeastOneRequiredArgumentError < InvocationError
109
105
  end
110
106
  end
data/lib/thor/group.rb CHANGED
@@ -169,7 +169,7 @@ class Thor::Group
169
169
  # options are added to group_options hash. Options that already exists
170
170
  # in base_options are not added twice.
171
171
  #
172
- def get_options_from_invocations(group_options, base_options) #:nodoc: # rubocop:disable MethodLength
172
+ def get_options_from_invocations(group_options, base_options) #:nodoc:
173
173
  invocations.each do |name, from_option|
174
174
  value = if from_option
175
175
  option = class_options[name]
@@ -211,6 +211,17 @@ class Thor::Group
211
211
  raise error, msg
212
212
  end
213
213
 
214
+ # Checks if a specified command exists.
215
+ #
216
+ # ==== Parameters
217
+ # command_name<String>:: The name of the command to check for existence.
218
+ #
219
+ # ==== Returns
220
+ # Boolean:: +true+ if the command exists, +false+ otherwise.
221
+ def command_exists?(command_name) #:nodoc:
222
+ commands.keys.include?(command_name)
223
+ end
224
+
214
225
  protected
215
226
 
216
227
  # The method responsible for dispatching given the args.
@@ -143,7 +143,7 @@ class Thor
143
143
 
144
144
  # Configuration values that are shared between invocations.
145
145
  def _shared_configuration #:nodoc:
146
- {:invocations => @_invocations}
146
+ {invocations: @_invocations}
147
147
  end
148
148
 
149
149
  # This method simply retrieves the class and command to be invoked.
@@ -13,10 +13,10 @@ class Thor
13
13
  end
14
14
 
15
15
  def entered?
16
- @depth > 0
16
+ @depth.positive?
17
17
  end
18
18
 
19
- private
19
+ private
20
20
 
21
21
  def push
22
22
  @depth += 1
@@ -24,6 +24,14 @@ class Thor
24
24
  validate! # Trigger specific validations
25
25
  end
26
26
 
27
+ def print_default
28
+ if @type == :array and @default.is_a?(Array)
29
+ @default.map(&:dump).join(" ")
30
+ else
31
+ @default
32
+ end
33
+ end
34
+
27
35
  def usage
28
36
  required? ? banner : "[#{banner}]"
29
37
  end
@@ -41,11 +49,19 @@ class Thor
41
49
  end
42
50
  end
43
51
 
52
+ def enum_to_s
53
+ if enum.respond_to? :join
54
+ enum.join(", ")
55
+ else
56
+ "#{enum.first}..#{enum.last}"
57
+ end
58
+ end
59
+
44
60
  protected
45
61
 
46
62
  def validate!
47
63
  raise ArgumentError, "An argument cannot be required and have default value." if required? && !default.nil?
48
- raise ArgumentError, "An argument cannot have an enum other than an array." if @enum && !@enum.is_a?(Array)
64
+ raise ArgumentError, "An argument cannot have an enum other than an enumerable." if @enum && !@enum.is_a?(Enumerable)
49
65
  end
50
66
 
51
67
  def valid_type?(type)
@@ -1,5 +1,5 @@
1
1
  class Thor
2
- class Arguments #:nodoc: # rubocop:disable ClassLength
2
+ class Arguments #:nodoc:
3
3
  NUMERIC = /[-+]?(\d*\.\d+|\d+)/
4
4
 
5
5
  # Receives an array of args and returns two arrays, one with arguments
@@ -30,11 +30,7 @@ class Thor
30
30
 
31
31
  arguments.each do |argument|
32
32
  if !argument.default.nil?
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
33
+ @assigns[argument.human_name] = argument.default.dup
38
34
  elsif argument.required?
39
35
  @non_assigned_required << argument
40
36
  end
@@ -121,8 +117,18 @@ class Thor
121
117
  #
122
118
  def parse_array(name)
123
119
  return shift if peek.is_a?(Array)
120
+
124
121
  array = []
125
- array << shift while current_is_value?
122
+
123
+ while current_is_value?
124
+ value = shift
125
+
126
+ if !value.empty?
127
+ validate_enum_value!(name, value, "Expected all values of '%s' to be one of %s; got %s")
128
+ end
129
+
130
+ array << value
131
+ end
126
132
  array
127
133
  end
128
134
 
@@ -138,11 +144,9 @@ class Thor
138
144
  end
139
145
 
140
146
  value = $&.index(".") ? shift.to_f : shift.to_i
141
- if @switches.is_a?(Hash) && switch = @switches[name]
142
- if switch.enum && !switch.enum.include?(value)
143
- raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}"
144
- end
145
- end
147
+
148
+ validate_enum_value!(name, value, "Expected '%s' to be one of %s; got %s")
149
+
146
150
  value
147
151
  end
148
152
 
@@ -156,15 +160,27 @@ class Thor
156
160
  nil
157
161
  else
158
162
  value = shift
159
- if @switches.is_a?(Hash) && switch = @switches[name]
160
- if switch.enum && !switch.enum.include?(value)
161
- raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}"
162
- end
163
- end
163
+
164
+ validate_enum_value!(name, value, "Expected '%s' to be one of %s; got %s")
165
+
164
166
  value
165
167
  end
166
168
  end
167
169
 
170
+ # Raises an error if the switch is an enum and the values aren't included on it.
171
+ #
172
+ def validate_enum_value!(name, value, message)
173
+ return unless @switches.is_a?(Hash)
174
+
175
+ switch = @switches[name]
176
+
177
+ return unless switch
178
+
179
+ if switch.enum && !switch.enum.include?(value)
180
+ raise MalformattedArgumentError, message % [name, switch.enum_to_s, value]
181
+ end
182
+ end
183
+
168
184
  # Raises an error if @non_assigned_required array is not empty.
169
185
  #
170
186
  def check_requirement!
@@ -11,7 +11,7 @@ class Thor
11
11
  super
12
12
  @lazy_default = options[:lazy_default]
13
13
  @group = options[:group].to_s.capitalize if options[:group]
14
- @aliases = Array(options[:aliases])
14
+ @aliases = normalize_aliases(options[:aliases])
15
15
  @hide = options[:hide]
16
16
  end
17
17
 
@@ -58,7 +58,7 @@ class Thor
58
58
  default = nil
59
59
  if VALID_TYPES.include?(value)
60
60
  value
61
- elsif required = (value == :required) # rubocop:disable AssignmentInCondition
61
+ elsif required = (value == :required) # rubocop:disable Lint/AssignmentInCondition
62
62
  :string
63
63
  end
64
64
  when TrueClass, FalseClass
@@ -69,7 +69,7 @@ class Thor
69
69
  value.class.name.downcase.to_sym
70
70
  end
71
71
 
72
- new(name.to_s, :required => required, :type => type, :default => default, :aliases => aliases)
72
+ new(name.to_s, required: required, type: type, default: default, aliases: aliases)
73
73
  end
74
74
 
75
75
  def switch_name
@@ -89,14 +89,27 @@ class Thor
89
89
 
90
90
  sample = "[#{sample}]".dup unless required?
91
91
 
92
- if boolean?
93
- sample << ", [#{dasherize('no-' + human_name)}]" unless (name == "force") || name.start_with?("no-")
92
+ if boolean? && name != "force" && !name.match(/\A(no|skip)[\-_]/)
93
+ sample << ", [#{dasherize('no-' + human_name)}], [#{dasherize('skip-' + human_name)}]"
94
94
  end
95
95
 
96
+ aliases_for_usage.ljust(padding) + sample
97
+ end
98
+
99
+ def aliases_for_usage
96
100
  if aliases.empty?
97
- (" " * padding) << sample
101
+ ""
102
+ else
103
+ "#{aliases.join(', ')}, "
104
+ end
105
+ end
106
+
107
+ def show_default?
108
+ case default
109
+ when TrueClass, FalseClass
110
+ true
98
111
  else
99
- "#{aliases.join(', ')}, #{sample}"
112
+ super
100
113
  end
101
114
  end
102
115
 
@@ -138,8 +151,8 @@ class Thor
138
151
  raise ArgumentError, err
139
152
  elsif @check_default_type == nil
140
153
  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'
154
+ "This will be rejected in the future unless you explicitly pass the options `check_default_type: false`" +
155
+ " or call `allow_incompatible_default_type!` in your code"
143
156
  end
144
157
  end
145
158
  end
@@ -155,5 +168,11 @@ class Thor
155
168
  def dasherize(str)
156
169
  (str.length > 1 ? "--" : "-") + str.tr("_", "-")
157
170
  end
171
+
172
+ private
173
+
174
+ def normalize_aliases(aliases)
175
+ Array(aliases).map { |short| short.to_s.sub(/^(?!\-)/, "-") }
176
+ end
158
177
  end
159
178
  end
@@ -1,5 +1,5 @@
1
1
  class Thor
2
- class Options < Arguments #:nodoc: # rubocop:disable ClassLength
2
+ class Options < Arguments #:nodoc:
3
3
  LONG_RE = /^(--\w+(?:-\w+)*)$/
4
4
  SHORT_RE = /^(-[a-z])$/i
5
5
  EQ_RE = /^(--\w+(?:-\w+)*|-[a-z])=(.*)$/i
@@ -29,8 +29,10 @@ class Thor
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, disable_required_check = false)
32
+ def initialize(hash_options = {}, defaults = {}, stop_on_unknown = false, disable_required_check = false, relations = {})
33
33
  @stop_on_unknown = stop_on_unknown
34
+ @exclusives = (relations[:exclusive_option_names] || []).select{|array| !array.empty?}
35
+ @at_least_ones = (relations[:at_least_one_option_names] || []).select{|array| !array.empty?}
34
36
  @disable_required_check = disable_required_check
35
37
  options = hash_options.values
36
38
  super(options)
@@ -45,12 +47,12 @@ class Thor
45
47
  @switches = {}
46
48
  @extra = []
47
49
  @stopped_parsing_after_extra_index = nil
50
+ @is_treated_as_value = false
48
51
 
49
52
  options.each do |option|
50
53
  @switches[option.switch_name] = option
51
54
 
52
- option.aliases.each do |short|
53
- name = short.to_s.sub(/^(?!\-)/, "-")
55
+ option.aliases.each do |name|
54
56
  @shorts[name] ||= option.switch_name
55
57
  end
56
58
  end
@@ -74,8 +76,19 @@ class Thor
74
76
  end
75
77
  end
76
78
 
77
- def parse(args) # rubocop:disable MethodLength
79
+ def shift
80
+ @is_treated_as_value = false
81
+ super
82
+ end
83
+
84
+ def unshift(arg, is_value: false)
85
+ @is_treated_as_value = is_value
86
+ super(arg)
87
+ end
88
+
89
+ def parse(args) # rubocop:disable Metrics/MethodLength
78
90
  @pile = args.dup
91
+ @is_treated_as_value = false
79
92
  @parsing_options = true
80
93
 
81
94
  while peek
@@ -88,7 +101,10 @@ class Thor
88
101
  when SHORT_SQ_RE
89
102
  unshift($1.split("").map { |f| "-#{f}" })
90
103
  next
91
- when EQ_RE, SHORT_NUM
104
+ when EQ_RE
105
+ unshift($2, is_value: true)
106
+ switch = $1
107
+ when SHORT_NUM
92
108
  unshift($2)
93
109
  switch = $1
94
110
  when LONG_RE, SHORT_RE
@@ -117,12 +133,38 @@ class Thor
117
133
  end
118
134
 
119
135
  check_requirement! unless @disable_required_check
136
+ check_exclusive!
137
+ check_at_least_one!
120
138
 
121
139
  assigns = Thor::CoreExt::HashWithIndifferentAccess.new(@assigns)
122
140
  assigns.freeze
123
141
  assigns
124
142
  end
125
143
 
144
+ def check_exclusive!
145
+ opts = @assigns.keys
146
+ # When option A and B are exclusive, if A and B are given at the same time,
147
+ # the difference of argument array size will decrease.
148
+ found = @exclusives.find{ |ex| (ex - opts).size < ex.size - 1 }
149
+ if found
150
+ names = names_to_switch_names(found & opts).map{|n| "'#{n}'"}
151
+ class_name = self.class.name.split("::").last.downcase
152
+ fail ExclusiveArgumentError, "Found exclusive #{class_name} #{names.join(", ")}"
153
+ end
154
+ end
155
+
156
+ def check_at_least_one!
157
+ opts = @assigns.keys
158
+ # When at least one is required of the options A and B,
159
+ # if the both options were not given, none? would be true.
160
+ found = @at_least_ones.find{ |one_reqs| one_reqs.none?{ |o| opts.include? o} }
161
+ if found
162
+ names = names_to_switch_names(found).map{|n| "'#{n}'"}
163
+ class_name = self.class.name.split("::").last.downcase
164
+ fail AtLeastOneRequiredArgumentError, "Not found at least one of required #{class_name} #{names.join(", ")}"
165
+ end
166
+ end
167
+
126
168
  def check_unknown!
127
169
  to_check = @stopped_parsing_after_extra_index ? @extra[0...@stopped_parsing_after_extra_index] : @extra
128
170
 
@@ -133,6 +175,17 @@ class Thor
133
175
 
134
176
  protected
135
177
 
178
+ # Option names changes to swith name or human name
179
+ def names_to_switch_names(names = [])
180
+ @switches.map do |_, o|
181
+ if names.include? o.name
182
+ o.respond_to?(:switch_name) ? o.switch_name : o.human_name
183
+ else
184
+ nil
185
+ end
186
+ end.compact
187
+ end
188
+
136
189
  def assign_result!(option, result)
137
190
  if option.repeatable && option.type == :hash
138
191
  (@assigns[option.human_name] ||= {}).merge!(result)
@@ -148,6 +201,7 @@ class Thor
148
201
  # Two booleans are returned. The first is true if the current value
149
202
  # starts with a hyphen; the second is true if it is a registered switch.
150
203
  def current_is_switch?
204
+ return [false, false] if @is_treated_as_value
151
205
  case peek
152
206
  when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
153
207
  [true, switch?($1)]
@@ -159,6 +213,7 @@ class Thor
159
213
  end
160
214
 
161
215
  def current_is_switch_formatted?
216
+ return false if @is_treated_as_value
162
217
  case peek
163
218
  when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM, SHORT_SQ_RE
164
219
  true
@@ -168,6 +223,7 @@ class Thor
168
223
  end
169
224
 
170
225
  def current_is_value?
226
+ return true if @is_treated_as_value
171
227
  peek && (!parsing_options? || super)
172
228
  end
173
229
 
@@ -176,7 +232,7 @@ class Thor
176
232
  end
177
233
 
178
234
  def switch_option(arg)
179
- if match = no_or_skip?(arg) # rubocop:disable AssignmentInCondition
235
+ if match = no_or_skip?(arg) # rubocop:disable Lint/AssignmentInCondition
180
236
  @switches[arg] || @switches["--#{match}"]
181
237
  else
182
238
  @switches[arg]
@@ -194,7 +250,8 @@ class Thor
194
250
  @parsing_options
195
251
  end
196
252
 
197
- # Parse boolean values which can be given as --foo=true, --foo or --no-foo.
253
+ # Parse boolean values which can be given as --foo=true or --foo for true values, or
254
+ # --foo=false, --no-foo or --skip-foo for false values.
198
255
  #
199
256
  def parse_boolean(switch)
200
257
  if current_is_value?
@@ -41,7 +41,7 @@ instance_eval do
41
41
  def task(*)
42
42
  task = super
43
43
 
44
- if klass = Thor::RakeCompat.rake_classes.last # rubocop:disable AssignmentInCondition
44
+ if klass = Thor::RakeCompat.rake_classes.last # rubocop:disable Lint/AssignmentInCondition
45
45
  non_namespaced_name = task.name.split(":").last
46
46
 
47
47
  description = non_namespaced_name
@@ -59,7 +59,7 @@ instance_eval do
59
59
  end
60
60
 
61
61
  def namespace(name)
62
- if klass = Thor::RakeCompat.rake_classes.last # rubocop:disable AssignmentInCondition
62
+ if klass = Thor::RakeCompat.rake_classes.last # rubocop:disable Lint/AssignmentInCondition
63
63
  const_name = Thor::Util.camel_case(name.to_s).to_sym
64
64
  klass.const_set(const_name, Class.new(Thor))
65
65
  new_klass = klass.const_get(const_name)