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 +20 -0
- data/README.markdown +58 -0
- data/Rakefile +6 -0
- data/bin/rake2thor +83 -0
- data/bin/thor +7 -0
- data/lib/thor/error.rb +3 -0
- data/lib/thor/options.rb +156 -0
- data/lib/thor/ordered_hash.rb +64 -0
- data/lib/thor/runner.rb +247 -0
- data/lib/thor/task.rb +80 -0
- data/lib/thor/task_hash.rb +22 -0
- data/lib/thor/tasks/package.rb +18 -0
- data/lib/thor/tasks.rb +74 -0
- data/lib/thor/util.rb +43 -0
- data/lib/thor.rb +141 -0
- metadata +71 -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,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
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/thor/error.rb
ADDED
data/lib/thor/options.rb
ADDED
|
@@ -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
|
data/lib/thor/runner.rb
ADDED
|
@@ -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
|
+
|