josevalim-thor 0.10.18 → 0.10.19

Sign up to get free protection for your applications and to get access to all the features.
data/lib/thor/actions.rb CHANGED
@@ -52,8 +52,10 @@ class Thor
52
52
  :invoke
53
53
  end
54
54
 
55
- super
56
55
  self.root = config[:root]
56
+ config[:root] = self.root # Cache in the config hash to be shared
57
+
58
+ super
57
59
  end
58
60
 
59
61
  # Wraps an action object and call it accordingly to the thor class behavior.
@@ -298,12 +300,6 @@ class Thor
298
300
 
299
301
  protected
300
302
 
301
- # Allow current root to be sent as configuration value to the invoked class.
302
- #
303
- def _overrides_config #:nodoc:
304
- super.merge!(:root => self.root)
305
- end
306
-
307
303
  def _cleanup_options_and_set(options, key)
308
304
  [:force, :skip, "force", "skip"].each { |i| options.delete(i) }
309
305
  options.merge!(key => true)
data/lib/thor/base.rb CHANGED
@@ -3,7 +3,7 @@ require 'thor/core_ext/ordered_hash'
3
3
  require 'thor/error'
4
4
  require 'thor/shell'
5
5
  require 'thor/invocation'
6
- require 'thor/options'
6
+ require 'thor/parser'
7
7
  require 'thor/task'
8
8
  require 'thor/util'
9
9
 
@@ -31,15 +31,40 @@ class Thor
31
31
  # config<Hash>:: Configuration for this Thor class.
32
32
  #
33
33
  def initialize(args=[], options={}, config={})
34
- # Set arguments and default values
35
- self.class.arguments.zip(args).each do |argument, value|
36
- send("#{argument.human_name}=", value || argument.default)
34
+ Thor::Arguments.parse(self.class.arguments, args).each do |key, value|
35
+ send("#{key}=", value)
37
36
  end
38
37
 
39
- self.class.merge_with_thor_options(options, self.class.class_options)
38
+ if options.is_a?(Array)
39
+ task_options = config.delete(:task_options)
40
+ parse_options = self.class.class_options
41
+ parse_options = parse_options.merge(task_options) if task_options
42
+
43
+ options = Thor::Options.parse(parse_options, options)
44
+ _set_default_options(options, task_options) if task_options
45
+ end
46
+
47
+ _set_default_options(options, self.class.class_options)
40
48
  self.options = Thor::CoreExt::HashWithIndifferentAccess.new(options).freeze
41
49
  end
42
50
 
51
+ protected
52
+
53
+ # Receives a hash of options, stringify keys and merge with default
54
+ # values from thor options.
55
+ #
56
+ def _set_default_options(options, default_options)
57
+ options.each_key do |key|
58
+ next unless key.is_a?(Symbol)
59
+ options[key.to_s] = options.delete(key)
60
+ end
61
+
62
+ default_options.each do |key, option|
63
+ next if option.default.nil? || options.key?(option.human_name.to_s)
64
+ options[option.human_name.to_s] ||= option.default
65
+ end
66
+ end
67
+
43
68
  class << self
44
69
  def included(base) #:nodoc:
45
70
  base.send :extend, ClassMethods
@@ -130,14 +155,16 @@ class Thor
130
155
  options[:default].nil?
131
156
  end
132
157
 
133
- class_options.values.each do |option|
134
- next unless option.argument? && !option.required?
158
+ remove_argument name
159
+
160
+ arguments.each do |argument|
161
+ next if argument.required?
135
162
  raise ArgumentError, "You cannot have #{name.to_s.inspect} as required argument after " <<
136
- "the non-required argument #{option.human_name.inspect}."
163
+ "the non-required argument #{argument.human_name.inspect}."
137
164
  end if required
138
165
 
139
- class_options[name] = Thor::Argument.new(name, options[:desc], required, options[:type],
140
- options[:default], options[:banner])
166
+ arguments << Thor::Argument.new(name, options[:desc], required, options[:type],
167
+ options[:default], options[:banner])
141
168
  end
