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.
@@ -0,0 +1,64 @@
1
+ class Thor
2
+ # This class is based on the Ruby 1.9 ordered hashes.
3
+ # It keeps the semantics and most of the efficiency of normal hashes
4
+ # while also keeping track of the order in which elements were set.
5
+ class OrderedHash
6
+ Node = Struct.new(:key, :value, :next, :prev)
7
+ include Enumerable
8
+
9
+ def initialize
10
+ @hash = {}
11
+ end
12
+
13
+ def initialize_copy(other)
14
+ @hash = other.instance_variable_get('@hash').clone
15
+ end
16
+
17
+ def [](key)
18
+ @hash[key] && @hash[key].value
19
+ end
20
+
21
+ def []=(key, value)
22
+ node = Node.new(key, value)
23
+
24
+ if old = @hash[key]
25
+ if old.prev
26
+ old.prev.next = old.next
27
+ else # old is @first and @last
28
+ @first = @last = nil
29
+ end
30
+ end
31
+
32
+ if @first.nil?
33
+ @first = @last = node
34
+ else
35
+ node.prev = @last
36
+ @last.next = node
37
+ @last = node
38
+ end
39
+
40
+ @hash[key] = node
41
+ value
42
+ end
43
+
44
+ def each
45
+ return unless @first
46
+ yield [@first.key, @first.value]
47
+ node = @first
48
+ yield [node.key, node.value] while node = node.next
49
+ self
50
+ end
51
+
52
+ def values
53
+ self.map { |k, v| v }
54
+ end
55
+
56
+ def +(other)
57
+ new = clone
58
+ other.each do |key, value|
59
+ new[key] = value unless self[key]
60
+ end
61
+ new
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,260 @@
1
+ require 'thor'
2
+ require "thor/util"
3
+ require "open-uri"
4
+ require "fileutils"
5
+ require "yaml"
6
+ require "digest/md5"
7
+ require "readline"
8
+
9
+ class Thor::Runner < Thor
10
+
11
+ def self.globs_for(path)
12
+ ["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"]
13
+ end
14
+
15
+ map "-T" => :list, "-i" => :install, "-u" => :update
16
+
17
+ desc "install NAME", "install a Thor file into your system tasks, optionally named for future updates"
18
+ method_options :as => :optional, :relative => :boolean
19
+ def install(name)
20
+ initialize_thorfiles
21
+ begin
22
+ contents = open(name).read
23
+ rescue OpenURI::HTTPError
24
+ raise Error, "Error opening URI `#{name}'"
25
+ rescue Errno::ENOENT
26
+ raise Error, "Error opening file `#{name}'"
27
+ end
28
+
29
+ is_uri = File.exist?(name) ? false : true
30
+
31
+ puts "Your Thorfile contains: "
32
+ puts contents
33
+ print "Do you wish to continue [y/N]? "
34
+ response = Readline.readline
35
+
36
+ return false unless response =~ /^\s*y/i
37
+
38
+ constants = Thor::Util.constants_in_contents(contents)
39
+
40
+ # name = name =~ /\.thor$/ || is_uri ? name : "#{name}.thor"
41
+
42
+ as = options["as"] || begin
43
+ first_line = contents.split("\n")[0]
44
+ (match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil
45
+ end
46
+
47
+ if !as
48
+ print "Please specify a name for #{name} in the system repository [#{name}]: "
49
+ as = Readline.readline
50
+ as = name if as.empty?
51
+ end
52
+
53
+ FileUtils.mkdir_p thor_root
54
+
55
+ yaml_file = File.join(thor_root, "thor.yml")
56
+ FileUtils.touch(yaml_file)
57
+ yaml = thor_yaml
58
+
59
+ location = (options[:relative] || is_uri) ? name : File.expand_path(name)
60
+ yaml[as] = {:filename => Digest::MD5.hexdigest(name + as), :location => location, :constants => constants}
61
+
62
+ save_yaml(yaml)
63
+
64
+ puts "Storing thor file in your system repository"
65
+
66
+ File.open(File.join(thor_root, yaml[as][:filename]), "w") do |file|
67
+ file.puts contents
68
+ end
69
+
70
+ yaml[as][:filename] # Indicate sucess
71
+ end
72
+
73
+ desc "uninstall NAME", "uninstall a named Thor module"
74
+ def uninstall(name)
75
+ yaml = thor_yaml
76
+ raise Error, "Can't find module `#{name}'" unless yaml[name]
77
+
78
+ puts "Uninstalling #{name}."
79
+
80
+ file = File.join(thor_root, "#{yaml[name][:filename]}")
81
+ File.delete(file)
82
+ yaml.delete(name)
83
+ save_yaml(yaml)
84
+
85
+ puts "Done."
86
+ end
87
+
88
+ desc "update NAME", "update a Thor file from its original location"
89
+ def update(name)
90
+ yaml = thor_yaml
91
+ raise Error, "Can't find module `#{name}'" if !yaml[name] || !yaml[name][:location]
92
+
93
+ puts "Updating `#{name}' from #{yaml[name][:location]}"
94
+ old_filename = yaml[name][:filename]
95
+ options["as"] = name
96
+ filename = install(yaml[name][:location])
97
+ unless filename == old_filename
98
+ File.delete(File.join(thor_root, old_filename))
99
+ end
100
+ end
101
+
102
+ desc "installed", "list the installed Thor modules and tasks (--internal means list the built-in tasks as well)"
103
+ method_options :internal => :boolean
104
+ def installed
105
+ thor_root_glob.each do |f|
106
+ next if f =~ /thor\.yml$/
107
+ load_thorfile f unless Thor.subclass_files.keys.include?(File.expand_path(f))
108
+ end
109
+
110
+ klasses = Thor.subclasses
111
+ klasses -= [Thor, Thor::Runner] unless options['internal']
112
+ display_klasses(true, klasses)
113
+ end
114
+
115
+ desc "list [SEARCH]", "list the available thor tasks (--substring means SEARCH can be anywhere in the module)"
116
+ method_options :substring => :boolean
117
+ def list(search = "")
118
+ initialize_thorfiles
119
+ search = ".*#{search}" if options["substring"]
120
+ search = /^#{search}.*/i
121
+
122
+ display_klasses(false, Thor.subclasses.select {|k|
123
+ Thor::Util.constant_to_thor_path(k.name) =~ search})
124
+ end
125
+
126
+ # Override Thor#help so we can give info about not-yet-loaded tasks
127
+ def help(task = nil)
128
+ initialize_thorfiles(task) if task && task.include?(?:)
129
+ super
130
+ end
131
+
132
+ def method_missing(meth, *args)
133
+ meth = meth.to_s
134
+ super(meth.to_sym, *args) unless meth.include? ?:
135
+
136
+ initialize_thorfiles(meth)
137
+ task = Thor[meth]
138
+ task.parse task.klass.new, ARGV[1..-1]
139
+ end
140
+
141
+ def self.thor_root
142
+ File.join(ENV["HOME"] || ENV["APPDATA"], ".thor")
143
+ end
144
+
145
+ def self.thor_root_glob
146
+ # On Windows thor_root will be something like this:
147
+ #
148
+ # C:\Documents and Settings\james\.thor
149
+ #
150
+ # If we don't #gsub the \ character, Dir.glob will fail.
151
+ Dir["#{thor_root.gsub(/\\/, '/')}/**/*"]
152
+ end
153
+
154
+ private
155
+ def thor_root
156
+ self.class.thor_root
157
+ end
158
+
159
+ def thor_root_glob
160
+ self.class.thor_root_glob
161
+ end
162
+
163
+ def thor_yaml
164
+ yaml_file = File.join(thor_root, "thor.yml")
165
+ yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file)
166
+ yaml || {}
167
+ end
168
+
169
+ def save_yaml(yaml)
170
+ yaml_file = File.join(thor_root, "thor.yml")
171
+ File.open(yaml_file, "w") {|f| f.puts yaml.to_yaml }
172
+ end
173
+
174
+ def display_klasses(with_modules = false, klasses = Thor.subclasses)
175
+ klasses -= [Thor, Thor::Runner] unless with_modules
176
+ raise Error, "No Thor tasks available" if klasses.empty?
177
+
178
+ if with_modules && !(yaml = thor_yaml).empty?
179
+ max_name = yaml.max {|(xk,xv),(yk,yv)| xk.size <=> yk.size }.first.size
180
+ modules_label = "Modules"
181
+ namespaces_label = "Namespaces"
182
+ column_width = [max_name + 4, modules_label.size + 1].max
183
+
184
+ print "%-#{column_width}s" % modules_label
185
+ puts namespaces_label
186
+ print "%-#{column_width}s" % ("-" * modules_label.size)
187
+ puts "-" * namespaces_label.size
188
+
189
+ yaml.each do |name, info|
190
+ print "%-#{column_width}s" % name
191
+ puts info[:constants].map {|c| Thor::Util.constant_to_thor_path(c)}.join(", ")
192
+ end
193
+
194
+ puts
195
+ end
196
+
197
+ puts "Tasks"
198
+ puts "-----"
199
+
200
+ # Calculate the largest base class name
201
+ max_base = klasses.max do |x,y|
202
+ Thor::Util.constant_to_thor_path(x.name).size <=> Thor::Util.constant_to_thor_path(y.name).size
203
+ end.name.size
204
+
205
+ # Calculate the size of the largest option description
206
+ max_left_item = klasses.max do |x,y|
207
+ (x.maxima.usage + x.maxima.opt).to_i <=> (y.maxima.usage + y.maxima.opt).to_i
208
+ end
209
+
210
+ max_left = max_left_item.maxima.usage + max_left_item.maxima.opt
211
+
212
+ klasses.each {|k| display_tasks(k, max_base, max_left)}
213
+ end
214
+
215
+ def display_tasks(klass, max_base, max_left)
216
+ base = Thor::Util.constant_to_thor_path(klass.name)
217
+ klass.tasks.each true do |name, task|
218
+ format_string = "%-#{max_left + max_base + 5}s"
219
+ print format_string % task.formatted_usage(true)
220
+ puts task.description
221
+ end
222
+ end
223
+
224
+ def initialize_thorfiles(relevant_to = nil)
225
+ thorfiles(relevant_to).each {|f| load_thorfile f unless Thor.subclass_files.keys.include?(File.expand_path(f))}
226
+ end
227
+
228
+ def load_thorfile(path)
229
+ begin
230
+ load path
231
+ rescue Object => e
232
+ $stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}"
233
+ end
234
+ end
235
+
236
+ def thorfiles(relevant_to = nil)
237
+ path = Dir.pwd
238
+ thorfiles = []
239
+
240
+ # Look for Thorfile or *.thor in the current directory or a parent directory, until the root
241
+ while thorfiles.empty?
242
+ thorfiles = Thor::Runner.globs_for(path).map {|g| Dir[g]}.flatten
243
+ path = File.dirname(path)
244
+ break if path == "/"
245
+ end
246
+
247
+ # We want to load system-wide Thorfiles first
248
+ # so the local Thorfiles will override them.
249
+ (relevant_to ? thorfiles_relevant_to(relevant_to) :
250
+ thor_root_glob) + thorfiles - ["#{thor_root}/thor.yml"]
251
+ end
252
+
253
+ def thorfiles_relevant_to(meth)
254
+ klass_str = Thor::Util.to_constant(meth.split(":")[0...-1].join(":"))
255
+ thor_yaml.select do |k, v|
256
+ v[:constants] && v[:constants].include?(klass_str)
257
+ end.map { |k, v| File.join(thor_root, "#{v[:filename]}") }
258
+ end
259
+
260
+ end
@@ -0,0 +1,68 @@
1
+ require 'thor/error'
2
+ require 'thor/util'
3
+
4
+ class Thor
5
+ class Task < Struct.new(:meth, :description, :usage, :opts, :klass)
6
+ def self.dynamic(meth, klass)
7
+ new(meth, "A dynamically-generated task", meth.to_s, nil, klass)
8
+ end
9
+
10
+ def parse(obj, args)
11
+ list, hash = parse_args(args)
12
+ obj.options = hash
13
+ run(obj, *list)
14
+ end
15
+
16
+ def run(obj, *params)
17
+ raise NoMethodError, "the `#{meth}' task of #{obj.class} is private" if
18
+ (obj.private_methods + obj.protected_methods).include?(meth)
19
+
20
+ obj.send(meth, *params)
21
+ rescue ArgumentError => e
22
+ # backtrace sans anything in this file
23
+ backtrace = e.backtrace.reject {|frame| frame =~ /^#{Regexp.escape(__FILE__)}/}
24
+ # and sans anything that got us here
25
+ backtrace -= caller
26
+ raise e unless backtrace.empty?
27
+
28
+ # okay, they really did call it wrong
29
+ raise Error, "`#{meth}' was called incorrectly. Call as `#{formatted_usage}'"
30
+ rescue NoMethodError => e
31
+ begin
32
+ raise e unless e.message =~ /^undefined method `#{meth}' for #{Regexp.escape(obj.inspect)}$/
33
+ rescue
34
+ raise e
35
+ end
36
+ raise Error, "The #{namespace false} namespace doesn't have a `#{meth}' task"
37
+ end
38
+
39
+ def namespace(remove_default = true)
40
+ Thor::Util.constant_to_thor_path(klass, remove_default)
41
+ end
42
+
43
+ def with_klass(klass)
44
+ new = self.dup
45
+ new.klass = klass
46
+ new
47
+ end
48
+
49
+ def opts
50
+ return super unless super.kind_of? Hash
51
+ self.opts = Options.new(super)
52
+ end
53
+
54
+ def formatted_usage(namespace = false)
55
+ (namespace ? self.namespace + ':' : '') + usage +
56
+ (opts ? " " + opts.formatted_usage : "")
57
+ end
58
+
59
+ protected
60
+
61
+ def parse_args(args)
62
+ return [args, {}] unless opts
63
+ hash = opts.parse(args)
64
+ list = opts.non_opts
65
+ [list, hash]
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,22 @@
1
+ require 'thor/ordered_hash'
2
+ require 'thor/task'
3
+
4
+ class Thor::TaskHash < Thor::OrderedHash
5
+ def initialize(klass)
6
+ super()
7
+ @klass = klass
8
+ end
9
+
10
+ def each(local = false, &block)
11
+ super() { |k, t| yield k, t.with_klass(@klass) }
12
+ @klass.superclass.tasks.each { |k, t| yield k, t.with_klass(@klass) } unless local || @klass == Thor
13
+ end
14
+
15
+ def [](name)
16
+ if task = super(name) || (@klass == Thor && @klass.superclass.tasks[name])
17
+ return task.with_klass(@klass)
18
+ end
19
+
20
+ Thor::Task.dynamic(name, @klass)
21
+ end
22
+ end
@@ -2,29 +2,28 @@ require "thor"
2
2
  require "fileutils"
3
3
 
4
4
  class Thor
5
- def self.package_task
6
- self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
7
- desc "package", "package up the gem"
8
- def package
9
- FileUtils.mkdir_p(File.join(Dir.pwd, "pkg"))
10
- Gem::Builder.new(SPEC).build
11
- FileUtils.mv(SPEC.file_name, File.join(Dir.pwd, "pkg", SPEC.file_name))
12
- end
13
- RUBY
5
+ def self.package_task(spec)
6
+ desc "package", "package up the gem"
7
+ define_method :package do
8
+ FileUtils.mkdir_p(File.join(Dir.pwd, "pkg"))
9
+ Gem::Builder.new(spec).build
10
+ FileUtils.mv(spec.file_name, File.join(Dir.pwd, "pkg", spec.file_name))
11
+ end
14
12
  end
15
13
 
16
- def self.install_task
17
- package_task
14
+ def self.install_task(spec)
15
+ package_task spec
16
+
17
+ null, sudo, gem = RUBY_PLATFORM =~ /w(in)?32$/ ? ['NUL', '', 'gem.bat'] :
18
+ ['/dev/null', 'sudo', 'gem']
18
19
 
19
- self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
20
- desc "install", "install the gem"
21
- def install
22
- old_stderr, $stderr = $stderr.dup, File.open("/dev/null", "w")
23
- package
24
- $stderr = old_stderr
25
- system %{sudo gem install pkg/#{GEM}-#{GEM_VERSION} --no-rdoc --no-ri --no-update-sources}
26
- end
27
- RUBY
20
+ desc "install", "install the gem"
21
+ define_method :install do
22
+ old_stderr, $stderr = $stderr.dup, File.open(null, "w")
23
+ package
24
+ $stderr = old_stderr
25
+ system %{#{sudo} #{gem} install pkg/#{spec.name}-#{spec.version} --no-rdoc --no-ri --no-update-sources}
26
+ end
28
27
  end
29
28
 
30
29
  def self.spec_task(file_list, opts = {})
@@ -42,32 +41,34 @@ class Thor
42
41
  FileUtils.rm_rf(File.join(Dir.pwd, rcov_dir))
43
42
  end
44
43
 
45
- self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
46
- desc("#{name}", "spec task")
47
- def #{name}
48
- cmd = "ruby "
49
- if #{rcov.inspect}
50
- cmd << "-S rcov -o #{rcov_dir} #{rcov_opts.inspect[1...-1]} "
51
- end
52
- cmd << `which spec`.chomp
53
- cmd << " -- " if #{rcov.inspect}
54
- cmd << " "
55
- cmd << #{file_list.inspect}
56
- cmd << " "
57
- cmd << #{options.inspect}
58
- puts cmd if #{verbose.inspect}
59
- system(cmd)
44
+ desc(name, "spec task")
45
+ define_method(name) do
46
+ cmd = "ruby "
47
+ if rcov
48
+ cmd << "-S rcov -o #{rcov_dir} #{rcov_opts} "
60
49
  end
61
- RUBY
50
+ cmd << `which spec`.chomp
51
+ cmd << " -- " if rcov
52
+ cmd << " "
53
+ cmd << file_list
54
+ cmd << " "
55
+ cmd << options
56
+ puts cmd if verbose
57
+ system(cmd)
58
+ exit($?.exitstatus)
59
+ end
62
60
  end
63
61
 
64
62
  private
65
63
  def self.convert_task_options(opts)
66
64
  opts.map do |key, value|
67
- if value == true
65
+ case value
66
+ when true
68
67
  "--#{key}"
69
- elsif value.is_a?(Array)
68
+ when Array
70
69
  value.map {|v| "--#{key} #{v.inspect}"}.join(" ")
70
+ when nil, false
71
+ ""
71
72
  else
72
73
  "--#{key} #{value.inspect}"
73
74
  end