thor 0.9.2 → 0.9.5

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,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