wycats-thor 0.9.2 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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