wycats-thor 0.9.2 → 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,35 @@
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.5, released 2008-08-27
9
+
10
+ * Improve Windows compatibility
11
+ * Update (incorrect) README and task.thor sample file
12
+ * Options hash is now frozen (once returned)
13
+ * Allow magic predicates on options object. For instance: `options.force?`
14
+ * Add support for :numeric type
15
+ * BACKWARDS INCOMPATIBLE: Refactor Thor::Options. You cannot access shorthand forms in options hash anymore (for instance, options[:f])
16
+ * Allow specifying optional args with default values: method_options(:user => "mislav")
17
+ * Don't write options for nil or false values. This allows, for example, turning color off when running specs.
18
+ * Exit with the status of the spec command to help CI stuff out some.
19
+
20
+ == 0.9.4, released 2008-08-13
21
+
22
+ * Try to add Windows compatibility.
23
+ * BACKWARDS INCOMPATIBLE: options hash is now accessed as a property in your class and is not passed as last argument anymore
24
+ * Allow options at the beginning of the argument list as well as the end.
25
+ * Make options available with symbol keys in addition to string keys.
26
+ * Allow true to be passed to Thor#method_options to denote a boolean option.
27
+ * If loading a thor file fails, don't give up, just print a warning and keep going.
28
+ * Make sure that we re-raise errors if they happened further down the pipe than we care about.
29
+ * Only delete the old file on updating when the installation of the new one is a success
30
+ * Make it Ruby 1.8.5 compatible.
31
+ * Don't raise an error if a boolean switch is defined multiple times.
32
+ * Thor::Options now doesn't parse through things that look like options but aren't.
33
+ * Add URI detection to install task, and make sure we don't append ".thor" to URIs
34
+ * Add rake2thor to the gem binfiles.
35
+ * Make sure local Thorfiles override system-wide ones.
@@ -4,56 +4,73 @@ thor
4
4
  Map options to a class. Simply create a class with the appropriate annotations, and have options automatically map
5
5
  to functions and parameters.
6
6
 
7
- Examples:
7
+ Example:
8
8
 
9
- class MyApp < Thor # [1]
10
- map "-L" => :list # [2]
11
-
12
- desc "install APP_NAME", "install one of the available apps" # [3]
13
- method_options :force => :boolean # [4]
14
- def install(name, opts)
15
- ... code ...
16
- if opts[:force]
9
+ class MyApp < Thor # [1]
10
+ map "-L" => :list # [2]
11
+
12
+ desc "install APP_NAME", "install one of the available apps" # [3]
13
+ method_options :force => :boolean, :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)
37
-
38
- [1] Inherit from Thor to turn a class into an option mapper
34
+ MyApp.new.install("myname")
35
+ # with {'force' => true} as options hash
39
36
 
40
- [2] Map additional non-valid identifiers to specific methods. In this case,
37
+ 1. Inherit from Thor to turn a class into an option mapper
38
+ 2. Map additional non-valid identifiers to specific methods. In this case,
41
39
  convert -L to :list
42
-
43
- [3] Describe the method immediately below. The first parameter is the usage information,
40
+ 3. Describe the method immediately below. The first parameter is the usage information,
44
41
  and the second parameter is the description.
45
-
46
- [4] Provide any additional options. These will be marshaled from -- and - params.
47
- In this case, a --force and a -f option is added.
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.
48
44
 
49
45
  Types for `method_options`
50
46
  --------------------------
51
47
 
52
48
  <dl>
53
- <dt>:boolean</dt>
54
- <dd>true if the option is passed</dd>
55
- <dt>:required</dt>
56
- <dd>A key/value option that MUST be provided</dd>
57
- <dt>:optional</dt>
58
- <dd>A key/value option that MAY be provided</dd>
59
- </dl>
49
+ <dt><code>:boolean</code></dt>
50
+ <dd>true if the option is passed</dd>
51
+ <dt><code>true</code></dt>
52
+ <dd>same as <code>:boolean</code></dd>
53
+ <dt><code>:required</code></dt>
54
+ <dd>the value for this option MUST be provided</dd>
55
+ <dt><code>:optional</code></dt>
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
@@ -1,10 +1,12 @@
1
1
  $:.unshift File.expand_path(File.dirname(__FILE__))
2
- require "getopt"
2
+ require "thor/options"
3
3
  require "thor/util"
4
4
  require "thor/task"
5
5
  require "thor/task_hash"
6
6
 
7
7
  class Thor
8
+ attr_accessor :options
9
+
8
10
  def self.map(map)
9
11
  @map ||= superclass.instance_variable_get("@map") || {}
10
12
  map.each do |key, value|
@@ -21,9 +23,7 @@ class Thor
21
23
  end
22
24
 
23
25
  def self.method_options(opts)
24
- @method_options = opts.inject({}) do |accum, (k,v)|
25
- accum.merge("--" + k.to_s => v.to_s.upcase)
26
- end
26
+ @method_options = opts
27
27
  end
28
28
 
29
29
  def self.subclass_files
