mislav-thor 0.9.5 → 0.9.10

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/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