142
169
 
143
170
  # Returns this class arguments, looking up in the ancestors chain.
@@ -146,7 +173,7 @@ class Thor
146
173
  # Array[Thor::Argument]
147
174
  #
148
175
  def arguments
149
- class_options.values.select{ |o| o.argument? }
176
+ @arguments ||= from_superclass(:arguments, [])
150
177
  end
151
178
 
152
179
  # Adds a bunch of options to the set of class options.
@@ -159,7 +186,7 @@ class Thor
159
186
  # Hash[Symbol => Object]
160
187
  #
161
188
  def class_options(options=nil)
162
- @class_options ||= from_superclass(:class_options, Thor::CoreExt::OrderedHash.new)
189
+ @class_options ||= from_superclass(:class_options, {})
163
190
  build_options(options, @class_options) if options
164
191
  @class_options
165
192
  end
@@ -199,7 +226,7 @@ class Thor
199
226
  options = names.last.is_a?(Hash) ? names.pop : {}
200
227
 
201
228
  names.each do |name|
202
- class_options.delete(name)
229
+ arguments.delete_if { |a| a.name == name.to_s }
203
230
  undef_method name, "#{name}=" if options[:undefine]
204
231
  end
205
232
  end
@@ -332,41 +359,6 @@ class Thor
332
359
  end
333
360
  end
334
361
 
335
- # Parses the task and options from the given args, instantiate the class
336
- # and invoke the task. This method is used when the arguments must be parsed
337
- # from an array. If you are inside Ruby and want to use a Thor class, you
338
- # can simply initialize it:
339
- #
340
- # script = MyScript.new(args, options, config)
341
- # script.invoke(:task, first_arg, second_arg, third_arg)
342
- #
343
- def start(args=ARGV, config={})
344
- config[:shell] ||= Thor::Base.shell.new
345
-
346
- task = normalize_arguments(args, config)
347
- return unless task
348
-
349
- instance, trailing = prepare(task, args, config)
350
- instance.invoke(task, trailing)
351
- rescue Thor::Error => e
352
- config[:shell].error e.message
353
- end
354
-
355
- # Receives a hash of options, stringify keys and merge with default
356
- # values from thor options.
357
- #
358
- def merge_with_thor_options(options, thor_options)
359
- options.each_key do |key|
360
- next unless key.is_a?(Symbol)
361
- options[key.to_s] = options.delete(key)
362
- end
363
-
364
- thor_options.each do |key, option|
365
- next if option.argument? || option.default.nil? || options.key?(option.human_name.to_s)
366
- options[option.human_name.to_s] ||= option.default
367
- end
368
- end
369
-
370
362
  protected
371
363
 
372
364
  # Prints the class options per group. If an option does not belong to
@@ -380,13 +372,17 @@ class Thor
380
372
  #
381
373
  def class_options_help(shell, ungrouped_name=nil, extra_group=nil) #:nodoc:
382
374
  unless self.class_options.empty?
383
- groups = self.class_options.group_values_by { |o| o.group }
375
+ groups = {}
376
+
377
+ class_options.each do |_, value|
378
+ groups[value.group] ||= []
379
+ groups[value.group] << value
380
+ end
384
381
 
385
382
  printer = lambda do |group_name, options|
386
383
  list = []
387
384
 
388
385
  options.each do |option|
389
- next if option.argument?
390
386
  list << [ option.usage, option.description || "" ]
391
387
  list << [ "", "Default: #{option.default}" ] if option.show_default?
392
388
  end
@@ -516,18 +512,6 @@ class Thor
516
512
  # class.
517
513
  def initialize_added #:nodoc:
518
514
  end