@@ -38,6 +38,10 @@ class Thor
38
38
  @tasks ||= TaskHash.new(self)
39
39
  end
40
40
 
41
+ def self.opts
42
+ (@opts || {}).merge(self == Thor ? {} : superclass.opts)
43
+ end
44
+
41
45
  def self.[](task)
42
46
  namespaces = task.split(":")
43
47
  klass = Thor::Util.constant_from_thor_path(namespaces[0...-1].join(":"))
@@ -49,17 +53,21 @@ class Thor
49
53
  @maxima ||= begin
50
54
  max_usage = tasks.map {|_, t| t.usage}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
51
55
  max_desc = tasks.map {|_, t| t.description}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
52
- max_opts = tasks.map {|_, t| t.formatted_opts}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
56
+ max_opts = tasks.map {|_, t| t.opts ? t.opts.formatted_usage : ""}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
53
57
  Struct.new(:description, :usage, :opt).new(max_desc, max_usage, max_opts)
54
58
  end
55
59
  end
56
60
 
57
61
  def self.start(args = ARGV)
62
+ options = Thor::Options.new(self.opts)
63
+ opts = options.parse(args, false)
64
+ args = options.trailing_non_opts
65
+
58
66
  meth = args.first
59
67
  meth = @map[meth].to_s if @map && @map[meth]
60
68
  meth ||= "help"
61
69
 
62
- tasks[meth].parse args[1..-1]
70
+ tasks[meth].parse new(opts, *args), args[1..-1]
63
71
  rescue Thor::Error => e
64
72
  $stderr.puts e.message
65
73
  end
@@ -72,6 +80,13 @@ class Thor
72
80
 
73
81
  def method_added(meth)
74
82
  meth = meth.to_s
83
+
84
+ if meth == "initialize"
85
+ @opts = @method_options
86
+ @method_options = nil
87
+ return
88
+ end
89
+
75
90
  return if !public_instance_methods.include?(meth) || !@usage
76
91
  register_klass_file self
77
92
 
@@ -91,6 +106,9 @@ class Thor
91
106
  subclasses << klass unless subclasses.include?(klass)
92
107
  end
93
108
  end
109
+
110
+ def initialize(opts = {}, *args)
111
+ end
94
112
 
95
113
  map ["-h", "-?", "--help", "-D"] => :help
96
114
 
@@ -106,15 +124,14 @@ class Thor
106
124
 
107
125
  puts task.formatted_usage(namespace)
108
126
  puts task.description
109
- return
110
- end
111
-
112
- puts "Options"
113
- puts "-------"
114
- self.class.tasks.each do |_, task|
115
- format = "%-" + (self.class.maxima.usage + self.class.maxima.opt + 4).to_s + "s"
116
- print format % ("#{task.formatted_usage}")
117
- puts task.description.split("\n").first
127
+ else
128
+ puts "Options"
129
+ puts "-------"
130
+ self.class.tasks.each do |_, task|
131
+ format = "%-" + (self.class.maxima.usage + self.class.maxima.opt + 4).to_s + "s"
132
+ print format % ("#{task.formatted_usage}")
133
+ puts task.description.split("\n").first
134
+ end
118
135
  end
119
136
  end
120
137
 
