mislav-thor 0.9.5 → 0.9.10
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +52 -0
- data/README.markdown +36 -18
- data/Rakefile +1 -1
- data/lib/thor.rb +47 -18
- data/lib/thor/options.rb +211 -100
- data/lib/thor/runner.rb +89 -39
- data/lib/thor/task.rb +26 -23
- data/lib/thor/tasks.rb +5 -2
- data/lib/thor/util.rb +41 -9
- metadata +5 -3
data/CHANGELOG.rdoc
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
== TODO
|
2
|
+
|
3
|
+
* Change Thor.start to parse ARGV in a single pass
|
4
|
+
* Improve spec coverage for Thor::Runner
|
5
|
+
* Improve help output to list shorthand switches, too
|
6
|
+
* Investigate and fix deep namespacing ("foo:bar:baz") issues
|
7
|
+
|
8
|
+
== 0.9.8, released 2008-10-20
|
9
|
+
|
10
|
+
* Fixed some tiny issues that were introduced lately.
|
11
|
+
|
12
|
+
== 0.9.7, released 2008-10-13
|
13
|
+
|
14
|
+
* Setting global method options on the initialize method works as expected:
|
15
|
+
All other tasks will accept these global options in addition to their own.
|
16
|
+
* Added 'group' notion to Thor task sets (class Thor); by default all tasks
|
17
|
+
are in the 'standard' group. Running 'thor -T' will only show the standard
|
18
|
+
tasks - adding --all will show all tasks. You can also filter on a specific
|
19
|
+
group using the --group option: thor -T --group advanced
|
20
|
+
|
21
|
+
== 0.9.6, released 2008-09-13
|
22
|
+
|
23
|
+
* Generic improvements
|
24
|
+
|
25
|
+
== 0.9.5, released 2008-08-27
|
26
|
+
|
27
|
+
* Improve Windows compatibility
|
28
|
+
* Update (incorrect) README and task.thor sample file
|
29
|
+
* Options hash is now frozen (once returned)
|
30
|
+
* Allow magic predicates on options object. For instance: `options.force?`
|
31
|
+
* Add support for :numeric type
|
32
|
+
* BACKWARDS INCOMPATIBLE: Refactor Thor::Options. You cannot access shorthand forms in options hash anymore (for instance, options[:f])
|
33
|
+
* Allow specifying optional args with default values: method_options(:user => "mislav")
|
34
|
+
* Don't write options for nil or false values. This allows, for example, turning color off when running specs.
|
35
|
+
* Exit with the status of the spec command to help CI stuff out some.
|
36
|
+
|
37
|
+
== 0.9.4, released 2008-08-13
|
38
|
+
|
39
|
+
* Try to add Windows compatibility.
|
40
|
+
* BACKWARDS INCOMPATIBLE: options hash is now accessed as a property in your class and is not passed as last argument anymore
|
41
|
+
* Allow options at the beginning of the argument list as well as the end.
|
42
|
+
* Make options available with symbol keys in addition to string keys.
|
43
|
+
* Allow true to be passed to Thor#method_options to denote a boolean option.
|
44
|
+
* If loading a thor file fails, don't give up, just print a warning and keep going.
|
45
|
+
* Make sure that we re-raise errors if they happened further down the pipe than we care about.
|
46
|
+
* Only delete the old file on updating when the installation of the new one is a success
|
47
|
+
* Make it Ruby 1.8.5 compatible.
|
48
|
+
* Don't raise an error if a boolean switch is defined multiple times.
|
49
|
+
* Thor::Options now doesn't parse through things that look like options but aren't.
|
50
|
+
* Add URI detection to install task, and make sure we don't append ".thor" to URIs
|
51
|
+
* Add rake2thor to the gem binfiles.
|
52
|
+
* Make sure local Thorfiles override system-wide ones.
|
data/README.markdown
CHANGED
@@ -10,49 +10,67 @@ Example:
|
|
10
10
|
map "-L" => :list # [2]
|
11
11
|
|
12
12
|
desc "install APP_NAME", "install one of the available apps" # [3]
|
13
|
-
method_options :force => :boolean
|
14
|
-
def install(name
|
15
|
-
|
16
|
-
if
|
13
|
+
method_options :force => :boolean, :alias => :optional # [4]
|
14
|
+
def install(name)
|
15
|
+
user_alias = options[:alias]
|
16
|
+
if options.force?
|
17
17
|
# do something
|
18
18
|
end
|
19
|
+
# ... other code ...
|
19
20
|
end
|
20
21
|
|
21
22
|
desc "list [SEARCH]", "list all of the available apps, limited by SEARCH"
|
22
23
|
def list(search = "")
|
23
24
|
# list everything
|
24
25
|
end
|
25
|
-
|
26
26
|
end
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
Thor automatically maps commands as follows:
|
28
|
+
Thor automatically maps commands as such:
|
31
29
|
|
32
|
-
app install
|
30
|
+
app install myname --force
|
33
31
|
|
34
32
|
That gets converted to:
|
35
33
|
|
36
|
-
MyApp.new.install("
|
34
|
+
MyApp.new.install("myname")
|
35
|
+
# with {'force' => true} as options hash
|
37
36
|
|
38
37
|
1. Inherit from Thor to turn a class into an option mapper
|
39
38
|
2. Map additional non-valid identifiers to specific methods. In this case,
|
40
39
|
convert -L to :list
|
41
40
|
3. Describe the method immediately below. The first parameter is the usage information,
|
42
41
|
and the second parameter is the description.
|
43
|
-
4. Provide any additional options. These will be marshaled from
|
44
|
-
In this case, a
|
42
|
+
4. Provide any additional options. These will be marshaled from `--` and `-` params.
|
43
|
+
In this case, a `--force` and a `-f` option is added.
|
45
44
|
|
46
45
|
Types for `method_options`
|
47
46
|
--------------------------
|
48
47
|
|
49
48
|
<dl>
|
50
49
|
<dt><code>:boolean</code></dt>
|
51
|
-
|
50
|
+
<dd>true if the option is passed</dd>
|
51
|
+
<dt><code>true or false</code></dt>
|
52
|
+
<dd>same as <code>:boolean</code>, but fall back to given boolean as default value</dd>
|
52
53
|
<dt><code>:required</code></dt>
|
53
|
-
|
54
|
+
<dd>the value for this option MUST be provided</dd>
|
54
55
|
<dt><code>:optional</code></dt>
|
55
|
-
|
56
|
-
<dt
|
57
|
-
|
58
|
-
</
|
56
|
+
<dd>the value for this option MAY be provided</dd>
|
57
|
+
<dt><code>:numeric</code></dt>
|
58
|
+
<dd>the value MAY be provided, but MUST be in numeric form</dd>
|
59
|
+
<dt>a String or Numeric</dt>
|
60
|
+
<dd>same as <code>:optional</code>, but fall back to the given object as default value</dd>
|
61
|
+
</dl>
|
62
|
+
|
63
|
+
In case of unsatisfied requirements, `Thor::Options::Error` is raised.
|
64
|
+
|
65
|
+
Examples of option parsing:
|
66
|
+
|
67
|
+
# let's say this is how we defined options for a method:
|
68
|
+
method_options(:force => :boolean, :retries => :numeric)
|
69
|
+
|
70
|
+
# here is how the following command-line invocations would be parsed:
|
71
|
+
|
72
|
+
command -f --retries 5 # => {'force' => true, 'retries' => 5}
|
73
|
+
command --force -r=5 # => {'force' => true, 'retries' => 5}
|
74
|
+
command -fr 5 # => {'force' => true, 'retries' => 5}
|
75
|
+
command --retries=5 # => {'retries' => 5}
|
76
|
+
command -r5 # => {'retries' => 5}
|
data/Rakefile
CHANGED
data/lib/thor.rb
CHANGED
@@ -7,6 +7,13 @@ require "thor/task_hash"
|
|
7
7
|
class Thor
|
8
8
|
attr_accessor :options
|
9
9
|
|
10
|
+
def self.default_task(meth=nil)
|
11
|
+
unless meth.nil?
|
12
|
+
@default_task = (meth == :none) ? 'help' : meth.to_s
|
13
|
+
end
|
14
|
+
@default_task ||= (self == Thor ? 'help' : superclass.default_task)
|
15
|
+
end
|
16
|
+
|
10
17
|
def self.map(map)
|
11
18
|
@map ||= superclass.instance_variable_get("@map") || {}
|
12
19
|
map.each do |key, value|
|
@@ -22,10 +29,16 @@ class Thor
|
|
22
29
|
@usage, @desc = usage, description
|
23
30
|
end
|
24
31
|
|
32
|
+
def self.group(name)
|
33
|
+
@group_name = name.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.group_name
|
37
|
+
@group_name || 'standard'
|
38
|
+
end
|
39
|
+
|
25
40
|
def self.method_options(opts)
|
26
|
-
@method_options =
|
27
|
-
accum.merge("--" + k.to_s => v)
|
28
|
-
end
|
41
|
+
@method_options = (@method_options || {}).merge(opts)
|
29
42
|
end
|
30
43
|
|
31
44
|
def self.subclass_files
|
@@ -55,25 +68,42 @@ class Thor
|
|
55
68
|
@maxima ||= begin
|
56
69
|
max_usage = tasks.map {|_, t| t.usage}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
|
57
70
|
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.
|
71
|
+
max_opts = tasks.map {|_, t| t.opts ? t.opts.formatted_usage : ""}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
|
59
72
|
Struct.new(:description, :usage, :opt).new(max_desc, max_usage, max_opts)
|
60
73
|
end
|
61
74
|
end
|
62
75
|
|
63
76
|
def self.start(args = ARGV)
|
64
|
-
|
65
|
-
|
66
|
-
|
77
|
+
|
78
|
+
options = Thor::Options.new(self.opts)
|
79
|
+
opts = options.parse(args, false)
|
80
|
+
args = options.trailing_non_opts
|
67
81
|
|
68
82
|
meth = args.first
|
69
83
|
meth = @map[meth].to_s if @map && @map[meth]
|
70
|
-
meth ||=
|
84
|
+
meth ||= default_task
|
85
|
+
meth = meth.to_s.gsub('-','_') # treat foo-bar > foo_bar
|
71
86
|
|
72
87
|
tasks[meth].parse new(opts, *args), args[1..-1]
|
73
88
|
rescue Thor::Error => e
|
74
89
|
$stderr.puts e.message
|
75
90
|
end
|
91
|
+
|
92
|
+
# Invokes a specific task. You can use this method instead of start()
|
93
|
+
# to run a thor task if you know the specific task you want to invoke.
|
94
|
+
def self.invoke(task_name=nil, args = ARGV)
|
95
|
+
args = args.dup
|
96
|
+
args.unshift(task_name || default_task)
|
97
|
+
start(args)
|
98
|
+
end
|
76
99
|
|
100
|
+
# Main entry point method that should actually invoke the method. You
|
101
|
+
# can override this to provide some class-wide processing. The default
|
102
|
+
# implementation simply invokes the named method
|
103
|
+
def invoke(meth, *args)
|
104
|
+
self.send(meth, *args)
|
105
|
+
end
|
106
|
+
|
77
107
|
class << self
|
78
108
|
protected
|
79
109
|
def inherited(klass)
|
@@ -82,7 +112,7 @@ class Thor
|
|
82
112
|
|
83
113
|
def method_added(meth)
|
84
114
|
meth = meth.to_s
|
85
|
-
|
115
|
+
|
86
116
|
if meth == "initialize"
|
87
117
|
@opts = @method_options
|
88
118
|
@method_options = nil
|
@@ -126,15 +156,14 @@ class Thor
|
|
126
156
|
|
127
157
|
puts task.formatted_usage(namespace)
|
128
158
|
puts task.description
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
puts task.description.split("\n").first
|
159
|
+
else
|
160
|
+
puts "Options"
|
161
|
+
puts "-------"
|
162
|
+
self.class.tasks.each do |_, task|
|
163
|
+
puts task.formatted_usage
|
164
|
+
puts ' ' * 4 + task.description.split("\n").first
|
165
|
+
puts
|
166
|
+
end
|
138
167
|
end
|
139
168
|
end
|
140
169
|
|
data/lib/thor/options.rb
CHANGED
@@ -1,57 +1,55 @@
|
|
1
|
-
# This is a modified version of Daniel Berger's Getopt::
|
1
|
+
# This is a modified version of Daniel Berger's Getopt::Long class,
|
2
2
|
# licensed under Ruby's license.
|
3
3
|
|
4
|
-
require 'set'
|
5
|
-
|
6
4
|
class Thor
|
7
5
|
class Options
|
8
6
|
class Error < StandardError; end
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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]
|
7
|
+
|
8
|
+
# simple Hash with indifferent access
|
9
|
+
class Hash < ::Hash
|
10
|
+
def initialize(hash)
|
11
|
+
super()
|
12
|
+
update hash
|
38
13
|
end
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
forms.each {|f| h[f] ||= v}
|
43
|
-
h
|
14
|
+
|
15
|
+
def [](key)
|
16
|
+
super convert_key(key)
|
44
17
|
end
|
45
|
-
|
46
|
-
|
47
|
-
|
18
|
+
|
19
|
+
def values_at(*indices)
|
20
|
+
indices.collect { |key| self[convert_key(key)] }
|
48
21
|
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
def convert_key(key)
|
25
|
+
key.kind_of?(Symbol) ? key.to_s : key
|
26
|
+
end
|
27
|
+
|
28
|
+
# Magic predicates. For instance:
|
29
|
+
# options.force? # => !!options['force']
|
30
|
+
def method_missing(method, *args, &block)
|
31
|
+
method = method.to_s
|
32
|
+
if method =~ /^(\w+)=$/
|
33
|
+
self[$1] = args.first
|
34
|
+
elsif method =~ /^(\w+)\?$/
|
35
|
+
!!self[$1]
|
36
|
+
else
|
37
|
+
self[method]
|
38
|
+
end
|
39
|
+
end
|
49
40
|
end
|
50
41
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
42
|
+
NUMERIC = /(\d*\.\d+|\d+)/
|
43
|
+
LONG_RE = /^(--\w+[-\w+]*)$/
|
44
|
+
SHORT_RE = /^(-[a-z])$/i
|
45
|
+
EQ_RE = /^(--\w+[-\w+]*|-[a-z])=(.*)$/i
|
46
|
+
SHORT_SQ_RE = /^-([a-z]{2,})$/i # Allow either -x -v or -xv style for single char args
|
47
|
+
SHORT_NUM = /^(-[a-z])#{NUMERIC}$/i
|
48
|
+
|
49
|
+
attr_reader :leading_non_opts, :trailing_non_opts
|
50
|
+
|
51
|
+
def non_opts
|
52
|
+
leading_non_opts + trailing_non_opts
|
55
53
|
end
|
56
54
|
|
57
55
|
# Takes an array of switches. Each array consists of up to three
|
@@ -65,92 +63,205 @@ class Thor
|
|
65
63
|
#
|
66
64
|
# Example:
|
67
65
|
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
66
|
+
# opts = Thor::Options.new(
|
67
|
+
# "--debug" => true,
|
68
|
+
# ["--verbose", "-v"] => true,
|
69
|
+
# ["--level", "-l"] => :numeric
|
70
|
+
# ).parse(args)
|
73
71
|
#
|
74
|
-
def
|
75
|
-
|
72
|
+
def initialize(switches)
|
73
|
+
@defaults = {}
|
74
|
+
@shorts = {}
|
75
|
+
|
76
|
+
@leading_non_opts, @trailing_non_opts = [], []
|
77
|
+
|
78
|
+
@switches = switches.inject({}) do |mem, (name, type)|
|
79
|
+
if name.is_a?(Array)
|
80
|
+
name, *shorts = name
|
81
|
+
else
|
82
|
+
name = name.to_s
|
83
|
+
shorts = []
|
84
|
+
end
|
85
|
+
# we need both nice and dasherized form of switch name
|
86
|
+
if name.index('-') == 0
|
87
|
+
nice_name = undasherize name
|
88
|
+
else
|
89
|
+
nice_name = name
|
90
|
+
name = dasherize name
|
91
|
+
end
|
92
|
+
# if there are no shortcuts specified, generate one using the first character
|
93
|
+
shorts << "-" + nice_name[0,1] if shorts.empty? and nice_name.length > 1
|
94
|
+
shorts.each { |short| @shorts[short] = name }
|
95
|
+
|
96
|
+
# normalize type
|
97
|
+
case type
|
98
|
+
when TrueClass
|
99
|
+
@defaults[nice_name] = true
|
100
|
+
type = :boolean
|
101
|
+
when FalseClass
|
102
|
+
@defaults[nice_name] = false
|
103
|
+
type = :boolean
|
104
|
+
when String
|
105
|
+
@defaults[nice_name] = type
|
106
|
+
type = :optional
|
107
|
+
when Numeric
|
108
|
+
@defaults[nice_name] = type
|
109
|
+
type = :numeric
|
110
|
+
end
|
111
|
+
|
112
|
+
mem[name] = type
|
113
|
+
mem
|
114
|
+
end
|
115
|
+
|
116
|
+
# remove shortcuts that happen to coincide with any of the main switches
|
117
|
+
@shorts.keys.each do |short|
|
118
|
+
@shorts.delete(short) if @switches.key?(short)
|
119
|
+
end
|
120
|
+
end
|
76
121
|
|
77
|
-
|
78
|
-
|
122
|
+
def parse(args, skip_leading_non_opts = true)
|
123
|
+
@args = args
|
124
|
+
# start with Thor::Options::Hash pre-filled with defaults
|
125
|
+
hash = Hash.new @defaults
|
126
|
+
|
127
|
+
@leading_non_opts = []
|
128
|
+
if skip_leading_non_opts
|
129
|
+
@leading_non_opts << shift until current_is_option? || @args.empty?
|
130
|
+
end
|
131
|
+
|
132
|
+
while current_is_option?
|
133
|
+
case shift
|
79
134
|
when SHORT_SQ_RE
|
80
|
-
|
81
|
-
next
|
82
|
-
when LONG_EQ_RE
|
83
|
-
push($1, $2)
|
135
|
+
unshift $1.split('').map { |f| "-#{f}" }
|
84
136
|
next
|
137
|
+
when EQ_RE, SHORT_NUM
|
138
|
+
unshift $2
|
139
|
+
switch = $1
|
85
140
|
when LONG_RE, SHORT_RE
|
86
141
|
switch = $1
|
87
142
|
end
|
88
|
-
|
89
|
-
|
143
|
+
|
144
|
+
switch = normalize_switch(switch)
|
145
|
+
nice_name = undasherize(switch)
|
146
|
+
type = switch_type(switch)
|
147
|
+
|
148
|
+
case type
|
90
149
|
when :required
|
91
|
-
|
92
|
-
raise Error, "cannot pass switch '#{peek}' as an argument" if
|
93
|
-
hash[
|
94
|
-
when :boolean
|
95
|
-
hash[switch] = true
|
150
|
+
assert_value!(switch)
|
151
|
+
raise Error, "cannot pass switch '#{peek}' as an argument" if valid?(peek)
|
152
|
+
hash[nice_name] = shift
|
96
153
|
when :optional
|
97
|
-
|
98
|
-
|
99
|
-
|
154
|
+
hash[nice_name] = peek.nil? || valid?(peek) || shift
|
155
|
+
when :boolean
|
156
|
+
if !@switches.key?(switch) && nice_name =~ /^no-(\w+)$/
|
157
|
+
hash[$1] = false
|
158
|
+
else
|
159
|
+
hash[nice_name] = true
|
160
|
+
end
|
161
|
+
|
162
|
+
when :numeric
|
163
|
+
assert_value!(switch)
|
164
|
+
unless peek =~ NUMERIC and $& == peek
|
165
|
+
raise Error, "expected numeric value for '#{switch}'; got #{peek.inspect}"
|
166
|
+
end
|
167
|
+
hash[nice_name] = $&.index('.') ? shift.to_f : shift.to_i
|
100
168
|
end
|
101
169
|
end
|
170
|
+
|
171
|
+
@trailing_non_opts = @args
|
102
172
|
|
103
|
-
|
104
|
-
|
173
|
+
check_required! hash
|
174
|
+
hash.freeze
|
105
175
|
hash
|
106
176
|
end
|
107
|
-
|
108
|
-
def
|
109
|
-
|
110
|
-
|
111
|
-
|
177
|
+
|
178
|
+
def formatted_usage
|
179
|
+
return "" if @switches.empty?
|
180
|
+
@switches.map do |opt, type|
|
181
|
+
case type
|
182
|
+
when :boolean
|
183
|
+
"[#{opt}]"
|
184
|
+
when :required
|
185
|
+
opt + "=" + opt.gsub(/\-/, "").upcase
|
186
|
+
else
|
187
|
+
sample = @defaults[undasherize(opt)]
|
188
|
+
sample ||= case type
|
189
|
+
when :optional then undasherize(opt).gsub(/\-/, "_").upcase
|
190
|
+
when :numeric then "N"
|
191
|
+
end
|
192
|
+
"[" + opt + "=" + sample.to_s + "]"
|
112
193
|
end
|
113
|
-
end
|
194
|
+
end.join(" ")
|
114
195
|
end
|
196
|
+
|
197
|
+
alias :to_s :formatted_usage
|
115
198
|
|
116
199
|
private
|
117
|
-
|
200
|
+
|
201
|
+
def assert_value!(switch)
|
202
|
+
raise Error, "no value provided for argument '#{switch}'" if peek.nil?
|
203
|
+
end
|
204
|
+
|
205
|
+
def undasherize(str)
|
206
|
+
str.sub(/^-{1,2}/, '')
|
207
|
+
end
|
208
|
+
|
209
|
+
def dasherize(str)
|
210
|
+
(str.length > 1 ? "--" : "-") + str
|
211
|
+
end
|
212
|
+
|
118
213
|
def peek
|
119
214
|
@args.first
|
120
215
|
end
|
121
216
|
|
122
|
-
def
|
123
|
-
|
124
|
-
@args = @args[1..-1] || []
|
125
|
-
arg
|
217
|
+
def shift
|
218
|
+
@args.shift
|
126
219
|
end
|
127
220
|
|
128
|
-
def
|
129
|
-
|
221
|
+
def unshift(arg)
|
222
|
+
unless arg.kind_of?(Array)
|
223
|
+
@args.unshift(arg)
|
224
|
+
else
|
225
|
+
@args = arg + @args
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def valid?(arg)
|
230
|
+
if arg.to_s =~ /^--no-(\w+)$/
|
231
|
+
@switches.key?(arg) or (@switches["--#{$1}"] == :boolean)
|
232
|
+
else
|
233
|
+
@switches.key?(arg) or @shorts.key?(arg)
|
234
|
+
end
|
130
235
|
end
|
131
236
|
|
132
|
-
def
|
237
|
+
def current_is_option?
|
133
238
|
case peek
|
134
|
-
when LONG_RE, SHORT_RE,
|
135
|
-
|
239
|
+
when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
|
240
|
+
valid?($1)
|
136
241
|
when SHORT_SQ_RE
|
137
|
-
$1.split(
|
242
|
+
$1.split('').any? { |f| valid?("-#{f}") }
|
138
243
|
end
|
139
244
|
end
|
140
|
-
|
141
|
-
|
142
|
-
|
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}
|
245
|
+
|
246
|
+
def normalize_switch(switch)
|
247
|
+
@shorts.key?(switch) ? @shorts[switch] : switch
|
153
248
|
end
|
154
|
-
|
249
|
+
|
250
|
+
def switch_type(switch)
|
251
|
+
if switch =~ /^--no-(\w+)$/
|
252
|
+
@switches[switch] || @switches["--#{$1}"]
|
253
|
+
else
|
254
|
+
@switches[switch]
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def check_required!(hash)
|
259
|
+
for name, type in @switches
|
260
|
+
if type == :required and !hash[undasherize(name)]
|
261
|
+
raise Error, "no value provided for required argument '#{name}'"
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
155
266
|
end
|
156
267
|
end
|
data/lib/thor/runner.rb
CHANGED
@@ -18,8 +18,17 @@ class Thor::Runner < Thor
|
|
18
18
|
method_options :as => :optional, :relative => :boolean
|
19
19
|
def install(name)
|
20
20
|
initialize_thorfiles
|
21
|
+
|
22
|
+
base = name
|
23
|
+
package = :file
|
24
|
+
|
21
25
|
begin
|
22
|
-
|
26
|
+
if File.directory?(File.expand_path(name))
|
27
|
+
base, package = File.join(name, "main.thor"), :directory
|
28
|
+
contents = open(base).read
|
29
|
+
else
|
30
|
+
contents = open(name).read
|
31
|
+
end
|
23
32
|
rescue OpenURI::HTTPError
|
24
33
|
raise Error, "Error opening URI `#{name}'"
|
25
34
|
rescue Errno::ENOENT
|
@@ -35,9 +44,7 @@ class Thor::Runner < Thor
|
|
35
44
|
|
36
45
|
return false unless response =~ /^\s*y/i
|
37
46
|
|
38
|
-
constants = Thor::Util.constants_in_contents(contents)
|
39
|
-
|
40
|
-
# name = name =~ /\.thor$/ || is_uri ? name : "#{name}.thor"
|
47
|
+
constants = Thor::Util.constants_in_contents(contents, base)
|
41
48
|
|
42
49
|
as = options["as"] || begin
|
43
50
|
first_line = contents.split("\n")[0]
|
@@ -63,8 +70,12 @@ class Thor::Runner < Thor
|
|
63
70
|
|
64
71
|
puts "Storing thor file in your system repository"
|
65
72
|
|
66
|
-
File.
|
67
|
-
|
73
|
+
destination = File.join(thor_root, yaml[as][:filename])
|
74
|
+
|
75
|
+
if package == :file
|
76
|
+
File.open(destination, "w") {|f| f.puts contents }
|
77
|
+
else
|
78
|
+
FileUtils.cp_r(name, destination)
|
68
79
|
end
|
69
80
|
|
70
81
|
yaml[as][:filename] # Indicate sucess
|
@@ -78,7 +89,7 @@ class Thor::Runner < Thor
|
|
78
89
|
puts "Uninstalling #{name}."
|
79
90
|
|
80
91
|
file = File.join(thor_root, "#{yaml[name][:filename]}")
|
81
|
-
|
92
|
+
FileUtils.rm_rf(file)
|
82
93
|
yaml.delete(name)
|
83
94
|
save_yaml(yaml)
|
84
95
|
|
@@ -92,7 +103,7 @@ class Thor::Runner < Thor
|
|
92
103
|
|
93
104
|
puts "Updating `#{name}' from #{yaml[name][:location]}"
|
94
105
|
old_filename = yaml[name][:filename]
|
95
|
-
options
|
106
|
+
self.options = self.options.merge("as" => name)
|
96
107
|
filename = install(yaml[name][:location])
|
97
108
|
unless filename == old_filename
|
98
109
|
File.delete(File.join(thor_root, old_filename))
|
@@ -102,7 +113,7 @@ class Thor::Runner < Thor
|
|
102
113
|
desc "installed", "list the installed Thor modules and tasks (--internal means list the built-in tasks as well)"
|
103
114
|
method_options :internal => :boolean
|
104
115
|
def installed
|
105
|
-
|
116
|
+
thor_root_glob.each do |f|
|
106
117
|
next if f =~ /thor\.yml$/
|
107
118
|
load_thorfile f unless Thor.subclass_files.keys.include?(File.expand_path(f))
|
108
119
|
end
|
@@ -113,14 +124,21 @@ class Thor::Runner < Thor
|
|
113
124
|
end
|
114
125
|
|
115
126
|
desc "list [SEARCH]", "list the available thor tasks (--substring means SEARCH can be anywhere in the module)"
|
116
|
-
method_options :substring => :boolean
|
127
|
+
method_options :substring => :boolean,
|
128
|
+
:group => :optional,
|
129
|
+
:all => :boolean,
|
130
|
+
['--descriptions', '-D'] => :boolean
|
117
131
|
def list(search = "")
|
118
132
|
initialize_thorfiles
|
119
133
|
search = ".*#{search}" if options["substring"]
|
120
134
|
search = /^#{search}.*/i
|
121
|
-
|
122
|
-
|
123
|
-
|
135
|
+
group = options[:group] || 'standard'
|
136
|
+
|
137
|
+
classes = Thor.subclasses.select do |k|
|
138
|
+
(options[:all] || k.group_name == group) &&
|
139
|
+
Thor::Util.constant_to_thor_path(k.name) =~ search
|
140
|
+
end
|
141
|
+
display_klasses(false, classes, options.descriptions?)
|
124
142
|
end
|
125
143
|
|
126
144
|
# Override Thor#help so we can give info about not-yet-loaded tasks
|
@@ -141,11 +159,27 @@ class Thor::Runner < Thor
|
|
141
159
|
def self.thor_root
|
142
160
|
File.join(ENV["HOME"] || ENV["APPDATA"], ".thor")
|
143
161
|
end
|
162
|
+
|
163
|
+
def self.thor_root_glob
|
164
|
+
# On Windows thor_root will be something like this:
|
165
|
+
#
|
166
|
+
# C:\Documents and Settings\james\.thor
|
167
|
+
#
|
168
|
+
# If we don't #gsub the \ character, Dir.glob will fail.
|
169
|
+
files = Dir["#{thor_root.gsub(/\\/, '/')}/*"]
|
170
|
+
files.map! do |file|
|
171
|
+
File.directory?(file) ? File.join(file, "main.thor") : file
|
172
|
+
end
|
173
|
+
end
|
144
174
|
|
145
175
|
private
|
146
176
|
def thor_root
|
147
177
|
self.class.thor_root
|
148
178
|
end
|
179
|
+
|
180
|
+
def thor_root_glob
|
181
|
+
self.class.thor_root_glob
|
182
|
+
end
|
149
183
|
|
150
184
|
def thor_yaml
|
151
185
|
yaml_file = File.join(thor_root, "thor.yml")
|
@@ -158,12 +192,12 @@ class Thor::Runner < Thor
|
|
158
192
|
File.open(yaml_file, "w") {|f| f.puts yaml.to_yaml }
|
159
193
|
end
|
160
194
|
|
161
|
-
def display_klasses(with_modules = false, klasses = Thor.subclasses)
|
195
|
+
def display_klasses(with_modules = false, klasses = Thor.subclasses, show_descriptions = false)
|
162
196
|
klasses -= [Thor, Thor::Runner] unless with_modules
|
163
197
|
raise Error, "No Thor tasks available" if klasses.empty?
|
164
198
|
|
165
199
|
if with_modules && !(yaml = thor_yaml).empty?
|
166
|
-
max_name = yaml.max {|(xk,xv),(yk,yv)| xk.size <=> yk.size }.first.size
|
200
|
+
max_name = yaml.max {|(xk,xv),(yk,yv)| xk.to_s.size <=> yk.to_s.size }.first.size
|
167
201
|
modules_label = "Modules"
|
168
202
|
namespaces_label = "Namespaces"
|
169
203
|
column_width = [max_name + 4, modules_label.size + 1].max
|
@@ -181,30 +215,41 @@ class Thor::Runner < Thor
|
|
181
215
|
puts
|
182
216
|
end
|
183
217
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
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
|
218
|
+
unless klasses.empty?
|
219
|
+
puts # add some spacing
|
220
|
+
klasses.each { |klass| display_tasks(klass, show_descriptions) }
|
221
|
+
else
|
222
|
+
puts "\033[1;34mNo Thor tasks available\033[0m"
|
195
223
|
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
224
|
end
|
201
225
|
|
202
|
-
def display_tasks(klass,
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
226
|
+
def display_tasks(klass, show_descriptions)
|
227
|
+
if klass.tasks.values.length > 1
|
228
|
+
|
229
|
+
base = Thor::Util.constant_to_thor_path(klass.name)
|
230
|
+
|
231
|
+
if base.to_a.empty?
|
232
|
+
base = 'default'
|
233
|
+
puts "\033[1;35m#{base}\033[0m"
|
234
|
+
else
|
235
|
+
puts "\033[1;34m#{base}\033[0m"
|
236
|
+
end
|
237
|
+
puts "-" * base.length
|
238
|
+
|
239
|
+
klass.tasks.each true do |name, task|
|
240
|
+
puts task.formatted_usage(true)
|
241
|
+
if show_descriptions
|
242
|
+
puts ' ' * 4 + task.description
|
243
|
+
puts
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
puts unless show_descriptions
|
248
|
+
|
249
|
+
unless klass.opts.empty?
|
250
|
+
puts "\nglobal options: #{Options.new(klass.opts)}"
|
251
|
+
puts # add some spacing
|
252
|
+
end
|
208
253
|
end
|
209
254
|
end
|
210
255
|
|
@@ -213,8 +258,9 @@ class Thor::Runner < Thor
|
|
213
258
|
end
|
214
259
|
|
215
260
|
def load_thorfile(path)
|
261
|
+
txt = File.read(path)
|
216
262
|
begin
|
217
|
-
|
263
|
+
Thor::Tasks.class_eval txt, path
|
218
264
|
rescue Object => e
|
219
265
|
$stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}"
|
220
266
|
end
|
@@ -233,8 +279,12 @@ class Thor::Runner < Thor
|
|
233
279
|
|
234
280
|
# We want to load system-wide Thorfiles first
|
235
281
|
# so the local Thorfiles will override them.
|
236
|
-
(relevant_to ? thorfiles_relevant_to(relevant_to) :
|
237
|
-
|
282
|
+
files = (relevant_to ? thorfiles_relevant_to(relevant_to) :
|
283
|
+
thor_root_glob) + thorfiles - ["#{thor_root}/thor.yml"]
|
284
|
+
|
285
|
+
files.map! do |file|
|
286
|
+
File.directory?(file) ? File.join(file, "main.thor") : file
|
287
|
+
end
|
238
288
|
end
|
239
289
|
|
240
290
|
def thorfiles_relevant_to(meth)
|
data/lib/thor/task.rb
CHANGED
@@ -3,9 +3,16 @@ require 'thor/util'
|
|
3
3
|
|
4
4
|
class Thor
|
5
5
|
class Task < Struct.new(:meth, :description, :usage, :opts, :klass)
|
6
|
+
|
6
7
|
def self.dynamic(meth, klass)
|
7
8
|
new(meth, "A dynamically-generated task", meth.to_s, nil, klass)
|
8
9
|
end
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
# keep the original opts - we need them later on
|
13
|
+
@options = args[3] || {}
|
14
|
+
super
|
15
|
+
end
|
9
16
|
|
10
17
|
def parse(obj, args)
|
11
18
|
list, hash = parse_args(args)
|
@@ -17,14 +24,18 @@ class Thor
|
|
17
24
|
raise NoMethodError, "the `#{meth}' task of #{obj.class} is private" if
|
18
25
|
(obj.private_methods + obj.protected_methods).include?(meth)
|
19
26
|
|
20
|
-
obj.
|
27
|
+
obj.invoke(meth, *params)
|
21
28
|
rescue ArgumentError => e
|
29
|
+
|
22
30
|
# backtrace sans anything in this file
|
23
31
|
backtrace = e.backtrace.reject {|frame| frame =~ /^#{Regexp.escape(__FILE__)}/}
|
32
|
+
# also nix anything in thor.rb
|
33
|
+
backtrace = backtrace.reject { |frame| frame =~ /\/thor.rb/ }
|
34
|
+
|
24
35
|
# and sans anything that got us here
|
25
36
|
backtrace -= caller
|
26
37
|
raise e unless backtrace.empty?
|
27
|
-
|
38
|
+
|
28
39
|
# okay, they really did call it wrong
|
29
40
|
raise Error, "`#{meth}' was called incorrectly. Call as `#{formatted_usage}'"
|
30
41
|
rescue NoMethodError => e
|
@@ -45,35 +56,27 @@ class Thor
|
|
45
56
|
new.klass = klass
|
46
57
|
new
|
47
58
|
end
|
48
|
-
|
49
|
-
def
|
50
|
-
return
|
51
|
-
|
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(" ")
|
59
|
+
|
60
|
+
def opts
|
61
|
+
return super unless super.kind_of? Hash
|
62
|
+
@_opts ||= Options.new(super)
|
61
63
|
end
|
62
|
-
|
64
|
+
|
65
|
+
def full_opts
|
66
|
+
@_full_opts ||= Options.new((klass.opts || {}).merge(@options))
|
67
|
+
end
|
68
|
+
|
63
69
|
def formatted_usage(namespace = false)
|
64
70
|
(namespace ? self.namespace + ':' : '') + usage +
|
65
|
-
(opts ? " " +
|
71
|
+
(opts ? " " + opts.formatted_usage : "")
|
66
72
|
end
|
67
73
|
|
68
74
|
protected
|
69
75
|
|
70
76
|
def parse_args(args)
|
71
|
-
return [
|
72
|
-
|
73
|
-
|
74
|
-
list = options.skip_non_opts
|
75
|
-
hash.update options.getopts(false)
|
76
|
-
options.check_required_args hash
|
77
|
+
return [[], {}] if args.nil?
|
78
|
+
hash = full_opts.parse(args)
|
79
|
+
list = full_opts.non_opts
|
77
80
|
[list, hash]
|
78
81
|
end
|
79
82
|
end
|
data/lib/thor/tasks.rb
CHANGED
@@ -13,13 +13,16 @@ class Thor
|
|
13
13
|
|
14
14
|
def self.install_task(spec)
|
15
15
|
package_task spec
|
16
|
+
|
17
|
+
null, sudo, gem = RUBY_PLATFORM =~ /w(in)?32$/ ? ['NUL', '', 'gem.bat'] :
|
18
|
+
['/dev/null', 'sudo', 'gem']
|
16
19
|
|
17
20
|
desc "install", "install the gem"
|
18
21
|
define_method :install do
|
19
|
-
old_stderr, $stderr = $stderr.dup, File.open(
|
22
|
+
old_stderr, $stderr = $stderr.dup, File.open(null, "w")
|
20
23
|
package
|
21
24
|
$stderr = old_stderr
|
22
|
-
system %{sudo gem install pkg/#{spec.name}-#{spec.version} --no-rdoc --no-ri --no-update-sources}
|
25
|
+
system %{#{sudo} #{Gem.ruby} -S #{gem} install pkg/#{spec.name}-#{spec.version} --no-rdoc --no-ri --no-update-sources}
|
23
26
|
end
|
24
27
|
end
|
25
28
|
|
data/lib/thor/util.rb
CHANGED
@@ -1,10 +1,38 @@
|
|
1
1
|
require 'thor/error'
|
2
2
|
|
3
|
+
module ObjectSpace
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
# @return <Array[Class]> All the classes in the object space.
|
8
|
+
def classes
|
9
|
+
klasses = []
|
10
|
+
ObjectSpace.each_object(Class) {|o| klasses << o}
|
11
|
+
klasses
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
3
17
|
class Thor
|
18
|
+
module Tasks; end
|
19
|
+
|
4
20
|
module Util
|
5
21
|
|
22
|
+
def self.full_const_get(obj, name)
|
23
|
+
list = name.split("::")
|
24
|
+
list.shift if list.first.empty?
|
25
|
+
list.each do |x|
|
26
|
+
# This is required because const_get tries to look for constants in the
|
27
|
+
# ancestor chain, but we only want constants that are HERE
|
28
|
+
obj = obj.const_defined?(x) ? obj.const_get(x) : obj.const_missing(x)
|
29
|
+
end
|
30
|
+
obj
|
31
|
+
end
|
32
|
+
|
6
33
|
def self.constant_to_thor_path(str, remove_default = true)
|
7
|
-
str =
|
34
|
+
str = str.to_s.gsub(/^Thor::Tasks::/, "")
|
35
|
+
str = snake_case(str).squeeze(":")
|
8
36
|
str.gsub!(/^default/, '') if remove_default
|
9
37
|
str
|
10
38
|
end
|
@@ -21,16 +49,20 @@ class Thor
|
|
21
49
|
str.gsub(/:(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
22
50
|
end
|
23
51
|
|
24
|
-
def self.constants_in_contents(str)
|
25
|
-
klasses =
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
52
|
+
def self.constants_in_contents(str, file = __FILE__)
|
53
|
+
klasses = ObjectSpace.classes.dup
|
54
|
+
Module.new.class_eval(str, file)
|
55
|
+
klasses = ObjectSpace.classes - klasses
|
56
|
+
klasses = klasses.select {|k| k < Thor }
|
57
|
+
klasses.map! {|k| k.to_s.gsub(/#<Module:\w+>::/, '')}
|
30
58
|
end
|
31
59
|
|
32
|
-
def self.make_constant(str)
|
33
|
-
|
60
|
+
def self.make_constant(str, base = [Thor::Tasks, Object])
|
61
|
+
which = base.find do |obj|
|
62
|
+
full_const_get(obj, str) rescue nil
|
63
|
+
end
|
64
|
+
return full_const_get(which, str) if which
|
65
|
+
raise NameError, "uninitialized constant #{str}"
|
34
66
|
end
|
35
67
|
|
36
68
|
def self.snake_case(str)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mislav-thor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yehuda Katz
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date:
|
12
|
+
date: 2009-01-27 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -23,9 +23,11 @@ extensions: []
|
|
23
23
|
extra_rdoc_files:
|
24
24
|
- README.markdown
|
25
25
|
- LICENSE
|
26
|
+
- CHANGELOG.rdoc
|
26
27
|
files:
|
27
|
-
- LICENSE
|
28
28
|
- README.markdown
|
29
|
+
- LICENSE
|
30
|
+
- CHANGELOG.rdoc
|
29
31
|
- Rakefile
|
30
32
|
- bin/rake2thor
|
31
33
|
- bin/thor
|