thor 0.9.2 → 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,12 +1,31 @@
1
1
  $:.unshift File.expand_path(File.dirname(__FILE__))
2
- require "getopt"
2
+ require "thor/options"
3
+ require "thor/util"
4
+ require "thor/task"
5
+ require "thor/task_hash"
3
6
 
4
7
  class Thor
5
- def self.inherited(klass)
6
- subclass_files[File.expand_path(caller[0].split(":")[0])] << klass
7
- subclasses << klass unless subclasses.include?(klass)
8
+ attr_accessor :options
9
+
10
+ def self.map(map)
11
+ @map ||= superclass.instance_variable_get("@map") || {}
12
+ map.each do |key, value|
13
+ if key.respond_to?(:each)
14
+ key.each {|subkey| @map[subkey] = value}
15
+ else
16
+ @map[key] = value
17
+ end
18
+ end
19
+ end
20
+
21
+ def self.desc(usage, description)
22
+ @usage, @desc = usage, description
8
23
  end
9
24
 
25
+ def self.method_options(opts)
26
+ @method_options = opts
27
+ end
28
+
10
29
  def self.subclass_files
11
30
  @subclass_files ||= Hash.new {|h,k| h[k] = []}
12
31
  end
@@ -15,117 +34,105 @@ class Thor
15
34
  @subclasses ||= []
16
35
  end
17
36
 
18
- def self.method_added(meth)
19
- meth = meth.to_s
20
- return if !public_instance_methods.include?(meth) || !@usage
21
- @descriptions ||= []
22
- @usages ||= []
23
- @opts ||= []
24
-
25
- @descriptions.delete(@descriptions.assoc(meth))
26
- @descriptions << [meth, @desc]
27
-
28
- @usages.delete(@usages.assoc(meth))
29
- @usages << [meth, @usage]
30
-
31
- @opts.delete(@opts.assoc(meth))
32
- @opts << [meth, @method_options] if @method_options
33
-
34
- @usage, @desc, @method_options = nil
37
+ def self.tasks
38
+ @tasks ||= TaskHash.new(self)
35
39
  end
36
40
 
37
- def self.map(map)
38
- @map ||= superclass.instance_variable_get("@map") || {}
39
- @map.merge! map
41
+ def self.opts
42
+ (@opts || {}).merge(self == Thor ? {} : superclass.opts)
40
43
  end
41
44
 
42
- def self.desc(usage, description)
43
- @usage, @desc = usage, description
44
- end
45
-
46
- def self.method_options(opts)
47
- @method_options = opts.inject({}) do |accum, (k,v)|
48
- accum.merge("--" + k.to_s => v.to_s.upcase)
49
- end
45
+ def self.[](task)
46
+ namespaces = task.split(":")
47
+ klass = Thor::Util.constant_from_thor_path(namespaces[0...-1].join(":"))
48
+ raise Error, "`#{klass}' is not a Thor class" unless klass <= Thor
49
+ klass.tasks[namespaces.last]
50
50
  end
51
51
 
52
- def self.help_list
53
- return nil unless @usages
54
- @help_list ||= begin
55
- max_usage = @usages.max {|x,y| x.last.to_s.size <=> y.last.to_s.size}.last.size
56
- max_opts = @opts.empty? ? 0 : format_opts(@opts.max {|x,y| x.last.to_s.size <=> y.last.to_s.size}.last).size
57
- max_desc = @descriptions.max {|x,y| x.last.to_s.size <=> y.last.to_s.size}.last.size
58
- Struct.new(:klass, :usages, :opts, :descriptions, :max).new(
59
- self, @usages, @opts, @descriptions, Struct.new(:usage, :opt, :desc).new(max_usage, max_opts, max_desc)
60
- )
52
+ def self.maxima
53
+ @maxima ||= begin
54
+ max_usage = tasks.map {|_, t| t.usage}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
55
+ max_desc = tasks.map {|_, t| t.description}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
56
+ max_opts = tasks.map {|_, t| t.opts ? t.opts.formatted_usage : ""}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
57
+ Struct.new(:description, :usage, :opt).new(max_desc, max_usage, max_opts)
61
58
  end
62
59
  end
63
60
 