@@ -0,0 +1,238 @@
1
+ # This is a modified version of Daniel Berger's Getopt::Long class,
2
+ # licensed under Ruby's license.
3
+
4
+ class Thor
5
+ class Options
6
+ class Error < StandardError; end
7
+
8
+ # simple Hash with indifferent access
9
+ class Hash < ::Hash
10
+ def initialize(hash)
11
+ super()
12
+ update hash
13
+ end
14
+
15
+ def [](key)
16
+ super convert_key(key)
17
+ end
18
+
19
+ def values_at(*indices)
20
+ indices.collect { |key| self[convert_key(key)] }
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.to_s =~ /^(\w+)\?$/ ? !!self[$1] : super
32
+ end
33
+ end
34
+
35
+ NUMERIC = /(\d*\.\d+|\d+)/
36
+ LONG_RE = /^(--\w+[-\w+]*)$/
37
+ SHORT_RE = /^(-[a-z])$/i
38
+ EQ_RE = /^(--\w+[-\w+]*|-[a-z])=(.*)$/i
39
+ SHORT_SQ_RE = /^-([a-z]{2,})$/i # Allow either -x -v or -xv style for single char args
40
+ SHORT_NUM = /^(-[a-z])#{NUMERIC}$/i
41
+
42
+ attr_reader :leading_non_opts, :trailing_non_opts
43
+
44
+ def non_opts
45
+ leading_non_opts + trailing_non_opts
46
+ end
47
+
48
+ # Takes an array of switches. Each array consists of up to three
49
+ # elements that indicate the name and type of switch. Returns a hash
50
+ # containing each switch name, minus the '-', as a key. The value
51
+ # for each key depends on the type of switch and/or the value provided
52
+ # by the user.
53
+ #
54
+ # The long switch _must_ be provided. The short switch defaults to the
55
+ # first letter of the short switch. The default type is :boolean.
56
+ #
57
+ # Example:
58
+ #
59
+ # opts = Thor::Options.new(
60
+ # "--debug" => true,
61
+ # ["--verbose", "-v"] => true,
62
+ # ["--level", "-l"] => :numeric
63
+ # ).parse(args)
64
+ #
65
+ def initialize(switches)
66
+ @defaults = {}
67
+ @shorts = {}
68
+
69
+ @switches = switches.inject({}) do |mem, (name, type)|
70
+ if name.is_a?(Array)
71
+ name, *shorts = name
72
+ else
73
+ name = name.to_s
74
+ shorts = []
75
+ end
76
+ # we need both nice and dasherized form of switch name
77
+ if name.index('-') == 0
78
+ nice_name = undasherize name
79
+ else
80
+ nice_name = name
81
+ name = dasherize name
82
+ end
83
+ # if there are no shortcuts specified, generate one using the first character
84
+ shorts << "-" + nice_name[0,1] if shorts.empty? and nice_name.length > 1
85
+ shorts.each { |short| @shorts[short] = name }
86
+
87
+ # normalize type
88
+ case type
89
+ when TrueClass then type = :boolean
90
+ when String
91
+ @defaults[nice_name] = type
92
+ type = :optional
93
+ when Numeric
94
+ @defaults[nice_name] = type
95
+ type = :numeric
96
+ end
97
+
98
+ mem[name] = type
99
+ mem
100
+ end
101
+
102
+ # remove shortcuts that happen to coincide with any of the main switches
103
+ @shorts.keys.each do |short|
104
+ @shorts.delete(short) if @switches.key?(short)
105
+ end
106
+ end
107
+
108
+ def parse(args, skip_leading_non_opts = true)
109
+ @args = args
110
+ # start with Thor::Options::Hash pre-filled with defaults
111
+ hash = Hash.new @defaults
112
+
113
+ @leading_non_opts = []
114
+ if skip_leading_non_opts
115
+ @leading_non_opts << shift until current_is_option? || @args.empty?
116
+ end
117
+
118
+ while current_is_option?
119
+ case shift
120
+ when SHORT_SQ_RE
121
+ unshift $1.split('').map { |f| "-#{f}" }
122
+ next
123
+ when EQ_RE, SHORT_NUM
124
+ unshift $2
125
+ switch = $1
126
+ when LONG_RE, SHORT_RE
127
+ switch = $1
128
+ end
129
+
130
+ switch = normalize_switch(switch)
131
+ nice_name = undasherize(switch)
132
+ type = switch_type(switch)
133
+
134
+ case type
135
+ when :required
136
+ assert_value!(switch)
137
+ raise Error, "cannot pass switch '#{peek}' as an argument" if valid?(peek)
138
+ hash[nice_name] = shift
139
+ when :optional
140
+ hash[nice_name] = peek.nil? || valid?(peek) || shift
141
+ when :boolean
142
+ hash[nice_name] = true
143
+ when :numeric
144
+ assert_value!(switch)
145
+ unless peek =~ NUMERIC and $& == peek
146
+ raise Error, "expected numeric value for '#{switch}'; got #{peek.inspect}"
147
+ end
148
+ hash[nice_name] = $&.index('.') ? shift.to_f : shift.to_i
149
+ end
150
+ end
151
+
152
+ @trailing_non_opts = @args
153
+
154
+ check_required! hash
155
+ hash.freeze
156
+ hash
157
+ end
158
+
159
+ def formatted_usage
160
+ return "" if @switches.empty?
161
+ @switches.map do |opt, type|
162
+ case type
163
+ when :boolean
164
+ "[#{opt}]"
165
+ when :required
166
+ opt + "=" + opt.gsub(/\-/, "").upcase
167
+ else
168
+ sample = @defaults[undasherize(opt)]
169
+ sample ||= case type
170
+ when :optional then opt.gsub(/\-/, "").upcase
171
+ when :numeric then "N"
172
+ end
173
+ "[" + opt + "=" + sample.to_s + "]"
174
+ end
175
+ end.join(" ")
176
+ end
177
+
178
+ private
179
+
180
+ def assert_value!(switch)
181
+ raise Error, "no value provided for argument '#{switch}'" if peek.nil?
182
+ end
183
+
184
+ def undasherize(str)
185
+ str.sub(/^-{1,2}/, '')
186
+ end
187
+
188
+ def dasherize(str)
189
+ (str.length > 1 ? "--" : "-") + str
190
+ end
191
+
192
+ def peek
193
+ @args.first
194
+ end
195
+
196
+ def shift
197
+ @args.shift
198
+ end
199
+
200
+ def unshift(arg)
201
+ unless arg.kind_of?(Array)
202
+ @args.unshift(arg)
203
+ else
204
+ @args = arg + @args
205
+ end
206
+ end
207
+
208
+ def valid?(arg)
209
+ @switches.key?(arg) or @shorts.key?(arg)
210
+ end
211
+
212
+ def current_is_option?
213
+ case peek
214
+ when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
215
+ valid?($1)
216
+ when SHORT_SQ_RE
217
+ $1.split('').any? { |f| valid?("-#{f}") }
218
+ end
219
+ end
220
+
221
+ def normalize_switch(switch)
222
+ @shorts.key?(switch) ? @shorts[switch] : switch
223
+ end
224
+
225
+ def switch_type(switch)
226
+ @switches[switch]
227
+ end
228
+
229
+ def check_required!(hash)
230
+ for name, type in @switches
231
+ if type == :required and !hash[undasherize(name)]
232
+ raise Error, "no value provided for required argument '#{name}'"
233
+ end
234
+ end
235
+ end
236
+
237
+ end
238
+ end
@@ -15,8 +15,8 @@ class Thor::Runner < Thor
15
15
  map "-T" => :list, "-i" => :install, "-u" => :update
