wycats-thor 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.markdown +59 -0
- data/Rakefile +6 -0
- data/bin/rake2thor +83 -0
- data/bin/thor +7 -0
- data/lib/getopt.rb +238 -0
- data/lib/thor.rb +121 -0
- data/lib/thor/error.rb +3 -0
- data/lib/thor/ordered_hash.rb +64 -0
- data/lib/thor/runner.rb +222 -0
- data/lib/thor/task.rb +74 -0
- data/lib/thor/task_hash.rb +22 -0
- data/lib/thor/tasks.rb +70 -0
- data/lib/thor/util.rb +43 -0
- metadata +69 -0
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,59 @@
|
|
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
|
+
Examples:
|
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
|
+
|
40
|
+
[2] Map additional non-valid identifiers to specific methods. In this case,
|
41
|
+
convert -L to :list
|
42
|
+
|
43
|
+
[3] Describe the method immediately below. The first parameter is the usage information,
|
44
|
+
and the second parameter is the description.
|
45
|
+
|
46
|
+
[4] Provide any additional options. These will be marshaled from -- and - params.
|
47
|
+
In this case, a --force and a -f option is added.
|
48
|
+
|
49
|
+
Types for `method_options`
|
50
|
+
--------------------------
|
51
|
+
|
52
|
+
<dl>
|
53
|
+
<dt>:boolean</dt>
|
54
|
+
<dd>true if the option is passed</dd>
|
55
|
+
<dt>:required</dt>
|
56
|
+
<dd>A key/value option that MUST be provided</dd>
|
57
|
+
<dt>:optional</dt>
|
58
|
+
<dd>A key/value option that MAY be provided</dd>
|
59
|
+
</dl>
|
data/Rakefile
ADDED
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
data/lib/getopt.rb
ADDED
@@ -0,0 +1,238 @@
|
|
1
|
+
# The last time the Getopt gem was modified was August 2007, so it's safe to vendor (it does everything we need)
|
2
|
+
|
3
|
+
module Getopt
|
4
|
+
|
5
|
+
REQUIRED = 0
|
6
|
+
BOOLEAN = 1
|
7
|
+
OPTIONAL = 2
|
8
|
+
INCREMENT = 3
|
9
|
+
NEGATABLE = 4
|
10
|
+
NUMERIC = 5
|
11
|
+
|
12
|
+
class Long
|
13
|
+
class Error < StandardError; end
|
14
|
+
|
15
|
+
VERSION = '1.3.6'
|
16
|
+
|
17
|
+
# Takes an array of switches. Each array consists of up to three
|
18
|
+
# elements that indicate the name and type of switch. Returns a hash
|
19
|
+
# containing each switch name, minus the '-', as a key. The value
|
20
|
+
# for each key depends on the type of switch and/or the value provided
|
21
|
+
# by the user.
|
22
|
+
#
|
23
|
+
# The long switch _must_ be provided. The short switch defaults to the
|
24
|
+
# first letter of the short switch. The default type is BOOLEAN.
|
25
|
+
#
|
26
|
+
# Example:
|
27
|
+
#
|
28
|
+
# opts = Getopt::Long.getopts(
|
29
|
+
# ["--debug"],
|
30
|
+
# ["--verbose", "-v"],
|
31
|
+
# ["--level", "-l", NUMERIC]
|
32
|
+
# )
|
33
|
+
#
|
34
|
+
# See the README file for more information.
|
35
|
+
#
|
36
|
+
def self.getopts(*switches)
|
37
|
+
if switches.empty?
|
38
|
+
raise ArgumentError, "no switches provided"
|
39
|
+
end
|
40
|
+
|
41
|
+
hash = {} # Hash returned to user
|
42
|
+
valid = [] # Tracks valid switches
|
43
|
+
types = {} # Tracks argument types
|
44
|
+
syns = {} # Tracks long and short arguments, or multiple shorts
|
45
|
+
|
46
|
+
# If a string is passed, split it and convert it to an array of arrays
|
47
|
+
if switches.first.kind_of?(String)
|
48
|
+
switches = switches.join.split
|
49
|
+
switches.map!{ |switch| switch = [switch] }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Set our list of valid switches, and proper types for each switch
|
53
|
+
switches.each{ |switch|
|
54
|
+
valid.push(switch[0]) # Set valid long switches
|
55
|
+
|
56
|
+
# Set type for long switch, default to BOOLEAN.
|
57
|
+
if switch[1].kind_of?(Fixnum)
|
58
|
+
switch[2] = switch[1]
|
59
|
+
types[switch[0]] = switch[2]
|
60
|
+
switch[1] = switch[0][1..2]
|
61
|
+
else
|
62
|
+
switch[2] ||= BOOLEAN
|
63
|
+
types[switch[0]] = switch[2]
|
64
|
+
switch[1] ||= switch[0][1..2]
|
65
|
+
end
|
66
|
+
|
67
|
+
# Create synonym hash. Default to first char of long switch for
|
68
|
+
# short switch, e.g. "--verbose" creates a "-v" synonym. The same
|
69
|
+
# synonym can only be used once - first one wins.
|
70
|
+
syns[switch[0]] = switch[1] unless syns[switch[1]]
|
71
|
+
syns[switch[1]] = switch[0] unless syns[switch[1]]
|
72
|
+
|
73
|
+
switch[1].each{ |char|
|
74
|
+
types[char] = switch[2] # Set type for short switch
|
75
|
+
valid.push(char) # Set valid short switches
|
76
|
+
}
|
77
|
+
|
78
|
+
if ARGV.empty? && switch[2] == REQUIRED
|
79
|
+
raise Error, "no value provided for required argument '#{switch[0]}'"
|
80
|
+
end
|
81
|
+
}
|
82
|
+
|
83
|
+
re_long = /^(--\w+[-\w+]*)?$/
|
84
|
+
re_short = /^(-\w)$/
|
85
|
+
re_long_eq = /^(--\w+[-\w+]*)?=(.*?)$|(-\w?)=(.*?)$/
|
86
|
+
re_short_sq = /^(-\w)(\S+?)$/
|
87
|
+
|
88
|
+
ARGV.each_with_index{ |opt, index|
|
89
|
+
|
90
|
+
# Allow either -x -v or -xv style for single char args
|
91
|
+
if re_short_sq.match(opt)
|
92
|
+
chars = opt.split("")[1..-1].map{ |s| s = "-#{s}" }
|
93
|
+
|
94
|
+
chars.each_with_index{ |char, i|
|
95
|
+
unless valid.include?(char)
|
96
|
+
raise Error, "invalid switch '#{char}'"
|
97
|
+
end
|
98
|
+
|
99
|
+
# Grab the next arg if the switch takes a required arg
|
100
|
+
if types[char] == REQUIRED
|
101
|
+
# Deal with a argument squished up against switch
|
102
|
+
if chars[i+1]
|
103
|
+
arg = chars[i+1..-1].join.tr("-","")
|
104
|
+
ARGV.push(char, arg)
|
105
|
+
break
|
106
|
+
else
|
107
|
+
arg = ARGV.delete_at(index+1)
|
108
|
+
if arg.nil? || valid.include?(arg) # Minor cheat here
|
109
|
+
err = "no value provided for required argument '#{char}'"
|
110
|
+
raise Error, err
|
111
|
+
end
|
112
|
+
ARGV.push(char, arg)
|
113
|
+
end
|
114
|
+
elsif types[char] == OPTIONAL
|
115
|
+
if chars[i+1] && !valid.include?(chars[i+1])
|
116
|
+
arg = chars[i+1..-1].join.tr("-","")
|
117
|
+
ARGV.push(char, arg)
|
118
|
+
break
|
119
|
+
elsif
|
120
|
+
if ARGV[index+1] && !valid.include?(ARGV[index+1])
|
121
|
+
arg = ARGV.delete_at(index+1)
|
122
|
+
ARGV.push(char, arg)
|
123
|
+
end
|
124
|
+
else
|
125
|
+
ARGV.push(char)
|
126
|
+
end
|
127
|
+
else
|
128
|
+
ARGV.push(char)
|
129
|
+
end
|
130
|
+
}
|
131
|
+
next
|
132
|
+
end
|
133
|
+
|
134
|
+
if match = re_long.match(opt) || match = re_short.match(opt)
|
135
|
+
switch = match.captures.first
|
136
|
+
end
|
137
|
+
|
138
|
+
if match = re_long_eq.match(opt)
|
139
|
+
switch, value = match.captures.compact
|
140
|
+
ARGV.push(switch, value)
|
141
|
+
next
|
142
|
+
end
|
143
|
+
|
144
|
+
# Make sure that all the switches are valid. If 'switch' isn't
|
145
|
+
# defined at this point, it means an option was passed without
|
146
|
+
# a preceding switch, e.g. --option foo bar.
|
147
|
+
unless valid.include?(switch)
|
148
|
+
switch ||= opt
|
149
|
+
raise Error, "invalid switch '#{switch}'"
|
150
|
+
end
|
151
|
+
|
152
|
+
# Required arguments
|
153
|
+
if types[switch] == REQUIRED
|
154
|
+
nextval = ARGV[index+1]
|
155
|
+
|
156
|
+
# Make sure there's a value for mandatory arguments
|
157
|
+
if nextval.nil?
|
158
|
+
err = "no value provided for required argument '#{switch}'"
|
159
|
+
raise Error, err
|
160
|
+
end
|
161
|
+
|
162
|
+
# If there is a value, make sure it's not another switch
|
163
|
+
if valid.include?(nextval)
|
164
|
+
err = "cannot pass switch '#{nextval}' as an argument"
|
165
|
+
raise Error, err
|
166
|
+
end
|
167
|
+
|
168
|
+
# If the same option appears more than once, put the values
|
169
|
+
# in array.
|
170
|
+
if hash[switch]
|
171
|
+
hash[switch] = [hash[switch], nextval].flatten
|
172
|
+
else
|
173
|
+
hash[switch] = nextval
|
174
|
+
end
|
175
|
+
ARGV.delete_at(index+1)
|
176
|
+
end
|
177
|
+
|
178
|
+
# For boolean arguments set the switch's value to true.
|
179
|
+
if types[switch] == BOOLEAN
|
180
|
+
if hash.has_key?(switch)
|
181
|
+
raise Error, "boolean switch already set"
|
182
|
+
end
|
183
|
+
hash[switch] = true
|
184
|
+
end
|
185
|
+
|
186
|
+
# For increment arguments, set the switch's value to 0, or
|
187
|
+
# increment it by one if it already exists.
|
188
|
+
if types[switch] == INCREMENT
|
189
|
+
if hash.has_key?(switch)
|
190
|
+
hash[switch] += 1
|
191
|
+
else
|
192
|
+
hash[switch] = 1
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# For optional argument, there may be an argument. If so, it
|
197
|
+
# cannot be another switch. If not, it is set to true.
|
198
|
+
if types[switch] == OPTIONAL
|
199
|
+
nextval = ARGV[index+1]
|
200
|
+
if valid.include?(nextval)
|
201
|
+
hash[switch] = true
|
202
|
+
else
|
203
|
+
hash[switch] = nextval
|
204
|
+
ARGV.delete_at(index+1)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
}
|
208
|
+
|
209
|
+
# Set synonymous switches to the same value, e.g. if -t is a synonym
|
210
|
+
# for --test, and the user passes "--test", then set "-t" to the same
|
211
|
+
# value that "--test" was set to.
|
212
|
+
#
|
213
|
+
# This allows users to refer to the long or short switch and get
|
214
|
+
# the same value
|
215
|
+
hash.each{ |switch, val|
|
216
|
+
if syns.keys.include?(switch)
|
217
|
+
syns[switch].each{ |key|
|
218
|
+
hash[key] = val
|
219
|
+
}
|
220
|
+
end
|
221
|
+
}
|
222
|
+
|
223
|
+
# Get rid of leading "--" and "-" to make it easier to reference
|
224
|
+
hash.each{ |key, value|
|
225
|
+
if key[0,2] == '--'
|
226
|
+
nkey = key.sub('--', '')
|
227
|
+
else
|
228
|
+
nkey = key.sub('-', '')
|
229
|
+
end
|
230
|
+
hash.delete(key)
|
231
|
+
hash[nkey] = value
|
232
|
+
}
|
233
|
+
|
234
|
+
hash
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
end
|
data/lib/thor.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
$:.unshift File.expand_path(File.dirname(__FILE__))
|
2
|
+
require "getopt"
|
3
|
+
require "thor/util"
|
4
|
+
require "thor/task"
|
5
|
+
require "thor/task_hash"
|
6
|
+
|
7
|
+
class Thor
|
8
|
+
def self.map(map)
|
9
|
+
@map ||= superclass.instance_variable_get("@map") || {}
|
10
|
+
map.each do |key, value|
|
11
|
+
if key.respond_to?(:each)
|
12
|
+
key.each {|subkey| @map[subkey] = value}
|
13
|
+
else
|
14
|
+
@map[key] = value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.desc(usage, description)
|
20
|
+
@usage, @desc = usage, description
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.method_options(opts)
|
24
|
+
@method_options = opts.inject({}) do |accum, (k,v)|
|
25
|
+
accum.merge("--" + k.to_s => v.to_s.upcase)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.subclass_files
|
30
|
+
@subclass_files ||= Hash.new {|h,k| h[k] = []}
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.subclasses
|
34
|
+
@subclasses ||= []
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.tasks
|
38
|
+
@tasks ||= TaskHash.new(self)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.[](task)
|
42
|
+
namespaces = task.split(":")
|
43
|
+
klass = Thor::Util.constant_from_thor_path(namespaces[0...-1].join(":"))
|
44
|
+
raise Error, "`#{klass}' is not a Thor class" unless klass <= Thor
|
45
|
+
klass.tasks[namespaces.last]
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.maxima
|
49
|
+
@maxima ||= begin
|
50
|
+
max_usage = tasks.map {|_, t| t.usage}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
|
51
|
+
max_desc = tasks.map {|_, t| t.description}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
|
52
|
+
max_opts = tasks.map {|_, t| t.formatted_opts}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
|
53
|
+
Struct.new(:description, :usage, :opt).new(max_desc, max_usage, max_opts)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.start(args = ARGV)
|
58
|
+
meth = args.first
|
59
|
+
meth = @map[meth].to_s if @map && @map[meth]
|
60
|
+
meth ||= "help"
|
61
|
+
|
62
|
+
tasks[meth].parse args[1..-1]
|
63
|
+
rescue Thor::Error => e
|
64
|
+
$stderr.puts e.message
|
65
|
+
end
|
66
|
+
|
67
|
+
class << self
|
68
|
+
protected
|
69
|
+
def inherited(klass)
|
70
|
+
register_klass_file klass
|
71
|
+
end
|
72
|
+
|
73
|
+
def method_added(meth)
|
74
|
+
meth = meth.to_s
|
75
|
+
return if !public_instance_methods.include?(meth) || !@usage
|
76
|
+
register_klass_file self
|
77
|
+
|
78
|
+
tasks[meth] = Task.new(meth, @desc, @usage, @method_options)
|
79
|
+
|
80
|
+
@usage, @desc, @method_options = nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def register_klass_file(klass, file = caller[1].split(":")[0])
|
84
|
+
unless self == Thor
|
85
|
+
superclass.register_klass_file(klass, file)
|
86
|
+
return
|
87
|
+
end
|
88
|
+
|
89
|
+
file_subclasses = subclass_files[File.expand_path(file)]
|
90
|
+
file_subclasses << klass unless file_subclasses.include?(klass)
|
91
|
+
subclasses << klass unless subclasses.include?(klass)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
map ["-h", "-?", "--help", "-D"] => :help
|
96
|
+
|
97
|
+
desc "help [TASK]", "describe available tasks or one specific task"
|
98
|
+
def help(task = nil)
|
99
|
+
if task
|
100
|
+
if task.include? ?:
|
101
|
+
task = self.class[task]
|
102
|
+
namespace = true
|
103
|
+
else
|
104
|
+
task = self.class.tasks[task]
|
105
|
+
end
|
106
|
+
|
107
|
+
puts task.formatted_usage(namespace)
|
108
|
+
puts task.description
|
109
|
+
return
|
110
|
+
end
|
111
|
+
|
112
|
+
puts "Options"
|
113
|
+
puts "-------"
|
114
|
+
self.class.tasks.each do |_, task|
|
115
|
+
format = "%-" + (self.class.maxima.usage + self.class.maxima.opt + 4).to_s + "s"
|
116
|
+
print format % ("#{task.formatted_usage}")
|
117
|
+
puts task.description.split("\n").first
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
data/lib/thor/error.rb
ADDED
@@ -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
|
data/lib/thor/runner.rb
ADDED
@@ -0,0 +1,222 @@
|
|
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
|
19
|
+
def install(name, opts = {})
|
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 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 = opts["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
|
+
yaml[as] = {:filename => Digest::MD5.hexdigest(name + as), :location => name, :constants => constants}
|
60
|
+
|
61
|
+
save_yaml(yaml)
|
62
|
+
|
63
|
+
puts "Storing thor file in your system repository"
|
64
|
+
|
65
|
+
File.open(File.join(thor_root, yaml[as][:filename] + ".thor"), "w") do |file|
|
66
|
+
file.puts contents
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
desc "uninstall NAME", "uninstall a named Thor module"
|
71
|
+
def uninstall(name)
|
72
|
+
yaml = thor_yaml
|
73
|
+
raise Error, "Can't find module `#{name}'" unless yaml[name]
|
74
|
+
|
75
|
+
puts "Uninstalling #{name}."
|
76
|
+
|
77
|
+
file = File.join(thor_root, "#{yaml[name][:filename]}.thor")
|
78
|
+
File.delete(file)
|
79
|
+
yaml.delete(name)
|
80
|
+
save_yaml(yaml)
|
81
|
+
|
82
|
+
puts "Done."
|
83
|
+
end
|
84
|
+
|
85
|
+
desc "update NAME", "update a Thor file from its original location"
|
86
|
+
def update(name)
|
87
|
+
yaml = thor_yaml
|
88
|
+
raise Error, "Can't find module `#{name}'" if !yaml[name] || !yaml[name][:location]
|
89
|
+
|
90
|
+
puts "Updating `#{name}' from #{yaml[name][:location]}"
|
91
|
+
install(yaml[name][:location], "as" => name)
|
92
|
+
end
|
93
|
+
|
94
|
+
desc "installed", "list the installed Thor modules and tasks (--internal means list the built-in tasks as well)"
|
95
|
+
method_options :internal => :boolean
|
96
|
+
def installed(opts = {})
|
97
|
+
Dir["#{ENV["HOME"]}/.thor/**/*.thor"].each do |f|
|
98
|
+
load f unless Thor.subclass_files.keys.include?(File.expand_path(f))
|
99
|
+
end
|
100
|
+
|
101
|
+
klasses = Thor.subclasses
|
102
|
+
klasses -= [Thor, Thor::Runner] unless opts['internal']
|
103
|
+
display_klasses(true, klasses)
|
104
|
+
end
|
105
|
+
|
106
|
+
desc "list [SEARCH]", "list the available thor tasks (--substring means SEARCH can be anywhere in the module)"
|
107
|
+
method_options :substring => :boolean
|
108
|
+
def list(search = "", options = {})
|
109
|
+
initialize_thorfiles
|
110
|
+
search = ".*#{search}" if options["substring"]
|
111
|
+
search = /^#{search}.*/i
|
112
|
+
|
113
|
+
display_klasses(false, Thor.subclasses.select {|k|
|
114
|
+
Thor::Util.constant_to_thor_path(k.name) =~ search})
|
115
|
+
end
|
116
|
+
|
117
|
+
# Override Thor#help so we can give info about not-yet-loaded tasks
|
118
|
+
def help(task = nil)
|
119
|
+
initialize_thorfiles(task) if task && task.include?(?:)
|
120
|
+
super
|
121
|
+
end
|
122
|
+
|
123
|
+
def method_missing(meth, *args)
|
124
|
+
meth = meth.to_s
|
125
|
+
super(meth.to_sym, *args) unless meth.include? ?:
|
126
|
+
|
127
|
+
initialize_thorfiles(meth)
|
128
|
+
Thor[meth].parse ARGV[1..-1]
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
def thor_root
|
133
|
+
File.join(ENV["HOME"], ".thor")
|
134
|
+
end
|
135
|
+
|
136
|
+
def thor_yaml
|
137
|
+
yaml_file = File.join(thor_root, "thor.yml")
|
138
|
+
yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file)
|
139
|
+
yaml || {}
|
140
|
+
end
|
141
|
+
|
142
|
+
def save_yaml(yaml)
|
143
|
+
yaml_file = File.join(thor_root, "thor.yml")
|
144
|
+
File.open(yaml_file, "w") {|f| f.puts yaml.to_yaml }
|
145
|
+
end
|
146
|
+
|
147
|
+
def display_klasses(with_modules = false, klasses = Thor.subclasses)
|
148
|
+
klasses -= [Thor, Thor::Runner] unless with_modules
|
149
|
+
raise Error, "No Thor tasks available" if klasses.empty?
|
150
|
+
|
151
|
+
if with_modules && !(yaml = thor_yaml).empty?
|
152
|
+
max_name = yaml.max {|(xk,xv),(yk,yv)| xk.size <=> yk.size }.first.size
|
153
|
+
|
154
|
+
print "%-#{max_name + 4}s" % "Modules"
|
155
|
+
puts "Namespaces"
|
156
|
+
print "%-#{max_name + 4}s" % "-------"
|
157
|
+
puts "----------"
|
158
|
+
|
159
|
+
yaml.each do |name, info|
|
160
|
+
print "%-#{max_name + 4}s" % name
|
161
|
+
puts info[:constants].map {|c| Thor::Util.constant_to_thor_path(c)}.join(", ")
|
162
|
+
end
|
163
|
+
|
164
|
+
puts
|
165
|
+
end
|
166
|
+
|
167
|
+
puts "Tasks"
|
168
|
+
puts "-----"
|
169
|
+
|
170
|
+
# Calculate the largest base class name
|
171
|
+
max_base = klasses.max do |x,y|
|
172
|
+
Thor::Util.constant_to_thor_path(x.name).size <=> Thor::Util.constant_to_thor_path(y.name).size
|
173
|
+
end.name.size
|
174
|
+
|
175
|
+
# Calculate the size of the largest option description
|
176
|
+
max_left_item = klasses.max do |x,y|
|
177
|
+
(x.maxima.usage + x.maxima.opt).to_i <=> (y.maxima.usage + y.maxima.opt).to_i
|
178
|
+
end
|
179
|
+
|
180
|
+
max_left = max_left_item.maxima.usage + max_left_item.maxima.opt
|
181
|
+
|
182
|
+
klasses.each {|k| display_tasks(k, max_base, max_left)}
|
183
|
+
end
|
184
|
+
|
185
|
+
def display_tasks(klass, max_base, max_left)
|
186
|
+
base = Thor::Util.constant_to_thor_path(klass.name)
|
187
|
+
klass.tasks.each true do |name, task|
|
188
|
+
format_string = "%-#{max_left + max_base + 5}s"
|
189
|
+
print format_string % task.formatted_usage(true)
|
190
|
+
puts task.description
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def initialize_thorfiles(relevant_to = nil)
|
195
|
+
thorfiles(relevant_to).each {|f| load f unless Thor.subclass_files.keys.include?(File.expand_path(f))}
|
196
|
+
end
|
197
|
+
|
198
|
+
def thorfiles(relevant_to = nil)
|
199
|
+
path = Dir.pwd
|
200
|
+
thorfiles = []
|
201
|
+
|
202
|
+
# Look for Thorfile or *.thor in the current directory or a parent directory, until the root
|
203
|
+
while thorfiles.empty?
|
204
|
+
thorfiles = Dir[*Thor::Runner.globs_for(path)]
|
205
|
+
path = File.dirname(path)
|
206
|
+
break if path == "/"
|
207
|
+
end
|
208
|
+
|
209
|
+
# We want to load system-wide Thorfiles first
|
210
|
+
# so the local Thorfiles will override them.
|
211
|
+
(relevant_to ? thorfiles_relevant_to(relevant_to) :
|
212
|
+
Dir["#{ENV["HOME"]}/.thor/**/*.thor"]) + thorfiles
|
213
|
+
end
|
214
|
+
|
215
|
+
def thorfiles_relevant_to(meth)
|
216
|
+
klass_str = Thor::Util.to_constant(meth.split(":")[0...-1].join(":"))
|
217
|
+
thor_yaml.select do |k, v|
|
218
|
+
v[:constants] && v[:constants].include?(klass_str)
|
219
|
+
end.map { |k, v| File.join(thor_root, "#{v[:filename]}.thor") }
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
data/lib/thor/task.rb
ADDED
@@ -0,0 +1,74 @@
|
|
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(args)
|
11
|
+
run(*parse_args(args))
|
12
|
+
end
|
13
|
+
|
14
|
+
def run(*params)
|
15
|
+
raise Error, "klass is not defined for #{self.inspect}" unless klass
|
16
|
+
raise NoMethodError, "the `#{meth}' task of #{klass} is private" if
|
17
|
+
(klass.private_instance_methods + klass.protected_instance_methods).include?(meth)
|
18
|
+
|
19
|
+
klass.new.send(meth, *params)
|
20
|
+
rescue ArgumentError => e
|
21
|
+
raise e unless e.backtrace.first =~ /:in `#{meth}'$/
|
22
|
+
raise Error, "`#{meth}' was called incorrectly. Call as `#{formatted_usage}'"
|
23
|
+
rescue NoMethodError => e
|
24
|
+
raise e unless e.message =~ /^undefined method `#{meth}' for #<#{klass}:.*>$/
|
25
|
+
raise Error, "The #{namespace false} namespace doesn't have a `#{meth}' task"
|
26
|
+
end
|
27
|
+
|
28
|
+
def namespace(remove_default = true)
|
29
|
+
Thor::Util.constant_to_thor_path(klass, remove_default)
|
30
|
+
end
|
31
|
+
|
32
|
+
def with_klass(klass)
|
33
|
+
new = self.dup
|
34
|
+
new.klass = klass
|
35
|
+
new
|
36
|
+
end
|
37
|
+
|
38
|
+
def formatted_opts
|
39
|
+
return "" if opts.nil?
|
40
|
+
opts.map do |opt, val|
|
41
|
+
if val == true || val == "BOOLEAN"
|
42
|
+
"[#{opt}]"
|
43
|
+
elsif val == "REQUIRED"
|
44
|
+
opt + "=" + opt.gsub(/\-/, "").upcase
|
45
|
+
elsif val == "OPTIONAL"
|
46
|
+
"[" + opt + "=" + opt.gsub(/\-/, "").upcase + "]"
|
47
|
+
end
|
48
|
+
end.join(" ")
|
49
|
+
end
|
50
|
+
|
51
|
+
def formatted_usage(namespace = false)
|
52
|
+
(namespace ? self.namespace + ':' : '') + usage +
|
53
|
+
(opts ? " " + formatted_opts : "")
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
def parse_args(args)
|
59
|
+
return args unless opts
|
60
|
+
|
61
|
+
args = args.dup
|
62
|
+
params = []
|
63
|
+
params << args.shift until args.empty? || args.first[0] == ?-
|
64
|
+
|
65
|
+
old_argv = ARGV.dup
|
66
|
+
ARGV.replace args
|
67
|
+
options = Getopt::Long.getopts(*opts.map do |opt, val|
|
68
|
+
[opt, val == true ? Getopt::BOOLEAN : Getopt.const_get(val)].flatten
|
69
|
+
end)
|
70
|
+
ARGV.replace old_argv
|
71
|
+
params + [options]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
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)
|
17
|
+
return task.with_klass(@klass)
|
18
|
+
end
|
19
|
+
|
20
|
+
Thor::Task.dynamic(name, @klass)
|
21
|
+
end
|
22
|
+
end
|
data/lib/thor/tasks.rb
ADDED
@@ -0,0 +1,70 @@
|
|
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
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
def self.convert_task_options(opts)
|
60
|
+
opts.map do |key, value|
|
61
|
+
if value == true
|
62
|
+
"--#{key}"
|
63
|
+
elsif value.is_a?(Array)
|
64
|
+
value.map {|v| "--#{key} #{v.inspect}"}.join(" ")
|
65
|
+
else
|
66
|
+
"--#{key} #{value.inspect}"
|
67
|
+
end
|
68
|
+
end.join(" ")
|
69
|
+
end
|
70
|
+
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
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wycats-thor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yehuda Katz
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-05-19 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/getopt.rb
|
33
|
+
- lib/thor
|
34
|
+
- lib/thor/error.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.rb
|
40
|
+
- lib/thor/util.rb
|
41
|
+
- lib/thor.rb
|
42
|
+
has_rdoc: true
|
43
|
+
homepage: http://yehudakatz.com
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project: thor
|
64
|
+
rubygems_version: 1.0.1
|
65
|
+
signing_key:
|
66
|
+
specification_version: 2
|
67
|
+
summary: A gem that maps options to a class
|
68
|
+
test_files: []
|
69
|
+
|