64
- def self.format_opts(opts)
65
- return "" if !opts
66
- opts.map do |opt, val|
67
- if val == true || val == "BOOLEAN"
68
- "[#{opt}]"
69
- elsif val == "REQUIRED"
70
- opt + "=" + opt.gsub(/\-/, "").upcase
71
- elsif val == "OPTIONAL"
72
- "[" + opt + "=" + opt.gsub(/\-/, "").upcase + "]"
73
- end
74
- end.join(" ")
61
+ def self.start(args = ARGV)
62
+ options = Thor::Options.new(self.opts)
63
+ opts = options.parse(args, false)
64
+ args = options.trailing_non_opts
65
+
66
+ meth = args.first
67
+ meth = @map[meth].to_s if @map && @map[meth]
68
+ meth ||= "help"
69
+
70
+ tasks[meth].parse new(opts, *args), args[1..-1]
71
+ rescue Thor::Error => e
72
+ $stderr.puts e.message
75
73
  end
76
-
77
- def self.start
78
- meth = ARGV.shift
79
- params = []
80
- while !ARGV.empty?
81
- break if ARGV.first =~ /^\-/
82
- params << ARGV.shift
83
- end
84
- if defined?(@map) && @map[meth]
85
- meth = @map[meth].to_s
74
+
75
+ class << self
76
+ protected
77
+ def inherited(klass)
78
+ register_klass_file klass
86
79
  end
87
-
88
- args = ARGV.dup
89
-
90
- if @opts.assoc(meth)
91
- opts = @opts.assoc(meth).last.map {|opt, val| [opt, val == true ? Getopt::BOOLEAN : Getopt.const_get(val)].flatten}
92
- options = Getopt::Long.getopts(*opts)
93
- params << options
80
+
81
+ def method_added(meth)
82
+ meth = meth.to_s
83
+
84
+ if meth == "initialize"
85
+ @opts = @method_options
86
+ @method_options = nil
87
+ return
88
+ end
89
+
90
+ return if !public_instance_methods.include?(meth) || !@usage
91
+ register_klass_file self
92
+
93
+ tasks[meth] = Task.new(meth, @desc, @usage, @method_options)
94
+
95
+ @usage, @desc, @method_options = nil
94
96
  end
95
-
96
- ARGV.replace args
97
-
98
- new(meth, params).instance_variable_get("@results")
99
- end
100
-
101
- def initialize(op, params)
102
- begin
103
- op ||= "help"
104
- @results = send(op.to_sym, *params) if public_methods.include?(op) || !methods.include?(op)
105
- rescue ArgumentError
106
- puts "`#{op}' was called incorrectly. Call as `#{usage(op)}'"
97
+
98
+ def register_klass_file(klass, file = caller[1].split(":")[0])
99
+ unless self == Thor
100
+ superclass.register_klass_file(klass, file)
101
+ return
102
+ end
103
+
104
+ file_subclasses = subclass_files[File.expand_path(file)]
105
+ file_subclasses << klass unless file_subclasses.include?(klass)
106
+ subclasses << klass unless subclasses.include?(klass)
107
107
  end
108
108
  end
109
109
 
110
- public :initialize
111
-
112
- def usage(meth)
113
- list = self.class.help_list
114
- list.usages.assoc(meth)[1] + (list.opts.assoc(meth) ? " " + self.class.format_opts(list.opts.assoc(meth)[1]) : "")
110
+ def initialize(opts = {}, *args)
115
111
  end
116
112
 
117
- map "--help" => :help
113
+ map ["-h", "-?", "--help", "-D"] => :help
118
114
 
119
- desc "help", "show this screen"
120
- def help
121
- list = self.class.help_list
122
- puts "Options"
123
- puts "-------"
124
- list.usages.each do |meth, use|
125
- format = "%-" + (list.max.usage + list.max.opt + 4).to_s + "s"
126
- print format % ("#{usage(meth)}")
127
- puts list.descriptions.assoc(meth)[1]
115
+ desc "help [TASK]", "describe available tasks or one specific task"
116
+ def help(task = nil)
117
+ if task
118
+ if task.include? ?:
119
+ task = self.class[task]
120
+ namespace = true
121
+ else
122
+ task = self.class.tasks[task]
123
+ end
124
+
125
+ puts task.formatted_usage(namespace)
126
+ puts task.description
127
+ else
128
+ puts "Options"
129
+ puts "-------"
130
+ self.class.tasks.each do |_, task|
131
+ format = "%-" + (self.class.maxima.usage + self.class.maxima.opt + 4).to_s + "s"
132
+ print format % ("#{task.formatted_usage}")
133
+ puts task.description.split("\n").first
134
+ end
128
135
  end