16
16
 
17
17
  desc "install NAME", "install a Thor file into your system tasks, optionally named for future updates"
18
- method_options :as => :optional
19
- def install(name, opts = {})
18
+ method_options :as => :optional, :relative => :boolean
19
+ def install(name)
20
20
  initialize_thorfiles
21
21
  begin
22
22
  contents = open(name).read
@@ -33,13 +33,13 @@ class Thor::Runner < Thor
33
33
  print "Do you wish to continue [y/N]? "
34
34
  response = Readline.readline
35
35
 
36
- return unless response =~ /^\s*y/i
36
+ return false unless response =~ /^\s*y/i
37
37
 
38
38
  constants = Thor::Util.constants_in_contents(contents)
39
39
 
40
- name = name =~ /\.thor$/ || is_uri ? name : "#{name}.thor"
40
+ # name = name =~ /\.thor$/ || is_uri ? name : "#{name}.thor"
41
41
 
42
- as = opts["as"] || begin
42
+ as = options["as"] || begin
43
43
  first_line = contents.split("\n")[0]
44
44
  (match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil
45
45
  end
@@ -56,15 +56,18 @@ class Thor::Runner < Thor
56
56
  FileUtils.touch(yaml_file)
57
57
  yaml = thor_yaml
58
58
 
59
- yaml[as] = {:filename => Digest::MD5.hexdigest(name + as), :location => name, :constants => constants}
59
+ location = (options[:relative] || is_uri) ? name : File.expand_path(name)
60
+ yaml[as] = {:filename => Digest::MD5.hexdigest(name + as), :location => location, :constants => constants}
60
61
 
61
62
  save_yaml(yaml)
62
63
 
63
64
  puts "Storing thor file in your system repository"
64
65
 
65
- File.open(File.join(thor_root, yaml[as][:filename] + ".thor"), "w") do |file|
66
+ File.open(File.join(thor_root, yaml[as][:filename]), "w") do |file|
66
67
  file.puts contents
67
68
  end
69
+
70
+ yaml[as][:filename] # Indicate sucess
68
71
  end
69
72
 
70
73
  desc "uninstall NAME", "uninstall a named Thor module"
@@ -74,7 +77,7 @@ class Thor::Runner < Thor
74
77
 
75
78
  puts "Uninstalling #{name}."
76
79
 
77
- file = File.join(thor_root, "#{yaml[name][:filename]}.thor")
80
+ file = File.join(thor_root, "#{yaml[name][:filename]}")
78
81
  File.delete(file)
79
82
  yaml.delete(name)
80
83
  save_yaml(yaml)
@@ -88,24 +91,30 @@ class Thor::Runner < Thor
88
91
  raise Error, "Can't find module `#{name}'" if !yaml[name] || !yaml[name][:location]
89
92
 
90
93
  puts "Updating `#{name}' from #{yaml[name][:location]}"
91
- install(yaml[name][:location], "as" => name)
94
+ old_filename = yaml[name][:filename]
95
+ options["as"] = name
96
+ filename = install(yaml[name][:location])
97
+ unless filename == old_filename
98
+ File.delete(File.join(thor_root, old_filename))
99
+ end
92
100
  end
93
101
 
94
102
  desc "installed", "list the installed Thor modules and tasks (--internal means list the built-in tasks as well)"
95
103
  method_options :internal => :boolean
96
- def installed(opts = {})
97
- Dir["#{ENV["HOME"]}/.thor/**/*.thor"].each do |f|
98
- load f unless Thor.subclass_files.keys.include?(File.expand_path(f))
104
+ def installed
105
+ thor_root_glob.each do |f|
106
+ next if f =~ /thor\.yml$/
107
+ load_thorfile f unless Thor.subclass_files.keys.include?(File.expand_path(f))
99
108
  end
100
109
 
101
110
  klasses = Thor.subclasses
102
- klasses -= [Thor, Thor::Runner] unless opts['internal']
111
+ klasses -= [Thor, Thor::Runner] unless options['internal']
103
112
  display_klasses(true, klasses)
104
113
  end
105
114
 
106
115
  desc "list [SEARCH]", "list the available thor tasks (--substring means SEARCH can be anywhere in the module)"
107
116
  method_options :substring => :boolean
108
- def list(search = "", options = {})
117
+ def list(search = "")
109
118
  initialize_thorfiles
110
119
  search = ".*#{search}" if options["substring"]
111
120
  search = /^#{search}.*/i
@@ -125,12 +134,30 @@ class Thor::Runner < Thor
125
134
  super(meth.to_sym, *args) unless meth.include? ?:
126
135
 
127
136
  initialize_thorfiles(meth)
128
- Thor[meth].parse ARGV[1..-1]
137
+ task = Thor[meth]
138
+ task.parse task.klass.new, ARGV[1..-1]
139
+ end
140
+
141
+ def self.thor_root
142
+ File.join(ENV["HOME"] || ENV["APPDATA"], ".thor")
143
+ end
144
+
145
+ def self.thor_root_glob
146
+ # On Windows thor_root will be something like this:
147
+ #
148
+ # C:\Documents and Settings\james\.thor
149
+ #
150
+ # If we don't #gsub the \ character, Dir.glob will fail.
151
+ Dir["#{thor_root.gsub(/\\/, '/')}/**/*"]
129
152
  end
130
153
 
131
154
  private
132
155
  def thor_root
133
- File.join(ENV["HOME"], ".thor")
156
+ self.class.thor_root
157
+ end
158
+
159
+ def thor_root_glob
160
+ self.class.thor_root_glob
134
161
  end
135
162
 
136
163
  def thor_yaml
@@ -150,14 +177,17 @@ class Thor::Runner < Thor
150
177
 
151
178
  if with_modules && !(yaml = thor_yaml).empty?
152
179
  max_name = yaml.max {|(xk,xv),(yk,yv)| xk.size <=> yk.size }.first.size
180
+ modules_label = "Modules"
181
+ namespaces_label = "Namespaces"
182
+ column_width = [max_name + 4, modules_label.size + 1].max
153
183
 
154
- print "%-#{max_name + 4}s" % "Modules"
155
- puts "Namespaces"
156
- print "%-#{max_name + 4}s" % "-------"
157
- puts "----------"
184
+ print "%-#{column_width}s" % modules_label
185
+ puts namespaces_label
186
+ print "%-#{column_width}s" % ("-" * modules_label.size)
187
+ puts "-" * namespaces_label.size
158
188
 
159
189
  yaml.each do |name, info|
160
- print "%-#{max_name + 4}s" % name
190
+ print "%-#{column_width}s" % name
161
191
  puts info[:constants].map {|c| Thor::Util.constant_to_thor_path(c)}.join(", ")
162
192
  end
163
193
 
@@ -192,7 +222,15 @@ class Thor::Runner < Thor
192
222
  end
193
223
 
194
224
  def initialize_thorfiles(relevant_to = nil)
195
- thorfiles(relevant_to).each {|f| load f unless Thor.subclass_files.keys.include?(File.expand_path(f))}
225
+ thorfiles(relevant_to).each {|f| load_thorfile f unless Thor.subclass_files.keys.include?(File.expand_path(f))}
226
+ end
227
+
228
+ def load_thorfile(path)
229
+ begin
230
+ load path
231
+ rescue Object => e
232
+ $stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}"
233
+ end
196
234
  end