519
-
520
- # SIGNATURE: Normalize arguments when invoked through start. Should
521
- # return the name of the task to be invoked. Returning nil makes the
522
- # start process to exist without error message.
523
- def normalize_arguments(args, config) #:nodoc:
524
- end
525
-
526
- # SIGNATURE: Receives a task, arguments to be parsed and configuration
527
- # values and initializes the current class. Trailing arguments are
528
- # returned to be sent to the invoked task.
529
- def prepare(task, args, config) #:nodoc:
530
- end
531
515
  end
532
516
  end
533
517
  end
@@ -1,3 +1,5 @@
1
+ require 'forwardable'
2
+
1
3
  class Thor #:nodoc:
2
4
  module CoreExt #:nodoc:
3
5
 
@@ -15,40 +17,13 @@ class Thor #:nodoc:
15
17
  @hash = {}
16
18
  end
17
19
 
18
- # Called on clone. It gets all the notes from the cloned object, dup them
19
- # and assign the duped objects siblings.
20
- #
21
- def initialize_copy(other)
22
- @hash = {}
23
-
24
- array = []
25
- other.each do |key, value|
26
- array << (@hash[key] = Node.new(key, value))
27
- end
28
-
29
- array.each_with_index do |node, i|
30
- node.next = array[i + 1]
31
- node.prev = array[i - 1] if i > 0
32
- end
33
-
34
- @first = array.first
35
- @last = array.last
36
- end
37
-
38
20
  def [](key)
39
21
  @hash[key] && @hash[key].value
40
22
  end
41
23
 
42
24
  def []=(key, value)
43
- if old = @hash[key]
44
- node = old.dup
25
+ if node = @hash[key]
45
26
  node.value = value
46
-
47
- @first = node if @first == old
48
- @last = node if @last == old
49
-
50
- old.prev.next = node if old.prev
51
- old.next.prev = node if old.next
52
27
  else
53
28
  node = Node.new(key, value)
54
29
 
@@ -79,7 +54,7 @@ class Thor #:nodoc:
79
54
  value = node.value
80
55
  end
81
56
 
82
- @hash[key] = nil
57
+ @hash.delete(key)
83
58
  value
84
59
  end
85
60
 
@@ -99,41 +74,23 @@ class Thor #:nodoc:
99
74
  self
100
75
  end
101
76
 
102
- def group_values_by
103
- assoc = self.class.new
104
- each do |_, element|
105
- key = yield(element)
106
- assoc[key] ||= []
107
- assoc[key] << element
108
- end
109
- assoc
110
- end
111
-
112
77
  def merge(other)
113
- dup.merge!(other)
114
- end
78
+ hash = self.class.new
79
+
80
+ self.each do |key, value|
81
+ hash[key] = value
82
+ end
115
83
 
116
- def merge!(other)
117
84
  other.each do |key, value|
118
- self[key] = value
85
+ hash[key] = value
119
86
  end
120
- self
87
+
88
+ hash
121
89
  end
122
90
 
123
91
  def empty?
124
92
  @hash.empty?
125
93
  end
126
-
127
- def to_a
128
- array = []
129
- each { |k, v| array << [k, v] }
130
- array
131
- end
132
-
133
- def to_s
134
- to_a.inspect
135
- end
136
- alias :inspect :to_s
137
94
  end
138
95
  end
139
96
  end
data/lib/thor/group.rb CHANGED
@@ -18,17 +18,21 @@ class Thor::Group
18
18
  end
19
19
  end
20
20
 
21
- # Implements the prepare interface being used by start.
21
+ # Start works differently in Thor::Group, it simply invokes all tasks
22
+ # inside the class.
22
23
  #
23
- def prepare(task, args, config) #:nodoc:
24
- opts = Thor::Options.new(class_options)
25
- opts.parse(args)
24
+ def start(given_args=ARGV, config={})
25
+ config[:shell] ||= Thor::Base.shell.new
26
26
 
27
- instance = new(opts.arguments, opts.options, config) do |klass, invoke, overrides|
28
- klass.prepare(invoke, args, config.merge(overrides))
27
+ if Thor::HELP_MAPPINGS.include?(given_args.first)
28
+ help(config[:shell])
29
+ return
29
30
  end
