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 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 # [4]
14
- def install(name, opts)
15
- ... code ...
16
- if opts[:force]
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
- MyApp.start
29
-
30
- Thor automatically maps commands as follows:
28
+ Thor automatically maps commands as such:
31
29
 
32
- app install name --force
30
+ app install myname --force
33
31
 
34
32
  That gets converted to:
35
33
 
36
- MyApp.new.install("name", :force => true)
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 -- and - params.
44
- In this case, a --force and a -f option is added.
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
- <dd>true if the option is passed</dd>
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
- <dd>the value for this option MUST be provided</dd>
54
+ <dd>the value for this option MUST be provided</dd>
54
55
  <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>
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
@@ -2,5 +2,5 @@ task :default => :install
2
2
 
3
3
  desc "install the gem locally"
4
4
  task :install do
5
- sh %{ruby #{File.dirname(__FILE__)}/bin/thor :install}
5
+ sh %{ruby "#{File.dirname(__FILE__)}/bin/thor" :install}
6
6
  end
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 = opts.inject({}) do |accum, (k,v)|
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.formatted_opts}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
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
- options = Thor::Options.new(args, self.opts)
65
- opts = options.getopts
66
- args = options.args
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 ||= "help"
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
- 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
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::ong class,
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
- 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]
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
- @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
14
+
15
+ def [](key)
16
+ super convert_key(key)
44
17
  end
45
- @syns = switches.inject({}) do |h, (forms,_)|
46
- forms.each {|f| h[f] ||= forms}
47
- h
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
- def skip_non_opts
52
- non_opts = []
53
- non_opts << pop until looking_at_opt? || @args.empty?
54
- non_opts
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
- # opts = Thor::Options.new(args,
69
- # "--debug" => true,
70
- # ["--verbose", "-v"] => true,
71
- # ["--level", "-l"] => :numeric
72
- # ).getopts
66
+ # opts = Thor::Options.new(
67
+ # "--debug" => true,
68
+ # ["--verbose", "-v"] => true,
69
+ # ["--level", "-l"] => :numeric
70
+ # ).parse(args)
73
71
  #
74
- def getopts(check_required = true)
75
- hash = @defaults.dup
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
- while looking_at_opt?
78
- case pop
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
- push(*$1.split("").map {|s| s = "-#{s}"})
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
- case @types[switch]
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
- 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
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
- # 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
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
- hash = normalize_hash hash
104
- check_required_args hash if check_required
173
+ check_required! hash
174
+ hash.freeze
105
175
  hash
106
176
  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}'"
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 pop
123
- arg = peek
124
- @args = @args[1..-1] || []
125
- arg
217
+ def shift
218
+ @args.shift
126
219
  end
127
220
 
128
- def push(*args)
129
- @args = args + @args
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 looking_at_opt?
237
+ def current_is_option?
133
238
  case peek
134
- when LONG_RE, SHORT_RE, LONG_EQ_RE
135
- @valid.include? $1
239
+ when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
240
+ valid?($1)
136
241
  when SHORT_SQ_RE
137
- $1.split("").any? {|f| @valid.include? "-#{f}"}
242
+ $1.split('').any? { |f| valid?("-#{f}") }
138
243
  end
139
244
  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}
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
- contents = open(name).read
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.open(File.join(thor_root, yaml[as][:filename]), "w") do |file|
67
- file.puts contents
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
- File.delete(file)
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["as"] = name
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
- Dir["#{thor_root}/**/*"].each do |f|
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
- display_klasses(false, Thor.subclasses.select {|k|
123
- Thor::Util.constant_to_thor_path(k.name) =~ search})
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
- 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
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, 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
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
- load path
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
- Dir["#{thor_root}/**/*"]) + thorfiles - ["#{thor_root}/thor.yml"]
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.send(meth, *params)
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 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(" ")
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 ? " " + formatted_opts : "")
71
+ (opts ? " " + opts.formatted_usage : "")
66
72
  end
67
73
 
68
74
  protected
69
75
 
70
76
  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
+ 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("/dev/null", "w")
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 = snake_case(str.to_s).squeeze(":")
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 = self.constants.dup
26
- eval(str)
27
- ret = self.constants - klasses
28
- ret.each {|k| self.send(:remove_const, k)}
29
- ret
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
- list = str.split("::").inject(Object) {|obj, x| obj.const_get(x)}
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.5
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: 2008-08-25 00:00:00 -07:00
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