197
235
 
198
236
  def thorfiles(relevant_to = nil)
@@ -201,7 +239,7 @@ class Thor::Runner < Thor
201
239
 
202
240
  # Look for Thorfile or *.thor in the current directory or a parent directory, until the root
203
241
  while thorfiles.empty?
204
- thorfiles = Dir[*Thor::Runner.globs_for(path)]
242
+ thorfiles = Thor::Runner.globs_for(path).map {|g| Dir[g]}.flatten
205
243
  path = File.dirname(path)
206
244
  break if path == "/"
207
245
  end
@@ -209,14 +247,14 @@ class Thor::Runner < Thor
209
247
  # We want to load system-wide Thorfiles first
210
248
  # so the local Thorfiles will override them.
211
249
  (relevant_to ? thorfiles_relevant_to(relevant_to) :
212
- Dir["#{ENV["HOME"]}/.thor/**/*.thor"]) + thorfiles
250
+ thor_root_glob) + thorfiles - ["#{thor_root}/thor.yml"]
213
251
  end
214
252
 
215
253
  def thorfiles_relevant_to(meth)
216
254
  klass_str = Thor::Util.to_constant(meth.split(":")[0...-1].join(":"))
217
255
  thor_yaml.select do |k, v|
218
256
  v[:constants] && v[:constants].include?(klass_str)
219
- end.map { |k, v| File.join(thor_root, "#{v[:filename]}.thor") }
257
+ end.map { |k, v| File.join(thor_root, "#{v[:filename]}") }
220
258
  end
221
259
 
222
260
  end
@@ -7,21 +7,32 @@ class Thor
7
7
  new(meth, "A dynamically-generated task", meth.to_s, nil, klass)