30
31
 
31
- return instance, nil
32
+ args, opts = Thor::Options.split(given_args)
33
+ new(args, opts, config).invoke
34
+ rescue Thor::Error => e
35
+ config[:shell].error e.message
32
36
  end
33
37
 
34
38
  # Prints help information.
@@ -54,7 +58,7 @@ class Thor::Group
54
58
  # thor class by another means which is not the Thor::Runner.
55
59
  #
56
60
  def banner #:nodoc:
57
- "#{self.namespace} #{self.arguments.map {|o| o.usage }.join(' ')}"
61
+ "#{self.namespace} #{self.arguments.map {|a| a.usage }.join(' ')}"
58
62
  end
59
63
 
60
64
  def baseclass #:nodoc:
@@ -68,26 +72,7 @@ class Thor::Group
68
72
  def create_task(meth) #:nodoc:
69
73
  tasks[meth.to_s] = Thor::Task.new(meth, nil, nil, nil)
70
74
  end
71
-
72
- def normalize_arguments(args, config) #:nodoc:
73
- if Thor::HELP_MAPPINGS.include?(args.first)
74
- help(config[:shell])
75
- nil
76
- else
77
- :all
78
- end
79
- end
80
75
  end
81
76
 
82
77
  include Thor::Base
83
-
84
- protected
85
-
86
- # Overwrite _setup_for_invoke to force invocation of all tasks when :all is
87
- # supplied.
88
- #
89
- def _setup_for_invoke(object, task=nil)
90
- super(object.to_s == "all" ? nil : object)
91
- end
92
-
93
78
  end
@@ -5,16 +5,19 @@ class Thor
5
5
  #
6
6
  def initialize(args=[], options={}, config={}, &block) #:nodoc:
7
7
  @_invocations = config[:invocations] || Hash.new { |h,k| h[k] = [] }
8
- @_initializer = block || lambda do |klass, invoke, overrides|
9
- klass.new(args, options, config.merge(overrides))
10
- end
8
+ config[:invocations] = @_invocations # Cache in the config hash to be shared
9
+
10
+ @_initializer = [ args, options, config ]
11
11
  super
12
12
  end
13
13
 
14
14
  # Receives a name and invokes it. The name can be a string (either "task" or
15
15
  # "namespace:task"), a Thor::Task, a Class or a Thor instance. If the task
16
- # cannot be guessed name, it can also be supplied as second argument. The
17
- # arguments used to invoke the task are always supplied as the last argument.
16
+ # cannot be guessed by name, it can also be supplied as second argument.
17
+ #
18
+ # You can also supply the arguments, options and configuration values for
19
+ # the task to be invoked, if none is given, the same values used to
20
+ # initialize the invoker are used to initialize the invoked.
18
21
  #
19
22
  # ==== Examples
20
23
  #
@@ -54,7 +57,7 @@ class Thor
54
57
  # end
55
58
  # end
56
59
  #
57
- # As you notice, it invokes the given mock framework, which might have its
60
+ # As you noticed, it invokes the given mock framework, which might have its
58
61
  # own options:
59
62
  #
60
63
  # class Rspec::RR < Thor::Group
@@ -68,29 +71,30 @@ class Thor
68
71
  # If you want Rspec::RR to be initialized with its own set of options, you
69
72
  # have to do that explicitely:
70
73
  #
71
- # invoke Rspec::RR.new([], :style => :foo)
74
+ # invoke "rspec:rr", [], :style => :foo
72
75
  #
73
76
  # Besides giving an instance, you can also give a class to invoke:
74
77
  #
75
- # invoke Rspec::RR
76
- #
77
- # Or even a class, the task to invoke from it and its arguments:
78
- #
79
- # invoke Rspec::RR, :foo, [ args ]
78
+ # invoke Rspec::RR, [], :style => :foo
80
79
  #
