josevalim-thor 0.10.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.
@@ -0,0 +1,282 @@
1
+ require 'thor/option'
2
+
3
+ class Thor
4
+
5
+ # This is a modified version of Daniel Berger's Getopt::Long class, licensed
6
+ # under Ruby's license.
7
+ #
8
+ class Options
9
+ NUMERIC = /(\d*\.\d+|\d+)/
10
+ LONG_RE = /^(--\w+[-\w+]*)$/
11
+ SHORT_RE = /^(-[a-z])$/i
12
+ EQ_RE = /^(--\w+[-\w+]*|-[a-z])=(.*)$/i
13
+ SHORT_SQ_RE = /^-([a-z]{2,})$/i # Allow either -x -v or -xv style for single char args
14
+ SHORT_NUM = /^(-[a-z])#{NUMERIC}$/i
15
+
16
+ # Receives a hash and makes it switches.
17
+ #
18
+ def self.to_switches(options)
19
+ options.map do |key, value|
20
+ case value
21
+ when true
22
+ "--#{key}"
23
+ when Array
24
+ "--#{key} #{value.map{ |v| v.inspect }.join(' ')}"
25
+ when Hash
26
+ "--#{key} #{value.map{ |k,v| "#{k}:#{v}" }.join(' ')}"
27
+ when nil, false
28
+ ""
29
+ else
30
+ "--#{key} #{value.inspect}"
31
+ end
32
+ end.join(" ")
33
+ end
34
+
35
+ attr_reader :arguments, :options, :trailing
36
+
37
+ # Takes an array of switches. Each array consists of up to three
38
+ # elements that indicate the name and type of switch. Returns a hash
39
+ # containing each switch name, minus the '-', as a key. The value
40
+ # for each key depends on the type of switch and/or the value provided
41
+ # by the user.
42
+ #
43
+ # The long switch _must_ be provided. The short switch defaults to the
44
+ # first letter of the short switch. The default type is :boolean.
45
+ #
46
+ # Example:
47
+ #
48
+ # opts = Thor::Options.new(
49
+ # "--debug" => true,
50
+ # ["--verbose", "-v"] => true,
51
+ # ["--level", "-l"] => :numeric
52
+ # ).parse(args)
53
+ #
54
+ def initialize(switches={})
55
+ @arguments, @shorts, @options = [], {}, {}
56
+ @non_assigned_required, @non_assigned_arguments, @trailing = [], [], []
57
+
58
+ @switches = switches.values.inject({}) do |mem, option|
59
+ @non_assigned_required << option if option.required?
60
+
61
+ if option.argument?
62
+ @non_assigned_arguments << option
63
+ elsif option.default
64
+ @options[option.human_name] = option.default
65
+ end
66
+
67
+ # If there are no shortcuts specified, generate one using the first character
68
+ shorts = option.aliases.dup
69
+ shorts << "-" + option.human_name[0,1] if shorts.empty? and option.human_name.length > 1
70
+ shorts.each { |short| @shorts[short.to_s] ||= option.switch_name }
71
+
72
+ mem[option.switch_name] = option
73
+ mem
74
+ end
75
+
76
+ remove_duplicated_shortcuts!
77
+ end
78
+
79
+ def parse(args)
80
+ @pile, @trailing = args, []
81
+
82
+ while peek
83
+ if current_is_switch?
84
+ case shift
85
+ when SHORT_SQ_RE
86
+ unshift($1.split('').map { |f| "-#{f}" })
87
+ next
88
+ when EQ_RE, SHORT_NUM
89
+ unshift($2)
90
+ switch = $1
91
+ when LONG_RE, SHORT_RE
92
+ switch = $1
93
+ end
94
+
95
+ switch = normalize_switch(switch)
96
+ option = switch_option(switch)
97
+
98
+ next if option.nil? || option.argument?
99
+
100
+ check_requirement!(switch, option)
101
+ parse_option(switch, option, @options)
102
+ else
103
+ unless @non_assigned_arguments.empty?
104
+ argument = @non_assigned_arguments.shift
105
+ parse_option(argument.switch_name, argument, @options)
106
+ @arguments << @options.delete(argument.human_name)
107
+ else
108
+ @trailing << shift
109
+ end
110
+ end
111
+ end
112
+
113
+ assign_arguments_default_values!
114
+ check_validity!
115
+ @options
116
+ end
117
+
118
+ private
119
+
120
+ def peek
121
+ @pile.first
122
+ end
123
+
124
+ def shift
125
+ @pile.shift
126
+ end
127
+
128
+ def unshift(arg)
129
+ unless arg.kind_of?(Array)
130
+ @pile.unshift(arg)
131
+ else
132
+ @pile = arg + @pile
133
+ end
134
+ end
135
+
136
+ # Returns true if the current peek is a switch.
137
+ #
138
+ def current_is_switch?
139
+ case peek
140
+ when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
141
+ switch?($1)
142
+ when SHORT_SQ_RE
143
+ $1.split('').any? { |f| switch?("-#{f}") }
144
+ end
145
+ end
146
+
147
+ # Check if the given argument matches with a switch.
148
+ #
149
+ def switch?(arg)
150
+ switch_option(arg) || @shorts.key?(arg)
151
+ end
152
+
153
+ # Returns the option object for the given switch.
154
+ #
155
+ def switch_option(arg)
156
+ if arg =~ /^--no-(\w+)$/
157
+ @switches[arg] || @switches["--#{$1}"]
158
+ else
159
+ @switches[arg]
160
+ end
161
+ end
162
+
163
+ # Check if the given argument is actually a shortcut.
164
+ #
165
+ def normalize_switch(arg)
166
+ @shorts.key?(arg) ? @shorts[arg] : arg
167
+ end
168
+
169
+ # Receives switch, option and the current values hash and assign the next
170
+ # value to it. At the end, remove the option from the array where non
171
+ # assigned requireds are kept.
172
+ #
173
+ def parse_option(switch, option, hash)
174
+ human_name = option.human_name
175
+
176
+ case option.type
177
+ when :default
178
+ hash[human_name] = peek.nil? || peek.to_s =~ /^-/ || shift
179
+ when :boolean
180
+ if !@switches.key?(switch) && switch =~ /^--no-(\w+)$/
181
+ hash[$1] = false
182
+ else
183
+ hash[human_name] = true
184
+ end
185
+ when :string
186
+ hash[human_name] = shift
187
+ when :numeric
188
+ hash[human_name] = parse_numeric(switch)
189
+ when :hash
190
+ hash[human_name] = parse_hash
191
+ when :array
192
+ hash[human_name] = parse_array
193
+ end
194
+
195
+ @non_assigned_required.delete(option)
196
+ end
197
+
198
+ # Runs through the argument array getting strings that contains ":" and
199
+ # mark it as a hash:
200
+ #
201
+ # [ "name:string", "age:integer" ]
202
+ #
203
+ # Becomes:
204
+ #
205
+ # { "name" => "string", "age" => "integer" }
206
+ #
207
+ def parse_hash
208
+ hash = {}
209
+
210
+ while peek && peek !~ /^\-/
211
+ key, value = shift.split(':')
212
+ hash[key] = value
213
+ end
214
+
215
+ hash
216
+ end
217
+
218
+ # Runs through the argument array getting all strings until no string is
219
+ # found or a switch is found.
220
+ #
221
+ # ["a", "b", "c"]
222
+ #
223
+ # And returns it as an array:
224
+ #
225
+ # ["a", "b", "c"]
226
+ #
227
+ def parse_array
228
+ array = []
229
+
230
+ while peek && peek !~ /^\-/
231
+ array << shift
232
+ end
233
+
234
+ array
235
+ end
236
+
237
+ # Check if the peel is numeric ofrmat and return a Float or Integer.
238
+ # Otherwise raises an error.
239
+ #
240
+ def parse_numeric(switch)
241
+ unless peek =~ NUMERIC && $& == peek
242
+ raise MalformattedArgumentError, "expected numeric value for '#{switch}'; got #{peek.inspect}"
243
+ end
244
+ $&.index('.') ? shift.to_f : shift.to_i
245
+ end
246
+
247
+ # Raises an error if the option requires an input but it's not present.
248
+ #
249
+ def check_requirement!(switch, option)
250
+ if option.input_required?
251
+ raise RequiredArgumentMissingError, "no value provided for argument '#{switch}'" if peek.nil?
252
+ raise MalformattedArgumentError, "cannot pass switch '#{peek}' as an argument" if switch?(peek)
253
+ end
254
+ end
255
+
256
+ # Raises an error if @required array is not empty after parsing.
257
+ #
258
+ def check_validity!
259
+ unless @non_assigned_required.empty?
260
+ switch_names = @non_assigned_required.map{ |o| o.switch_name }.join(', ')
261
+ raise RequiredArgumentMissingError, "no value provided for required arguments '#{switch_names}'"
262
+ end
263
+ end
264
+
265
+ # Assign default values to the argument hash.
266
+ #
267
+ def assign_arguments_default_values!
268
+ @non_assigned_arguments.each do |option|
269
+ @arguments << option.default
270
+ end
271
+ end
272
+
273
+ # Remove shortcuts that happen to coincide with any of the main switches
274
+ #
275
+ def remove_duplicated_shortcuts!
276
+ @shorts.keys.each do |short|
277
+ @shorts.delete(short) if @switches.key?(short)
278
+ end
279
+ end
280
+
281
+ end
282
+ end
@@ -0,0 +1,296 @@
1
+ require 'fileutils'
2
+ require 'open-uri'
3
+ require 'yaml'
4
+ require 'digest/md5'
5
+ require 'pathname'
6
+
7
+ class Thor::Runner < Thor
8
+ map "-T" => :list, "-i" => :install, "-u" => :update
9
+
10
+ # Override Thor#help so it can give information about any class and any method.
11
+ #
12
+ def help(meth=nil)
13
+ if meth && !self.respond_to?(meth)
14
+ initialize_thorfiles(meth)
15
+ klass, task = Thor::Util.namespace_to_thor_class(meth)
16
+ klass.start(["-h", task].compact, :shell => self.shell) # send mapping -h because it works with Thor::Group too
17
+ else
18
+ super
19
+ end
20
+ end
21
+
22
+ # If a task is not found on Thor::Runner, method missing is invoked and
23
+ # Thor::Runner is then responsable for finding the task in all classes.
24
+ #
25
+ def method_missing(meth, *args)
26
+ meth = meth.to_s
27
+ initialize_thorfiles(meth)
28
+ klass, task = Thor::Util.namespace_to_thor_class(meth)
29
+ args.unshift(task) if task
30
+ klass.start(args, :shell => self.shell)
31
+ end
32
+
33
+ desc "install NAME", "Install a Thor file into your system tasks, optionally named for future updates"
34
+ method_options :as => :optional, :relative => :boolean
35
+ def install(name)
36
+ initialize_thorfiles
37
+
38
+ # If a directory name is provided as the argument, look for a 'main.thor'
39
+ # task in said directory.
40
+ begin
41
+ if File.directory?(File.expand_path(name))
42
+ base, package = File.join(name, "main.thor"), :directory
43
+ contents = open(base).read
44
+ else
45
+ base, package = name, :file
46
+ contents = open(name).read
47
+ end
48
+ rescue OpenURI::HTTPError
49
+ raise Error, "Error opening URI '#{name}'"
50
+ rescue Errno::ENOENT
51
+ raise Error, "Error opening file '#{name}'"
52
+ end
53
+
54
+ say "Your Thorfile contains:"
55
+ say contents
56
+
57
+ return false if no?("Do you wish to continue [y/N]?")
58
+
59
+ as = options["as"] || begin
60
+ first_line = contents.split("\n")[0]
61
+ (match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil
62
+ end
63
+
64
+ unless as
65
+ basename = File.basename(name)
66
+ as = ask("Please specify a name for #{name} in the system repository [#{basename}]:")
67
+ as = basename if as.empty?
68
+ end
69
+
70
+ location = if options[:relative] || name =~ /^http:\/\//
71
+ name
72
+ else
73
+ File.expand_path(name)
74
+ end
75
+
76
+ thor_yaml[as] = {
77
+ :filename => Digest::MD5.hexdigest(name + as),
78
+ :location => location,
79
+ :namespaces => Thor::Util.namespaces_in_contents(contents, base)
80
+ }
81
+
82
+ save_yaml(thor_yaml)
83
+ say "Storing thor file in your system repository"
84
+ destination = File.join(thor_root, thor_yaml[as][:filename])
85
+
86
+ if package == :file
87
+ File.open(destination, "w") { |f| f.puts contents }
88
+ else
89
+ FileUtils.cp_r(name, destination)
90
+ end
91
+
92
+ thor_yaml[as][:filename] # Indicate success
93
+ end
94
+
95
+ desc "uninstall NAME", "Uninstall a named Thor module"
96
+ def uninstall(name)
97
+ raise Error, "Can't find module '#{name}'" unless thor_yaml[name]
98
+ say "Uninstalling #{name}."
99
+ FileUtils.rm_rf(File.join(thor_root, "#{thor_yaml[name][:filename]}"))
100
+
101
+ thor_yaml.delete(name)
102
+ save_yaml(thor_yaml)
103
+
104
+ puts "Done."
105
+ end
106
+
107
+ desc "update NAME", "Update a Thor file from its original location"
108
+ def update(name)
109
+ raise Error, "Can't find module '#{name}'" if !thor_yaml[name] || !thor_yaml[name][:location]
110
+
111
+ say "Updating '#{name}' from #{thor_yaml[name][:location]}"
112
+
113
+ old_filename = thor_yaml[name][:filename]
114
+ self.options = self.options.merge("as" => name)
115
+ filename = install(thor_yaml[name][:location])
116
+
117
+ unless filename == old_filename
118
+ File.delete(File.join(thor_root, old_filename))
119
+ end
120
+ end
121
+
122
+ desc "installed", "List the installed Thor modules and tasks"
123
+ method_options :internal => :boolean
124
+ def installed
125
+ initialize_thorfiles(nil, true)
126
+
127
+ klasses = Thor::Base.subclasses
128
+ klasses -= [Thor, Thor::Runner] unless options["internal"]
129
+
130
+ display_klasses(true, klasses)
131
+ end
132
+
133
+ desc "list [SEARCH]",
134
+ "List the available thor tasks (--substring means SEARCH anywhere in the namespace)"
135
+ method_options :substring => :boolean, :group => :optional, :all => :boolean
136
+ def list(search="")
137
+ initialize_thorfiles
138
+
139
+ search = ".*#{search}" if options["substring"]
140
+ search = /^#{search}.*/i
141
+ group = options[:group] || "standard"
142
+
143
+ klasses = Thor::Base.subclasses.select do |k|
144
+ (options[:all] || k.group_name == group) && k.namespace =~ search
145
+ end
146
+
147
+ display_klasses(false, klasses)
148
+ end
149
+
150
+ private
151
+
152
+ def thor_root
153
+ Thor::Util.thor_root
154
+ end
155
+
156
+ def thor_yaml
157
+ @thor_yaml ||= begin
158
+ yaml_file = File.join(thor_root, "thor.yml")
159
+ yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file)
160
+ yaml || {}
161
+ end
162
+ end
163
+
164
+ # Save the yaml file. If none exists in thor root, creates one.
165
+ #
166
+ def save_yaml(yaml)
167
+ yaml_file = File.join(thor_root, "thor.yml")
168
+
169
+ unless File.exists?(yaml_file)
170
+ FileUtils.mkdir_p(thor_root)
171
+ yaml_file = File.join(thor_root, "thor.yml")
172
+ FileUtils.touch(yaml_file)
173
+ end
174
+
175
+ File.open(yaml_file, "w") { |f| f.puts yaml.to_yaml }
176
+ end
177
+
178
+ # Load the thorfiles. If relevant_to is supplied, looks for specific files
179
+ # in the thor_root instead of loading them all.
180
+ #
181
+ # By default, it also traverses the current path until find Thor files, as
182
+ # described in thorfiles. This look up can be skipped by suppliying
183
+ # skip_lookup true.
184
+ #
185
+ def initialize_thorfiles(relevant_to=nil, skip_lookup=false)
186
+ thorfiles(relevant_to, skip_lookup).each do |f|
187
+ Thor::Util.load_thorfile(f) unless Thor::Base.subclass_files.keys.include?(File.expand_path(f))
188
+ end
189
+ end
190
+
191
+ # Finds Thorfiles by traversing from your current directory down to the root
192
+ # directory of your system. If at any time we find a Thor file, we stop.
193
+ #
194
+ # We also ensure that system-wide Thorfiles are loaded first, so local
195
+ # Thorfiles can override them.
196
+ #
197
+ # ==== Example
198
+ #
199
+ # If we start at /Users/wycats/dev/thor ...
200
+ #
201
+ # 1. /Users/wycats/dev/thor
202
+ # 2. /Users/wycats/dev
203
+ # 3. /Users/wycats <-- we find a Thorfile here, so we stop
204
+ #
205
+ # Suppose we start at c:\Documents and Settings\james\dev\thor ...
206
+ #
207
+ # 1. c:\Documents and Settings\james\dev\thor
208
+ # 2. c:\Documents and Settings\james\dev
209
+ # 3. c:\Documents and Settings\james
210
+ # 4. c:\Documents and Settings
211
+ # 5. c:\ <-- no Thorfiles found!
212
+ #
213
+ def thorfiles(relevant_to=nil, skip_lookup=false)
214
+ # Deal with deprecated thor when :namespaces: is available as constants
215
+ save_yaml(thor_yaml) if Thor::Util.convert_constants_to_namespaces(thor_yaml)
216
+
217
+ thorfiles = []
218
+
219
+ unless skip_lookup
220
+ Pathname.pwd.ascend do |path|
221
+ thorfiles = Thor::Util.globs_for(path).map { |g| Dir[g] }.flatten
222
+ break unless thorfiles.empty?
223
+ end
224
+ end
225
+
226
+ files = (relevant_to ? thorfiles_relevant_to(relevant_to) : Thor::Util.thor_root_glob)
227
+ files += thorfiles
228
+ files -= ["#{thor_root}/thor.yml"]
229
+
230
+ files.map! do |file|
231
+ File.directory?(file) ? File.join(file, "main.thor") : file
232
+ end
233
+ end
234
+
235
+ # Load thorfiles relevant to the given method. If you provide "foo:bar" it
236
+ # will load all thor files in the thor.yaml that has "foo" e "foo:bar"
237
+ # namespaces registered.
238
+ #
239
+ def thorfiles_relevant_to(meth)
240
+ lookup = [ meth, meth.split(":")[0...-1].join(":") ]
241
+
242
+ files = thor_yaml.select do |k, v|
243
+ v[:namespaces] && !(v[:namespaces] & lookup).empty?
244
+ end
245
+
246
+ files.map! { |k, v| File.join(thor_root, "#{v[:filename]}") }
247
+ files
248
+ end
249
+
250
+ # Display information about the given klasses. If with_module is given,
251
+ # it shows a table with information extracted from the yaml file.
252
+ #
253
+ def display_klasses(with_modules=false, klasses=Thor.subclasses)
254
+ klasses -= [Thor, Thor::Runner] unless with_modules
255
+ raise Error, "No Thor tasks available" if klasses.empty?
256
+
257
+ if with_modules && !thor_yaml.empty?
258
+ info = []
259
+ labels = ["Modules", "Namespaces"]
260
+
261
+ info << labels
262
+ info << [ "-" * labels[0].size, "-" * labels[1].size ]
263
+
264
+ thor_yaml.each do |name, hash|
265
+ info << [ name, hash[:namespaces].join(", ") ]
266
+ end
267
+
268
+ print_table info
269
+ say ""
270
+ end
271
+
272
+ unless klasses.empty?
273
+ klasses.each { |k| display_tasks(k) }
274
+ else
275
+ say "\033[1;34mNo Thor tasks available\033[0m"
276
+ end
277
+ end
278
+
279
+ # Display tasks from the given Thor class.
280
+ #
281
+ def display_tasks(klass)
282
+ unless klass.tasks.empty?
283
+ base = klass.namespace
284
+
285
+ if base == "default"
286
+ say "\033[1;35m#{base}\033[0m"
287
+ else
288
+ say "\033[1;34m#{base}\033[0m"
289
+ end
290
+ say "-" * base.length
291
+
292
+ klass.help(shell, :short => true, :namespace => true)
293
+ say
294
+ end
295
+ end
296
+ end