8
8
  end
9
9
 
10
- def parse(args)
11
- run(*parse_args(args))
10
+ def parse(obj, args)
11
+ list, hash = parse_args(args)
12
+ obj.options = hash
13
+ run(obj, *list)
12
14
  end
13
15
 
14
- def run(*params)
15
- raise Error, "klass is not defined for #{self.inspect}" unless klass
16
- raise NoMethodError, "the `#{meth}' task of #{klass} is private" if
17
- (klass.private_instance_methods + klass.protected_instance_methods).include?(meth)
18
-
19
- klass.new.send(meth, *params)
16
+ def run(obj, *params)
17
+ raise NoMethodError, "the `#{meth}' task of #{obj.class} is private" if
18
+ (obj.private_methods + obj.protected_methods).include?(meth)
19
+
20
+ obj.send(meth, *params)
20
21
  rescue ArgumentError => e
21
- raise e unless e.backtrace.first =~ /:in `#{meth}'$/
22
+ # backtrace sans anything in this file
23
+ backtrace = e.backtrace.reject {|frame| frame =~ /^#{Regexp.escape(__FILE__)}/}
24
+ # and sans anything that got us here
25
+ backtrace -= caller
26
+ raise e unless backtrace.empty?
27
+
28
+ # okay, they really did call it wrong
22
29
  raise Error, "`#{meth}' was called incorrectly. Call as `#{formatted_usage}'"
23
30
  rescue NoMethodError => e
24
- raise e unless e.message =~ /^undefined method `#{meth}' for #<#{klass}:.*>$/
31
+ begin
32
+ raise e unless e.message =~ /^undefined method `#{meth}' for #{Regexp.escape(obj.inspect)}$/
33
+ rescue
34
+ raise e
35
+ end
25
36
  raise Error, "The #{namespace false} namespace doesn't have a `#{meth}' task"
26
37
  end
27
38
 
@@ -34,41 +45,24 @@ class Thor
34
45
  new.klass = klass
35
46
  new
36
47
  end
37
-
38
- def formatted_opts
39
- return "" if opts.nil?
40
- opts.map do |opt, val|
41
- if val == true || val == "BOOLEAN"
42
- "[#{opt}]"
43
- elsif val == "REQUIRED"
44
- opt + "=" + opt.gsub(/\-/, "").upcase
45
- elsif val == "OPTIONAL"
46
- "[" + opt + "=" + opt.gsub(/\-/, "").upcase + "]"
47
- end
48
- end.join(" ")
48
+
49
+ def opts
50
+ return super unless super.kind_of? Hash
51
+ self.opts = Options.new(super)
49
52
  end
50
53
 
51
54
  def formatted_usage(namespace = false)
52
55
  (namespace ? self.namespace + ':' : '') + usage +
53
- (opts ? " " + formatted_opts : "")
56
+ (opts ? " " + opts.formatted_usage : "")
54
57
  end
55
58
 
56
59
  protected
57
60
 
58
61
  def parse_args(args)
59
- return args unless opts
60
-
61
- args = args.dup
62
- params = []
63
- params << args.shift until args.empty? || args.first[0] == ?-
64
-
65
- old_argv = ARGV.dup
66
- ARGV.replace args
67
- options = Getopt::Long.getopts(*opts.map do |opt, val|
68
- [opt, val == true ? Getopt::BOOLEAN : Getopt.const_get(val)].flatten
69
- end)
70
- ARGV.replace old_argv
71
- params + [options]
62
+ return [args, {}] unless opts
63
+ hash = opts.parse(args)
64
+ list = opts.non_opts
65
+ [list, hash]
72
66
  end
73
67
  end
74
68
  end
@@ -13,7 +13,7 @@ class Thor::TaskHash < Thor::OrderedHash
13
13
  end
14
14
 
15
15
  def [](name)
16
- if task = super(name)
16
+ if task = super(name) || (@klass == Thor && @klass.superclass.tasks[name])
17
17
  return task.with_klass(@klass)
18
18
  end
19
19
 
@@ -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} install pkg/#{spec.name}-#{spec.version} --no-rdoc --no-ri --no-update-sources}
23
26
  end
24
27
  end
25
28
 
@@ -52,16 +55,20 @@ class Thor
52
55
  cmd << options
53
56
  puts cmd if verbose
54
57
  system(cmd)
58
+ exit($?.exitstatus)
55
59
  end
56
60
  end
57
61
 
58
62
  private
59
63
  def self.convert_task_options(opts)
60
64
  opts.map do |key, value|
61
- if value == true
65
+ case value
66
+ when true
62
67
  "--#{key}"
63
- elsif value.is_a?(Array)
68
+ when Array
64
69
  value.map {|v| "--#{key} #{v.inspect}"}.join(" ")
70
+ when nil, false
71
+ ""
65
72
  else
66
73
  "--#{key} #{value.inspect}"
67
74
  end