81
- def invoke(name, task=nil, method_args=nil)
82
- task, method_args = nil, task if task.is_a?(Array)
80
+ def invoke(name=nil, task=nil, args=nil, opts=nil, config={})
81
+ task, args, opts, config = nil, task, args, opts if task.is_a?(Array)
83
82
  object, task = _setup_for_invoke(name, task)
84
83
 
85
84
  if object.is_a?(Class)
86
85
  klass = object
87
- instance, trailing = @_initializer.call(klass, task, _overrides_config)
88
- method_args ||= trailing
86
+
87
+ stored_args, stored_opts, stored_config = @_initializer
88
+ args ||= stored_args.dup
89
+ opts ||= stored_opts.dup
90
+ config = stored_config.merge(config)
91
+
92
+ instance = klass.new(args, opts, config)
89
93
  else
90
94
  klass, instance = object.class, object
91
95
  end
92
96
 
93
- method_args ||= []
97
+ method_args = []
94
98
  current = @_invocations[klass]
95
99
 
96
100
  iterator = lambda do |_, task|
@@ -101,21 +105,16 @@ class Thor
101
105
  end
102
106
 
103
107
  if task
108
+ args ||= []
109
+ method_args = args[Range.new(klass.arguments.size, -1)] || []
104
110
  iterator.call(nil, task)
105
111
  else
106
- method_args = []
107
112
  klass.all_tasks.map(&iterator)
108
113
  end
109
114
  end
110
115
 
111
116
  protected
112
117
 
113
- # Values that are sent to overwrite defined configuration values.
114
- #
115
- def _overrides_config #:nodoc:
116
- { :invocations => @_invocations }
117
- end
118
-
119
118
  # This is the method responsable for retrieving and setting up an
120
119
  # instance to be used in invoke.
121
120
  #
