mislav-thor 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.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Yehuda Katz
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,58 @@
1
+ thor
2
+ ====
3
+
4
+ Map options to a class. Simply create a class with the appropriate annotations, and have options automatically map
5
+ to functions and parameters.
6
+
7
+ Example:
8
+
9
+ class MyApp < Thor # [1]
10
+ map "-L" => :list # [2]
11
+
12
+ desc "install APP_NAME", "install one of the available apps" # [3]
13
+ method_options :force => :boolean # [4]
14
+ def install(name, opts)
15
+ ... code ...
16
+ if opts[:force]
17
+ # do something
18
+ end
19
+ end
20
+
21
+ desc "list [SEARCH]", "list all of the available apps, limited by SEARCH"
22
+ def list(search = "")
23
+ # list everything
24
+ end
25
+
26
+ end
27
+
28
+ MyApp.start
29
+
30
+ Thor automatically maps commands as follows:
31
+
32
+ app install name --force
33
+
34
+ That gets converted to:
35
+
36
+ MyApp.new.install("name", :force => true)
37
+
38
+ 1. Inherit from Thor to turn a class into an option mapper
39
+ 2. Map additional non-valid identifiers to specific methods. In this case,
40
+ convert -L to :list
41
+ 3. Describe the method immediately below. The first parameter is the usage information,
42
+ and the second parameter is the description.
43
+ 4. Provide any additional options. These will be marshaled from -- and - params.
44
+ In this case, a --force and a -f option is added.
45
+
46
+ Types for `method_options`
47
+ --------------------------
48
+
49
+ <dl>
50
+ <dt><code>:boolean</code></dt>
51
+ <dd>true if the option is passed</dd>
52
+ <dt><code>:required</code></dt>
53
+ <dd>the value for this option MUST be provided</dd>
54
+ <dt><code>:optional</code></dt>
55
+ <dd>the value for this option MAY be provided</dd>
56
+ <dt>a String</dt>
57
+ <dd>same as <code>:optional</code>; fall back to the given string as default value</dd>
58
+ </dl>
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ task :default => :install
2
+
3
+ desc "install the gem locally"
4
+ task :install do
5
+ sh %{ruby #{File.dirname(__FILE__)}/bin/thor :install}
6
+ end
data/bin/rake2thor ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'ruby2ruby'
5
+ require 'rake'
6
+
7
+ input = ARGV[0] || 'Rakefile'
8
+ output = ARGV[1] || 'Thorfile'
9
+
10
+ $requires = []
11
+
12
+ module Kernel
13
+ def require_with_record(file)
14
+ $requires << file if caller[1] =~ /rake2thor:/
15
+ require_without_record file
16
+ end
17
+ alias_method :require_without_record, :require
18
+ alias_method :require, :require_with_record
19
+ end
20
+
21
+ load input
22
+
23
+ @private_methods = []
24
+
25
+ def file_task_name(name)
26
+ "compile_" + name.gsub('/', '_slash_').gsub('.', '_dot_').gsub(/\W/, '_')
27
+ end
28
+
29
+ def method_for_task(task)
30
+ file_task = task.is_a?(Rake::FileTask)
31
+ comment = task.instance_variable_get('@comment')
32
+ prereqs = task.instance_variable_get('@prerequisites').select(&Rake::Task.method(:task_defined?))
33
+ actions = task.instance_variable_get('@actions')
34
+ name = task.name.gsub(/^([^:]+:)+/, '')
35
+ name = file_task_name(name) if file_task
36
+ meth = ''
37
+
38
+ meth << "desc #{name.inspect}, #{comment.inspect}\n" if comment
39
+ meth << "def #{name}\n"
40
+
41
+ meth << prereqs.map do |pre|
42
+ pre = pre.to_s
43
+ pre = file_task_name(pre) if Rake::Task[pre].is_a?(Rake::FileTask)
44
+ ' ' + pre
45
+ end.join("\n")
46
+
47
+ meth << "\n\n" unless prereqs.empty? || actions.empty?
48
+
49
+ meth << actions.map do |act|
50
+ act = act.to_ruby
51
+ unless act.gsub!(/^proc \{ \|(\w+)\|\n/,
52
+ " \\1 = Struct.new(:name).new(#{name.inspect}) # A crude mock Rake::Task object\n")
53
+ act.gsub!(/^proc \{\n/, '')
54
+ end
55
+ act.gsub(/\n\}$/, '')
56
+ end.join("\n")
57
+
58
+ meth << "\nend"
59
+
60
+ if file_task
61
+ @private_methods << meth
62
+ return
63
+ end
64
+
65
+ meth
66
+ end
67
+
68
+ body = Rake::Task.tasks.map(&method(:method_for_task)).compact.map { |meth| meth.gsub(/^/, ' ') }.join("\n\n")
69
+
70
+ unless @private_methods.empty?
71
+ body << "\n\n private\n\n"
72
+ body << @private_methods.map { |meth| meth.gsub(/^/, ' ') }.join("\n\n")
73
+ end
74
+
75
+ requires = $requires.map { |r| "require #{r.inspect}" }.join("\n")
76
+
77
+ File.open(output, 'w') { |f| f.write(<<END.lstrip) }
78
+ #{requires}
79
+
80
+ class Default < Thor
81
+ #{body}
82
+ end
83
+ END
data/bin/thor ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- mode: ruby -*-
3
+
4
+ require File.dirname(__FILE__) + "/../lib/thor"
5
+ require 'thor/runner'
6
+
7
+ Thor::Runner.start
data/lib/thor/error.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Thor
2
+ class Error < Exception; end
3
+ end
@@ -0,0 +1,156 @@
1
+ # This is a modified version of Daniel Berger's Getopt::ong class,
2
+ # licensed under Ruby's license.
3
+
4
+ require 'set'
5
+
6
+ class Thor
7
+ class Options
8
+ class Error < StandardError; end
9
+
10
+ LONG_RE = /^(--\w+[-\w+]*)$/
11
+ SHORT_RE = /^(-\w)$/
12
+ LONG_EQ_RE = /^(--\w+[-\w+]*)=(.*?)$|(-\w?)=(.*?)$/
13
+ SHORT_SQ_RE = /^-(\w\S+?)$/ # Allow either -x -v or -xv style for single char args
14
+
15
+ attr_accessor :args
16
+
17
+ def initialize(args, switches)
18
+ @args = args
19
+ @defaults = {}
20
+
21
+ switches = switches.map do |names, type|
22
+ case type
23
+ when TrueClass then type = :boolean
24
+ when String
25
+ @defaults[names] = type
26
+ type = :optional
27
+ end
28
+
29
+ if names.is_a?(String)
30
+ if names =~ LONG_RE
31
+ names = [names, "-" + names[2].chr]
32
+ else
33
+ names = [names]
34
+ end
35
+ end
36
+
37
+ [names, type]
38
+ end
39
+
40
+ @valid = switches.map {|s| s.first}.flatten.to_set
41
+ @types = switches.inject({}) do |h, (forms,v)|
42
+ forms.each {|f| h[f] ||= v}
43
+ h
44
+ end
45
+ @syns = switches.inject({}) do |h, (forms,_)|
46
+ forms.each {|f| h[f] ||= forms}
47
+ h
48
+ end
49
+ end
50
+
51
+ def skip_non_opts
52
+ non_opts = []
53
+ non_opts << pop until looking_at_opt? || @args.empty?
54
+ non_opts
55
+ end
56
+
57
+ # Takes an array of switches. Each array consists of up to three
58
+ # elements that indicate the name and type of switch. Returns a hash
59
+ # containing each switch name, minus the '-', as a key. The value
60
+ # for each key depends on the type of switch and/or the value provided
61
+ # by the user.
62
+ #
63
+ # The long switch _must_ be provided. The short switch defaults to the
64
+ # first letter of the short switch. The default type is :boolean.
65
+ #
66
+ # Example:
67
+ #
68
+ # opts = Thor::Options.new(args,
69
+ # "--debug" => true,
70
+ # ["--verbose", "-v"] => true,
71
+ # ["--level", "-l"] => :numeric
72
+ # ).getopts
73
+ #
74
+ def getopts(check_required = true)
75
+ hash = @defaults.dup
76
+
77
+ while looking_at_opt?
78
+ case pop
79
+ when SHORT_SQ_RE
80
+ push(*$1.split("").map {|s| s = "-#{s}"})
81
+ next
82
+ when LONG_EQ_RE
83
+ push($1, $2)
84
+ next
85
+ when LONG_RE, SHORT_RE
86
+ switch = $1
87
+ end
88
+
89
+ case @types[switch]
90
+ when :required
91
+ raise Error, "no value provided for required argument '#{switch}'" if peek.nil?
92
+ raise Error, "cannot pass switch '#{peek}' as an argument" if @valid.include?(peek)
93
+ hash[switch] = pop
94
+ when :boolean
95
+ hash[switch] = true
96
+ when :optional
97
+ # For optional arguments, there may be an argument. If so, it
98
+ # cannot be another switch. If not, it is set to true.
99
+ hash[switch] = @valid.include?(peek) || peek.nil? || pop
100
+ end
101
+ end
102
+
103
+ hash = normalize_hash hash
104
+ check_required_args hash if check_required
105
+ hash
106
+ end
107
+
108
+ def check_required_args(hash)
109
+ @types.select {|k,v| v == :required}.map {|k,v| @syns[k]}.uniq.each do |syns|
110
+ unless syns.map {|s| s.gsub(/^-+/, '')}.any? {|s| hash[s]}
111
+ raise Error, "no value provided for required argument '#{syns.first}'"
112
+ end
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def peek
119
+ @args.first
120
+ end
121
+
122
+ def pop
123
+ arg = peek
124
+ @args = @args[1..-1] || []
125
+ arg
126
+ end
127
+
128
+ def push(*args)
129
+ @args = args + @args
130
+ end
131
+
132
+ def looking_at_opt?
133
+ case peek
134
+ when LONG_RE, SHORT_RE, LONG_EQ_RE
135
+ @valid.include? $1
136
+ when SHORT_SQ_RE
137
+ $1.split("").any? {|f| @valid.include? "-#{f}"}
138
+ end
139
+ end
140
+
141
+ # Set synonymous switches to the same value, e.g. if -t is a synonym
142
+ # for --test, and the user passes "--test", then set "-t" to the same
143
+ # value that "--test" was set to.
144
+ #
145
+ # This allows users to refer to the long or short switch and get
146
+ # the same value
147
+ def normalize_hash(hash)
148
+ hash.map do |switch, val|
149
+ @syns[switch].map {|key| [key, val]}
150
+ end.inject([]) {|a, v| a + v}.map do |key, value|
151
+ [key.sub(/^-+/, ''), value]
152
+ end.inject({}) {|h, (k,v)| h[k] = v; h[k.to_sym] = v; h}
153
+ end
154
+
155
+ end
156
+ end
@@ -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,247 @@
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
+ Dir["#{thor_root}/**/*"].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
+ private
146
+ def thor_root
147
+ self.class.thor_root
148
+ end
149
+
150
+ def thor_yaml
151
+ yaml_file = File.join(thor_root, "thor.yml")
152
+ yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file)
153
+ yaml || {}
154
+ end
155
+
156
+ def save_yaml(yaml)
157
+ yaml_file = File.join(thor_root, "thor.yml")
158
+ File.open(yaml_file, "w") {|f| f.puts yaml.to_yaml }
159
+ end
160
+
161
+ def display_klasses(with_modules = false, klasses = Thor.subclasses)
162
+ klasses -= [Thor, Thor::Runner] unless with_modules
163
+ raise Error, "No Thor tasks available" if klasses.empty?
164
+
165
+ if with_modules && !(yaml = thor_yaml).empty?
166
+ max_name = yaml.max {|(xk,xv),(yk,yv)| xk.size <=> yk.size }.first.size
167
+ modules_label = "Modules"
168
+ namespaces_label = "Namespaces"
169
+ column_width = [max_name + 4, modules_label.size + 1].max
170
+
171
+ print "%-#{column_width}s" % modules_label
172
+ puts namespaces_label
173
+ print "%-#{column_width}s" % ("-" * modules_label.size)
174
+ puts "-" * namespaces_label.size
175
+
176
+ yaml.each do |name, info|
177
+ print "%-#{column_width}s" % name
178
+ puts info[:constants].map {|c| Thor::Util.constant_to_thor_path(c)}.join(", ")
179
+ end
180
+
181
+ puts
182
+ end
183
+
184
+ puts "Tasks"
185
+ puts "-----"
186
+
187
+ # Calculate the largest base class name
188
+ max_base = klasses.max do |x,y|
189
+ Thor::Util.constant_to_thor_path(x.name).size <=> Thor::Util.constant_to_thor_path(y.name).size
190
+ end.name.size
191
+
192
+ # Calculate the size of the largest option description
193
+ max_left_item = klasses.max do |x,y|
194
+ (x.maxima.usage + x.maxima.opt).to_i <=> (y.maxima.usage + y.maxima.opt).to_i
195
+ end
196
+
197
+ max_left = max_left_item.maxima.usage + max_left_item.maxima.opt
198
+
199
+ klasses.each {|k| display_tasks(k, max_base, max_left)}
200
+ end
201
+
202
+ def display_tasks(klass, max_base, max_left)
203
+ base = Thor::Util.constant_to_thor_path(klass.name)
204
+ klass.tasks.each true do |name, task|
205
+ format_string = "%-#{max_left + max_base + 5}s"
206
+ print format_string % task.formatted_usage(true)
207
+ puts task.description
208
+ end
209
+ end
210
+
211
+ def initialize_thorfiles(relevant_to = nil)
212
+ thorfiles(relevant_to).each {|f| load_thorfile f unless Thor.subclass_files.keys.include?(File.expand_path(f))}
213
+ end
214
+
215
+ def load_thorfile(path)
216
+ begin
217
+ load path
218
+ rescue Object => e
219
+ $stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}"
220
+ end
221
+ end
222
+
223
+ def thorfiles(relevant_to = nil)
224
+ path = Dir.pwd
225
+ thorfiles = []
226
+
227
+ # Look for Thorfile or *.thor in the current directory or a parent directory, until the root
228
+ while thorfiles.empty?
229
+ thorfiles = Thor::Runner.globs_for(path).map {|g| Dir[g]}.flatten
230
+ path = File.dirname(path)
231
+ break if path == "/"
232
+ end
233
+
234
+ # We want to load system-wide Thorfiles first
235
+ # so the local Thorfiles will override them.
236
+ (relevant_to ? thorfiles_relevant_to(relevant_to) :
237
+ Dir["#{thor_root}/**/*"]) + thorfiles - ["#{thor_root}/thor.yml"]
238
+ end
239
+
240
+ def thorfiles_relevant_to(meth)
241
+ klass_str = Thor::Util.to_constant(meth.split(":")[0...-1].join(":"))
242
+ thor_yaml.select do |k, v|
243
+ v[:constants] && v[:constants].include?(klass_str)
244
+ end.map { |k, v| File.join(thor_root, "#{v[:filename]}") }
245
+ end
246
+
247
+ end
data/lib/thor/task.rb ADDED
@@ -0,0 +1,80 @@
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 formatted_opts
50
+ return "" if opts.nil?
51
+ opts.map do |opt, val|
52
+ if val == true || val == :boolean
53
+ "[#{opt}]"
54
+ elsif val == :required
55
+ opt + "=" + opt.gsub(/\-/, "").upcase
56
+ else
57
+ sample = val == :optional ? opt.gsub(/\-/, "").upcase : val
58
+ "[" + opt + "=" + sample + "]"
59
+ end
60
+ end.join(" ")
61
+ end
62
+
63
+ def formatted_usage(namespace = false)
64
+ (namespace ? self.namespace + ':' : '') + usage +
65
+ (opts ? " " + formatted_opts : "")
66
+ end
67
+
68
+ protected
69
+
70
+ def parse_args(args)
71
+ return [args, {}] unless opts
72
+ options = Thor::Options.new(args, opts)
73
+ hash = options.getopts(false)
74
+ list = options.skip_non_opts
75
+ hash.update options.getopts(false)
76
+ options.check_required_args hash
77
+ [list, hash]
78
+ end
79
+ end
80
+ 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
@@ -0,0 +1,18 @@
1
+ require "thor/task"
2
+
3
+ class Thor::PackageTask < Thor::Task
4
+ attr_accessor :spec
5
+ attr_accessor :opts
6
+
7
+ def initialize(gemspec, opts = {})
8
+ super(:package, "build a gem package")
9
+ @spec = gemspec
10
+ @opts = {:dir => File.join(Dir.pwd, "pkg")}.merge(opts)
11
+ end
12
+
13
+ def run
14
+ FileUtils.mkdir_p(@opts[:dir])
15
+ Gem::Builder.new(spec).build
16
+ FileUtils.mv(spec.file_name, File.join(@opts[:dir], spec.file_name))
17
+ end
18
+ end
data/lib/thor/tasks.rb ADDED
@@ -0,0 +1,74 @@
1
+ require "thor"
2
+ require "fileutils"
3
+
4
+ class Thor
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
12
+ end
13
+
14
+ def self.install_task(spec)
15
+ package_task spec
16
+
17
+ desc "install", "install the gem"
18
+ define_method :install do
19
+ old_stderr, $stderr = $stderr.dup, File.open("/dev/null", "w")
20
+ package
21
+ $stderr = old_stderr
22
+ system %{sudo gem install pkg/#{spec.name}-#{spec.version} --no-rdoc --no-ri --no-update-sources}
23
+ end
24
+ end
25
+
26
+ def self.spec_task(file_list, opts = {})
27
+ name = opts.delete(:name) || "spec"
28
+ rcov_dir = opts.delete(:rcov_dir) || "coverage"
29
+ file_list = file_list.map {|f| %["#{f}"]}.join(" ")
30
+ verbose = opts.delete(:verbose)
31
+ opts = {:format => "specdoc", :color => true}.merge(opts)
32
+
33
+ rcov_opts = convert_task_options(opts.delete(:rcov) || {})
34
+ rcov = !rcov_opts.empty?
35
+ options = convert_task_options(opts)
36
+
37
+ if rcov
38
+ FileUtils.rm_rf(File.join(Dir.pwd, rcov_dir))
39
+ end
40
+
41
+ desc(name, "spec task")
42
+ define_method(name) do
43
+ cmd = "ruby "
44
+ if rcov
45
+ cmd << "-S rcov -o #{rcov_dir} #{rcov_opts} "
46
+ end
47
+ cmd << `which spec`.chomp
48
+ cmd << " -- " if rcov
49
+ cmd << " "
50
+ cmd << file_list
51
+ cmd << " "
52
+ cmd << options
53
+ puts cmd if verbose
54
+ system(cmd)
55
+ exit($?.exitstatus)
56
+ end
57
+ end
58
+
59
+ private
60
+ def self.convert_task_options(opts)
61
+ opts.map do |key, value|
62
+ case value
63
+ when true
64
+ "--#{key}"
65
+ when Array
66
+ value.map {|v| "--#{key} #{v.inspect}"}.join(" ")
67
+ when nil, false
68
+ ""
69
+ else
70
+ "--#{key} #{value.inspect}"
71
+ end
72
+ end.join(" ")
73
+ end
74
+ end
data/lib/thor/util.rb ADDED
@@ -0,0 +1,43 @@
1
+ require 'thor/error'
2
+
3
+ class Thor
4
+ module Util
5
+
6
+ def self.constant_to_thor_path(str, remove_default = true)
7
+ str = snake_case(str.to_s).squeeze(":")
8
+ str.gsub!(/^default/, '') if remove_default
9
+ str
10
+ end
11
+
12
+ def self.constant_from_thor_path(str)
13
+ make_constant(to_constant(str))
14
+ rescue NameError => e
15
+ raise e unless e.message =~ /^uninitialized constant (.*)$/
16
+ raise Error, "There was no available namespace `#{str}'."
17
+ end
18
+
19
+ def self.to_constant(str)
20
+ str = 'default' if str.empty?
21
+ str.gsub(/:(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
22
+ end
23
+
24
+ def self.constants_in_contents(str)
25
+ klasses = self.constants.dup
26
+ eval(str)
27
+ ret = self.constants - klasses
28
+ ret.each {|k| self.send(:remove_const, k)}
29
+ ret
30
+ end
31
+
32
+ def self.make_constant(str)
33
+ list = str.split("::").inject(Object) {|obj, x| obj.const_get(x)}
34
+ end
35
+
36
+ def self.snake_case(str)
37
+ return str.downcase if str =~ /^[A-Z_]+$/
38
+ str.gsub(/\B[A-Z]/, '_\&').squeeze('_') =~ /_*(.*)/
39
+ return $+.downcase
40
+ end
41
+
42
+ end
43
+ end
data/lib/thor.rb ADDED
@@ -0,0 +1,141 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__))
2
+ require "thor/options"
3
+ require "thor/util"
4
+ require "thor/task"
5
+ require "thor/task_hash"
6
+
7
+ class Thor
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
23
+ end
24
+
25
+ def self.method_options(opts)
26
+ @method_options = opts.inject({}) do |accum, (k,v)|
27
+ accum.merge("--" + k.to_s => v)
28
+ end
29
+ end
30
+
31
+ def self.subclass_files
32
+ @subclass_files ||= Hash.new {|h,k| h[k] = []}
33
+ end
34
+
35
+ def self.subclasses
36
+ @subclasses ||= []
37
+ end
38
+
39
+ def self.tasks
40
+ @tasks ||= TaskHash.new(self)
41
+ end
42
+
43
+ def self.opts
44
+ (@opts || {}).merge(self == Thor ? {} : superclass.opts)
45
+ end
46
+
47
+ def self.[](task)
48
+ namespaces = task.split(":")
49
+ klass = Thor::Util.constant_from_thor_path(namespaces[0...-1].join(":"))
50
+ raise Error, "`#{klass}' is not a Thor class" unless klass <= Thor
51
+ klass.tasks[namespaces.last]
52
+ end
53
+
54
+ def self.maxima
55
+ @maxima ||= begin
56
+ max_usage = tasks.map {|_, t| t.usage}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
57
+ max_desc = tasks.map {|_, t| t.description}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
58
+ max_opts = tasks.map {|_, t| t.formatted_opts}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
59
+ Struct.new(:description, :usage, :opt).new(max_desc, max_usage, max_opts)
60
+ end
61
+ end
62
+
63
+ def self.start(args = ARGV)
64
+ options = Thor::Options.new(args, self.opts)
65
+ opts = options.getopts
66
+ args = options.args
67
+
68
+ meth = args.first
69
+ meth = @map[meth].to_s if @map && @map[meth]
70
+ meth ||= "help"
71
+
72
+ tasks[meth].parse new(opts, *args), args[1..-1]
73
+ rescue Thor::Error => e
74
+ $stderr.puts e.message
75
+ end
76
+
77
+ class << self
78
+ protected
79
+ def inherited(klass)
80
+ register_klass_file klass
81
+ end
82
+
83
+ def method_added(meth)
84
+ meth = meth.to_s
85
+
86
+ if meth == "initialize"
87
+ @opts = @method_options
88
+ @method_options = nil
89
+ return
90
+ end
91
+
92
+ return if !public_instance_methods.include?(meth) || !@usage
93
+ register_klass_file self
94
+
95
+ tasks[meth] = Task.new(meth, @desc, @usage, @method_options)
96
+
97
+ @usage, @desc, @method_options = nil
98
+ end
99
+
100
+ def register_klass_file(klass, file = caller[1].split(":")[0])
101
+ unless self == Thor
102
+ superclass.register_klass_file(klass, file)
103
+ return
104
+ end
105
+
106
+ file_subclasses = subclass_files[File.expand_path(file)]
107
+ file_subclasses << klass unless file_subclasses.include?(klass)
108
+ subclasses << klass unless subclasses.include?(klass)
109
+ end
110
+ end
111
+
112
+ def initialize(opts = {}, *args)
113
+ end
114
+
115
+ map ["-h", "-?", "--help", "-D"] => :help
116
+
117
+ desc "help [TASK]", "describe available tasks or one specific task"
118
+ def help(task = nil)
119
+ if task
120
+ if task.include? ?:
121
+ task = self.class[task]
122
+ namespace = true
123
+ else
124
+ task = self.class.tasks[task]
125
+ end
126
+
127
+ puts task.formatted_usage(namespace)
128
+ puts task.description
129
+ return
130
+ end
131
+
132
+ puts "Options"
133
+ puts "-------"
134
+ self.class.tasks.each do |_, task|
135
+ format = "%-" + (self.class.maxima.usage + self.class.maxima.opt + 4).to_s + "s"
136
+ print format % ("#{task.formatted_usage}")
137
+ puts task.description.split("\n").first
138
+ end
139
+ end
140
+
141
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mislav-thor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.5
5
+ platform: ruby
6
+ authors:
7
+ - Yehuda Katz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-08-25 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: A gem that maps options to a class
17
+ email: wycats@gmail.com
18
+ executables:
19
+ - thor
20
+ - rake2thor
21
+ extensions: []
22
+
23
+ extra_rdoc_files:
24
+ - README.markdown
25
+ - LICENSE
26
+ files:
27
+ - LICENSE
28
+ - README.markdown
29
+ - Rakefile
30
+ - bin/rake2thor
31
+ - bin/thor
32
+ - lib/thor
33
+ - lib/thor/error.rb
34
+ - lib/thor/options.rb
35
+ - lib/thor/ordered_hash.rb
36
+ - lib/thor/runner.rb
37
+ - lib/thor/task.rb
38
+ - lib/thor/task_hash.rb
39
+ - lib/thor/tasks
40
+ - lib/thor/tasks/package.rb
41
+ - lib/thor/tasks.rb
42
+ - lib/thor/util.rb
43
+ - lib/thor.rb
44
+ has_rdoc: true
45
+ homepage: http://yehudakatz.com
46
+ post_install_message:
47
+ rdoc_options: []
48
+
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ requirements: []
64
+
65
+ rubyforge_project: thor
66
+ rubygems_version: 1.2.0
67
+ signing_key:
68
+ specification_version: 2
69
+ summary: A gem that maps options to a class
70
+ test_files: []
71
+