@@ -0,0 +1,18 @@
1
+ require "thor/task"
2
+
3
+ class Thor::PackageTask < Thor::Task
4
+ attr_accessor :spec
5
+ attr_accessor :opts
6
+
7
+ def initialize(gemspec, opts = {})
8
+ super(:package, "build a gem package")
9
+ @spec = gemspec
10
+ @opts = {:dir => File.join(Dir.pwd, "pkg")}.merge(opts)
11
+ end
12
+
13
+ def run
14
+ FileUtils.mkdir_p(@opts[:dir])
15
+ Gem::Builder.new(spec).build
16
+ FileUtils.mv(spec.file_name, File.join(@opts[:dir], spec.file_name))
17
+ end
18
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wycats-thor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.9.5
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-05-19 00:00:00 -07:00
12
+ date: 2008-08-27 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -22,20 +22,24 @@ extensions: []
22
22
 
23
23
  extra_rdoc_files:
24
24
  - README.markdown
25
+ - CHANGELOG.rdoc
25
26
  - LICENSE
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
32
- - lib/getopt.rb
33
34
  - lib/thor
34
35
  - lib/thor/error.rb
36
+ - lib/thor/options.rb
35
37
  - lib/thor/ordered_hash.rb
36
38
  - lib/thor/runner.rb
37
39
  - lib/thor/task.rb
38
40
  - lib/thor/task_hash.rb
41
+ - lib/thor/tasks
42
+ - lib/thor/tasks/package.rb
39
43
  - lib/thor/tasks.rb
40
44
  - lib/thor/util.rb
41
45
  - lib/thor.rb
@@ -61,7 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
65
  requirements: []
62
66
 
63
67
  rubyforge_project: thor
64
- rubygems_version: 1.0.1
68
+ rubygems_version: 1.2.0
65
69
  signing_key:
66
70
  specification_version: 2
67
71
  summary: A gem that maps options to a class