@@ -0,0 +1,66 @@
1
+ class Thor
2
+ class Argument
3
+ VALID_TYPES = [ :numeric, :hash, :array, :string ]
4
+
5
+ attr_reader :name, :description, :required, :type, :default, :banner
6
+ alias :human_name :name
7
+
8
+ def initialize(name, description=nil, required=true, type=:string, default=nil, banner=nil)
9
+ class_name = self.class.name.split("::").last
10
+
11
+ raise ArgumentError, "#{class_name} name can't be nil." if name.nil?
12
+ raise ArgumentError, "#{class_name} cannot be required and have default value." if required && !default.nil?
13
+ raise ArgumentError, "Type :#{type} is not valid for #{class_name.downcase}s." if type && !valid_type?(type)
14
+
15
+ @name = name.to_s
16
+ @description = description
17
+ @required = required || false
18
+ @type = (type || :default).to_sym
19
+ @default = default
20
+ @banner = banner || default_banner
21
+ end
22
+
23
+ def usage
24
+ required? ? banner : "[#{banner}]"
25
+ end
26
+
27
+ def required?
28
+ required
29
+ end
30
+
31
+ def show_default?
32
+ case default
33
+ when Array, String, Hash
34
+ !default.empty?
35
+ else
36
+ default
37
+ end
38
+ end
39
+
40
+ def input_required?
41
+ [ :numeric, :hash, :array, :string ].include?(type)
42
+ end
43
+
44
+ protected
45
+
46
+ def valid_type?(type)
47
+ VALID_TYPES.include?(type.to_sym)
48
+ end
49
+
50
+ def default_banner
51
+ case type
52
+ when :boolean
53
+ nil
54
+ when :string, :default
55
+ human_name.upcase
56
+ when :numeric
57
+ "N"
58
+ when :hash
59
+ "key:value"
60
+ when :array
61
+ "one two three"
62
+ end
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,140 @@
1
+ class Thor
2
+ class Arguments
3
+ NUMERIC = /(\d*\.\d+|\d+)/
4
+
5
+ # Receives an array of args and returns two arrays, one with arguments
6
+ # and one with switches.
7
+ #
8
+ def self.split(args)
9
+ arguments = []
10
+
11
+ args.each do |item|
12
+ break if item =~ /^-/
13
+ arguments << item
14
+ end
15
+
16
+ return arguments, args[Range.new(arguments.size, -1)]
17
+ end
18
+
19
+ def self.parse(base, args)
20
+ new(base).parse(args)
21
+ end
22
+
23
+ # Takes an array of Thor::Argument objects.
24
+ #
25
+ def initialize(arguments=[])
26
+ @arguments = arguments
27
+ @non_assigned_required = @arguments.select { |a| a.required? }
28
+ end
29
+
30
+ def parse(args)
31
+ @pile, assigns = args.dup, {}
32
+
33
+ @arguments.each do |argument|
34
+ assigns[argument.human_name] = if peek
35
+ @non_assigned_required.delete(argument)
36
+ send(:"parse_#{argument.type}", argument.human_name)
37
+ else
38
+ argument.default
39
+ end
40
+ end
41
+
42
+ check_requirement!
43
+ assigns
44
+ end
45
+
46
+ private
47
+
48
+ def peek
49
+ @pile.first
50
+ end
51
+
52
+ def shift
53
+ @pile.shift
54
+ end
55
+
56
+ def unshift(arg)
57
+ unless arg.kind_of?(Array)
58
+ @pile.unshift(arg)
59
+ else
60
+ @pile = arg + @pile
61
+ end
62
+ end
63
+
64
+ def current_is_value?
65
+ peek && peek.to_s !~ /^-/
66
+ end
67
+
68
+ # Runs through the argument array getting strings that contains ":" and
69
+ # mark it as a hash:
70
+ #
71
+ # [ "name:string", "age:integer" ]
72
+ #
73
+ # Becomes:
74
+ #
75
+ # { "name" => "string", "age" => "integer" }
76
+ #
77
+ def parse_hash(name)
78
+ return shift if peek.is_a?(Hash)
79
+ hash = {}
80
+
81
+ while current_is_value? && peek.include?(?:)
82
+ key, value = shift.split(':')
83
+ hash[key] = value
84
+ end
85
+ hash
86
+ end
87
+
88
+ # Runs through the argument array getting all strings until no string is
89
+ # found or a switch is found.
90
+ #
91
+ # ["a", "b", "c"]
92
+ #
93
+ # And returns it as an array:
94
+ #
95
+ # ["a", "b", "c"]
96
+ #
97
+ def parse_array(name)
98
+ return shift if peek.is_a?(Array)
99
+ array = []
100
+
101
+ while current_is_value?
102
+ array << shift
103
+ end
104
+ array
105
+ end
106
+
107
+ # Check if the peel is numeric ofrmat and return a Float or Integer.
108
+ # Otherwise raises an error.
109
+ #
110
+ def parse_numeric(name)
111
+ return shift if peek.is_a?(Numeric)
112
+
113
+ unless peek =~ NUMERIC && $& == peek
114
+ raise MalformattedArgumentError, "expected numeric value for '#{name}'; got #{peek.inspect}"
115
+ end
116
+
117
+ $&.index('.') ? shift.to_f : shift.to_i
118
+ end
119
+
120
+ # Parse string, i.e., just return the current value in the pile.
121
+ #
122
+ def parse_string(name)
123
+ shift
124
+ end
125
+
126
+ # Raises an error if @non_assigned_required array is not empty.
127
+ #
128
+ def check_requirement!
129
+ unless @non_assigned_required.empty?
130
+ names = @non_assigned_required.map do |o|
131
+ o.respond_to?(:switch_name) ? o.switch_name : o.human_name
132
+ end.join("', '")
133
+
134
+ class_name = self.class.name.split('::').last.downcase
135
+ raise RequiredArgumentMissingError, "no value provided for required #{class_name} '#{names}'"
136
+ end
137
+ end
138
+
139
+ end
140
+ end