129
136
  end
130
137
 
131
- end
138
+ end
@@ -0,0 +1,3 @@
1
+ class Thor
2
+ class Error < Exception; end
3
+ end
@@ -0,0 +1,238 @@
1
+ # This is a modified version of Daniel Berger's Getopt::Long class,
2
+ # licensed under Ruby's license.
3
+
4
+ class Thor
5
+ class Options
6
+ class Error < StandardError; end
7
+
8
+ # simple Hash with indifferent access
9
+ class Hash < ::Hash
10
+ def initialize(hash)
11
+ super()
12
+ update hash
13
+ end
14
+
15
+ def [](key)
16
+ super convert_key(key)
17
+ end
18
+
19
+ def values_at(*indices)
20
+ indices.collect { |key| self[convert_key(key)] }
21
+ end
22
+
23
+ protected
24
+ def convert_key(key)
25
+ key.kind_of?(Symbol) ? key.to_s : key
26
+ end
27
+
28
+ # Magic predicates. For instance:
29
+ # options.force? # => !!options['force']
30
+ def method_missing(method, *args, &block)
31
+ method.to_s =~ /^(\w+)\?$/ ? !!self[$1] : super
32
+ end
33
+ end
34
+
35
+ NUMERIC = /(\d*\.\d+|\d+)/
36
+ LONG_RE = /^(--\w+[-\w+]*)$/
37
+ SHORT_RE = /^(-[a-z])$/i
38
+ EQ_RE = /^(--\w+[-\w+]*|-[a-z])=(.*)$/i
39
+ SHORT_SQ_RE = /^-([a-z]{2,})$/i # Allow either -x -v or -xv style for single char args
40
+ SHORT_NUM = /^(-[a-z])#{NUMERIC}$/i
41
+
42
+ attr_reader :leading_non_opts, :trailing_non_opts
43
+
44
+ def non_opts
45
+ leading_non_opts + trailing_non_opts
46
+ end
47
+
48
+ # Takes an array of switches. Each array consists of up to three
49
+ # elements that indicate the name and type of switch. Returns a hash
50
+ # containing each switch name, minus the '-', as a key. The value
51
+ # for each key depends on the type of switch and/or the value provided
52
+ # by the user.
53
+ #
54
+ # The long switch _must_ be provided. The short switch defaults to the
55
+ # first letter of the short switch. The default type is :boolean.
56
+ #
57
+ # Example:
58
+ #
59
+ # opts = Thor::Options.new(
60
+ # "--debug" => true,
61
+ # ["--verbose", "-v"] => true,
62
+ # ["--level", "-l"] => :numeric
63
+ # ).parse(args)
64
+ #
65
+ def initialize(switches)
66
+ @defaults = {}
67
+ @shorts = {}
68
+
69
+ @switches = switches.inject({}) do |mem, (name, type)|
70
+ if name.is_a?(Array)
71
+ name, *shorts = name
72
+ else
73
+ name = name.to_s
74
+ shorts = []
75
+ end
76
+ # we need both nice and dasherized form of switch name
77
+ if name.index('-') == 0
78
+ nice_name = undasherize name
79
+ else
80
+ nice_name = name
81
+ name = dasherize name
82
+ end
83
+ # if there are no shortcuts specified, generate one using the first character
84
+ shorts << "-" + nice_name[0,1] if shorts.empty? and nice_name.length > 1
85
+ shorts.each { |short| @shorts[short] = name }
86
+
87
+ # normalize type
88
+ case type
89
+ when TrueClass then type = :boolean
90
+ when String
91
+ @defaults[nice_name] = type
92
+ type = :optional
93
+ when Numeric
94
+ @defaults[nice_name] = type
95
+ type = :numeric
96
+ end
97
+
98
+ mem[name] = type
99
+ mem
100
+ end
101
+
102
+ # remove shortcuts that happen to coincide with any of the main switches
103
+ @shorts.keys.each do |short|
104
+ @shorts.delete(short) if @switches.key?(short)
105
+ end
106
+ end
107
+
108
+ def parse(args, skip_leading_non_opts = true)
109
+ @args = args
110
+ # start with Thor::Options::Hash pre-filled with defaults
111
+ hash = Hash.new @defaults
112
+
113
+ @leading_non_opts = []
114
+ if skip_leading_non_opts
115
+ @leading_non_opts << shift until current_is_option? || @args.empty?
116
+ end
117
+
118
+ while current_is_option?
119
+ case shift
120
+ when SHORT_SQ_RE
121
+ unshift $1.split('').map { |f| "-#{f}" }
122
+ next
123
+ when EQ_RE, SHORT_NUM
124
+ unshift $2
125
+ switch = $1
126
+ when LONG_RE, SHORT_RE
127
+ switch = $1
128
+ end
129
+
130
+ switch = normalize_switch(switch)
131
+ nice_name = undasherize(switch)
132
+ type = switch_type(switch)
133
+
134
+ case type
135
+ when :required
136
+ assert_value!(switch)
137
+ raise Error, "cannot pass switch '#{peek}' as an argument" if valid?(peek)
138
+ hash[nice_name] = shift
139
+ when :optional
140
+ hash[nice_name] = peek.nil? || valid?(peek) || shift
141
+ when :boolean
142
+ hash[nice_name] = true
143
+ when :numeric
144
+ assert_value!(switch)
145
+ unless peek =~ NUMERIC and $& == peek
146
+ raise Error, "expected numeric value for '#{switch}'; got #{peek.inspect}"
147
+ end
148
+ hash[nice_name] = $&.index('.') ? shift.to_f : shift.to_i
149
+ end
150
+ end
151
+
152
+ @trailing_non_opts = @args
153
+
154
+ check_required! hash
155
+ hash.freeze
156
+ hash
157
+ end
158
+
159
+ def formatted_usage
160
+ return "" if @switches.empty?
161
+ @switches.map do |opt, type|
162
+ case type
163
+ when :boolean
164
+ "[#{opt}]"
165
+ when :required
166
+ opt + "=" + opt.gsub(/\-/, "").upcase
167
+ else
168
+ sample = @defaults[undasherize(opt)]
169
+ sample ||= case type
170
+ when :optional then opt.gsub(/\-/, "").upcase
171
+ when :numeric then "N"
172
+ end
173
+ "[" + opt + "=" + sample.to_s + "]"
174
+ end
175
+ end.join(" ")
176
+ end
177
+
178
+ private
179
+
180
+ def assert_value!(switch)
181
+ raise Error, "no value provided for argument '#{switch}'" if peek.nil?
182
+ end
183
+
184
+ def undasherize(str)
185
+ str.sub(/^-{1,2}/, '')
186
+ end
187
+
188
+ def dasherize(str)
189
+ (str.length > 1 ? "--" : "-") + str
190
+ end
191
+
192
+ def peek
193
+ @args.first
194
+ end
195
+
196
+ def shift
197
+ @args.shift
198
+ end
199
+
200
+ def unshift(arg)
201
+ unless arg.kind_of?(Array)
202
+ @args.unshift(arg)
203
+ else
204
+ @args = arg + @args
205
+ end
206
+ end
207
+
208
+ def valid?(arg)
209
+ @switches.key?(arg) or @shorts.key?(arg)
210
+ end
211
+
212
+ def current_is_option?
213
+ case peek
214
+ when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
215
+ valid?($1)
216
+ when SHORT_SQ_RE
217
+ $1.split('').any? { |f| valid?("-#{f}") }
218
+ end
219
+ end
220
+
221
+ def normalize_switch(switch)
222
+ @shorts.key?(switch) ? @shorts[switch] : switch
223
+ end
224
+
225
+ def switch_type(switch)
226
+ @switches[switch]
227
+ end
228
+
229
+ def check_required!(hash)
230
+ for name, type in @switches
231
+ if type == :required and !hash[undasherize(name)]
232
+ raise Error, "no value provided for required argument '#{name}'"
233
+ end
234
+ end
235
+ end
236
+
237
+ end
238
+ end