@@ -1,238 +0,0 @@
1
- # The last time the Getopt gem was modified was August 2007, so it's safe to vendor (it does everything we need)
2
-
3
- module Getopt
4
-
5
- REQUIRED = 0
6
- BOOLEAN = 1
7
- OPTIONAL = 2
8
- INCREMENT = 3
9
- NEGATABLE = 4
10
- NUMERIC = 5
11
-
12
- class Long
13
- class Error < StandardError; end
14
-
15
- VERSION = '1.3.6'
16
-
17
- # Takes an array of switches. Each array consists of up to three
18
- # elements that indicate the name and type of switch. Returns a hash
19
- # containing each switch name, minus the '-', as a key. The value
20
- # for each key depends on the type of switch and/or the value provided
21
- # by the user.
22
- #
23
- # The long switch _must_ be provided. The short switch defaults to the
24
- # first letter of the short switch. The default type is BOOLEAN.
25
- #
26
- # Example:
27
- #
28
- # opts = Getopt::Long.getopts(
29
- # ["--debug"],
30
- # ["--verbose", "-v"],
31
- # ["--level", "-l", NUMERIC]
32
- # )
33
- #
34
- # See the README file for more information.
35
- #
36
- def self.getopts(*switches)
37
- if switches.empty?
38
- raise ArgumentError, "no switches provided"
39
- end
40
-
41
- hash = {} # Hash returned to user
42
- valid = [] # Tracks valid switches
43
- types = {} # Tracks argument types
44
- syns = {} # Tracks long and short arguments, or multiple shorts
45
-
46
- # If a string is passed, split it and convert it to an array of arrays
47
- if switches.first.kind_of?(String)
48
- switches = switches.join.split
49
- switches.map!{ |switch| switch = [switch] }
50
- end
51
-
52
- # Set our list of valid switches, and proper types for each switch
53
- switches.each{ |switch|
54
- valid.push(switch[0]) # Set valid long switches
55
-
56
- # Set type for long switch, default to BOOLEAN.
57
- if switch[1].kind_of?(Fixnum)
58
- switch[2] = switch[1]
59
- types[switch[0]] = switch[2]
60
- switch[1] = switch[0][1..2]
61
- else
62
- switch[2] ||= BOOLEAN
63
- types[switch[0]] = switch[2]
64
- switch[1] ||= switch[0][1..2]
65
- end
66
-
67
- # Create synonym hash. Default to first char of long switch for
68
- # short switch, e.g. "--verbose" creates a "-v" synonym. The same
69
- # synonym can only be used once - first one wins.
70
- syns[switch[0]] = switch[1] unless syns[switch[1]]
71
- syns[switch[1]] = switch[0] unless syns[switch[1]]
72
-
73
- switch[1].each{ |char|
74
- types[char] = switch[2] # Set type for short switch
75
- valid.push(char) # Set valid short switches
76
- }
77
-
78
- if ARGV.empty? && switch[2] == REQUIRED
79
- raise Error, "no value provided for required argument '#{switch[0]}'"
80
- end
81
- }
82
-
83
- re_long = /^(--\w+[-\w+]*)?$/
84
- re_short = /^(-\w)$/
85
- re_long_eq = /^(--\w+[-\w+]*)?=(.*?)$|(-\w?)=(.*?)$/
86
- re_short_sq = /^(-\w)(\S+?)$/
87
-
88
- ARGV.each_with_index{ |opt, index|
89
-
90
- # Allow either -x -v or -xv style for single char args
91
- if re_short_sq.match(opt)
92
- chars = opt.split("")[1..-1].map{ |s| s = "-#{s}" }
93
-
94
- chars.each_with_index{ |char, i|
95
- unless valid.include?(char)
96
- raise Error, "invalid switch '#{char}'"
97
- end
98
-
99
- # Grab the next arg if the switch takes a required arg
100
- if types[char] == REQUIRED
101
- # Deal with a argument squished up against switch
102
- if chars[i+1]
103
- arg = chars[i+1..-1].join.tr("-","")
104
- ARGV.push(char, arg)
105
- break
106
- else
107
- arg = ARGV.delete_at(index+1)
108
- if arg.nil? || valid.include?(arg) # Minor cheat here
109
- err = "no value provided for required argument '#{char}'"
110
- raise Error, err
111
- end
112
- ARGV.push(char, arg)
113
- end
114
- elsif types[char] == OPTIONAL
115
- if chars[i+1] && !valid.include?(chars[i+1])
116
- arg = chars[i+1..-1].join.tr("-","")
117
- ARGV.push(char, arg)
118
- break
119
- elsif
120
- if ARGV[index+1] && !valid.include?(ARGV[index+1])
121
- arg = ARGV.delete_at(index+1)
122
- ARGV.push(char, arg)
123
- end
124
- else
125
- ARGV.push(char)
126
- end
127
- else
128
- ARGV.push(char)
129
- end
130
- }
131
- next
132
- end
133
-
134
- if match = re_long.match(opt) || match = re_short.match(opt)
135
- switch = match.captures.first
136
- end
137
-
138
- if match = re_long_eq.match(opt)
139
- switch, value = match.captures.compact
140
- ARGV.push(switch, value)
141
- next
142
- end
143
-
144
- # Make sure that all the switches are valid. If 'switch' isn't
145
- # defined at this point, it means an option was passed without
146
- # a preceding switch, e.g. --option foo bar.
147
- unless valid.include?(switch)
148
- switch ||= opt
149
- raise Error, "invalid switch '#{switch}'"
150
- end
151
-
152
- # Required arguments
153
- if types[switch] == REQUIRED
154
- nextval = ARGV[index+1]
155
-
156
- # Make sure there's a value for mandatory arguments
157
- if nextval.nil?
158
- err = "no value provided for required argument '#{switch}'"
159
- raise Error, err
160
- end
161
-
162
- # If there is a value, make sure it's not another switch
163
- if valid.include?(nextval)
164
- err = "cannot pass switch '#{nextval}' as an argument"
165
- raise Error, err
166
- end
167
-
168
- # If the same option appears more than once, put the values
169
- # in array.
170
- if hash[switch]
171
- hash[switch] = [hash[switch], nextval].flatten
172
- else
173
- hash[switch] = nextval
174
- end
175
- ARGV.delete_at(index+1)
176
- end
177
-
178
- # For boolean arguments set the switch's value to true.
179
- if types[switch] == BOOLEAN
180
- if hash.has_key?(switch)
181
- raise Error, "boolean switch already set"
182
- end
183
- hash[switch] = true
184
- end
185
-
186
- # For increment arguments, set the switch's value to 0, or
187
- # increment it by one if it already exists.
188
- if types[switch] == INCREMENT
189
- if hash.has_key?(switch)
190
- hash[switch] += 1
191
- else
192
- hash[switch] = 1
193
- end
194
- end
195
-
196
- # For optional argument, there may be an argument. If so, it
197
- # cannot be another switch. If not, it is set to true.
198
- if types[switch] == OPTIONAL
199
- nextval = ARGV[index+1]
200
- if valid.include?(nextval)
201
- hash[switch] = true
202
- else
203
- hash[switch] = nextval
204
- ARGV.delete_at(index+1)
205
- end
206
- end
207
- }
208
-
209
- # Set synonymous switches to the same value, e.g. if -t is a synonym
210
- # for --test, and the user passes "--test", then set "-t" to the same
211
- # value that "--test" was set to.
212
- #
213
- # This allows users to refer to the long or short switch and get
214
- # the same value
215
- hash.each{ |switch, val|
216
- if syns.keys.include?(switch)
217
- syns[switch].each{ |key|
218
- hash[key] = val
219
- }
220
- end
221
- }
222
-
223
- # Get rid of leading "--" and "-" to make it easier to reference
224
- hash.each{ |key, value|
225
- if key[0,2] == '--'
226
- nkey = key.sub('--', '')
227
- else
228
- nkey = key.sub('-', '')
229
- end
230
- hash.delete(key)
231
- hash[nkey] = value
232
- }
233
-
234
- hash
235
- end
236
-
237
- end
238
- end