jherdman-thor 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc ADDED
@@ -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.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Yehuda Katz
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,76 @@
1
+ thor
2
+ ====
3
+
4
+ Map options to a class. Simply create a class with the appropriate annotations, and have options automatically map
5
+ to functions and parameters.
6
+
7
+ Example:
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, :alias => :optional # [4]
14
+ def install(name)
15
+ user_alias = options[:alias]
16
+ if options.force?
17
+ # do something
18
+ end
19
+ # ... other code ...
20
+ end
21
+
22
+ desc "list [SEARCH]", "list all of the available apps, limited by SEARCH"
23
+ def list(search = "")
24
+ # list everything
25
+ end
26
+ end
27
+
28
+ Thor automatically maps commands as such:
29
+
30
+ app install myname --force
31
+
32
+ That gets converted to:
33
+
34
+ MyApp.new.install("myname")
35
+ # with {'force' => true} as options hash
36
+
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,
39
+ convert -L to :list
40
+ 3. Describe the method immediately below. The first parameter is the usage information,
41
+ and the second parameter is the description.
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.
44
+
45
+ Types for `method_options`
46
+ --------------------------
47
+
48
+ <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 ADDED
@@ -0,0 +1,6 @@
1
+ task :default => :install
2
+
3
+ desc "install the gem locally"
4
+ task :install do
5
+ sh %{ruby "#{File.dirname(__FILE__)}/bin/thor" :install}
6
+ end
data/bin/rake2thor ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'ruby2ruby'
5
+ require 'rake'
6
+
7
+ input = ARGV[0] || 'Rakefile'
8
+ output = ARGV[1] || 'Thorfile'
9
+
10
+ $requires = []
11
+
12
+ module Kernel
13
+ def require_with_record(file)
14
+ $requires << file if caller[1] =~ /rake2thor:/
15
+ require_without_record file
16
+ end
17
+ alias_method :require_without_record, :require
18
+ alias_method :require, :require_with_record
19
+ end
20
+
21
+ load input
22
+
23
+ @private_methods = []
24
+
25
+ def file_task_name(name)
26
+ "compile_" + name.gsub('/', '_slash_').gsub('.', '_dot_').gsub(/\W/, '_')
27
+ end
28
+
29
+ def method_for_task(task)
30
+ file_task = task.is_a?(Rake::FileTask)
31
+ comment = task.instance_variable_get('@comment')
32
+ prereqs = task.instance_variable_get('@prerequisites').select(&Rake::Task.method(:task_defined?))
33
+ actions = task.instance_variable_get('@actions')
34
+ name = task.name.gsub(/^([^:]+:)+/, '')
35
+ name = file_task_name(name) if file_task
36
+ meth = ''
37
+
38
+ meth << "desc #{name.inspect}, #{comment.inspect}\n" if comment
39
+ meth << "def #{name}\n"
40
+
41
+ meth << prereqs.map do |pre|
42
+ pre = pre.to_s
43
+ pre = file_task_name(pre) if Rake::Task[pre].is_a?(Rake::FileTask)
44
+ ' ' + pre
45
+ end.join("\n")
46
+
47
+ meth << "\n\n" unless prereqs.empty? || actions.empty?
48
+
49
+ meth << actions.map do |act|
50
+ act = act.to_ruby
51
+ unless act.gsub!(/^proc \{ \|(\w+)\|\n/,
52
+ " \\1 = Struct.new(:name).new(#{name.inspect}) # A crude mock Rake::Task object\n")
53
+ act.gsub!(/^proc \{\n/, '')
54
+ end
55
+ act.gsub(/\n\}$/, '')
56
+ end.join("\n")
57
+
58
+ meth << "\nend"
59
+
60
+ if file_task
61
+ @private_methods << meth
62
+ return
63
+ end
64
+
65
+ meth
66
+ end
67
+
68
+ body = Rake::Task.tasks.map(&method(:method_for_task)).compact.map { |meth| meth.gsub(/^/, ' ') }.join("\n\n")
69
+
70
+ unless @private_methods.empty?
71
+ body << "\n\n private\n\n"
72
+ body << @private_methods.map { |meth| meth.gsub(/^/, ' ') }.join("\n\n")
73
+ end
74
+
75
+ requires = $requires.map { |r| "require #{r.inspect}" }.join("\n")
76
+
77
+ File.open(output, 'w') { |f| f.write(<<END.lstrip) }
78
+ #{requires}
79
+
80
+ class Default < Thor
81
+ #{body}
82
+ end
83
+ END
data/bin/thor ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- mode: ruby -*-
3
+
4
+ require File.dirname(__FILE__) + "/../lib/thor"
5
+ require 'thor/runner'
6
+
7
+ Thor::Runner.start
data/lib/thor.rb ADDED
@@ -0,0 +1,138 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__))
2
+ require "thor/options"
3
+ require "thor/util"
4
+ require "thor/task"
5
+ require "thor/task_hash"
6
+
7
+ class Thor
8
+ attr_accessor :options
9
+
10
+ def self.map(map)
11
+ @map ||= superclass.instance_variable_get("@map") || {}
12
+ map.each do |key, value|
13
+ if key.respond_to?(:each)
14
+ key.each {|subkey| @map[subkey] = value}
15
+ else
16
+ @map[key] = value
17
+ end
18
+ end
19
+ end
20
+
21
+ def self.desc(usage, description)
22
+ @usage, @desc = usage, description
23
+ end
24
+
25
+ def self.method_options(opts)
26
+ @method_options = opts
27
+ end
28
+
29
+ def self.subclass_files
30
+ @subclass_files ||= Hash.new {|h,k| h[k] = []}
31
+ end
32
+
33
+ def self.subclasses
34
+ @subclasses ||= []
35
+ end
36
+
37
+ def self.tasks
38
+ @tasks ||= TaskHash.new(self)
39
+ end
40
+
41
+ def self.opts
42
+ (@opts || {}).merge(self == Thor ? {} : superclass.opts)
43
+ end
44
+
45
+ def self.[](task)
46
+ namespaces = task.split(":")
47
+ klass = Thor::Util.constant_from_thor_path(namespaces[0...-1].join(":"))
48
+ raise Error, "`#{klass}' is not a Thor class" unless klass <= Thor
49
+ klass.tasks[namespaces.last]
50
+ end
51
+
52
+ def self.maxima
53
+ @maxima ||= begin
54
+ max_usage = tasks.map {|_, t| t.usage}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
55
+ max_desc = tasks.map {|_, t| t.description}.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
57
+ Struct.new(:description, :usage, :opt).new(max_desc, max_usage, max_opts)
58
+ end
59
+ end
60
+
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
+
66
+ meth = args.first
67
+ meth = @map[meth].to_s if @map && @map[meth]
68
+ meth ||= "help"
69
+
70
+ tasks[meth].parse new(opts, *args), args[1..-1]
71
+ rescue Thor::Error => e
72
+ $stderr.puts e.message
73
+ end
74
+
75
+ class << self
76
+ protected
77
+ def inherited(klass)
78
+ register_klass_file klass
79
+ end
80
+
81
+ def method_added(meth)
82
+ meth = meth.to_s
83
+
84
+ if meth == "initialize"
85
+ @opts = @method_options
86
+ @method_options = nil
87
+ return
88
+ end
89
+
90
+ return if !public_instance_methods.include?(meth) || !@usage
91
+ register_klass_file self
92
+
93
+ tasks[meth] = Task.new(meth, @desc, @usage, @method_options)
94
+
95
+ @usage, @desc, @method_options = nil
96
+ end
97
+
98
+ def register_klass_file(klass, file = caller[1].split(":")[0])
99
+ unless self == Thor
100
+ superclass.register_klass_file(klass, file)
101
+ return
102
+ end
103
+
104
+ file_subclasses = subclass_files[File.expand_path(file)]
105
+ file_subclasses << klass unless file_subclasses.include?(klass)
106
+ subclasses << klass unless subclasses.include?(klass)
107
+ end
108
+ end
109
+
110
+ def initialize(opts = {}, *args)
111
+ end
112
+
113
+ map ["-h", "-?", "--help", "-D"] => :help
114
+
115
+ desc "help [TASK]", "describe available tasks or one specific task"
116
+ def help(task = nil)
117
+ if task
118
+ if task.include? ?:
119
+ task = self.class[task]
120
+ namespace = true
121
+ else
122
+ task = self.class.tasks[task]
123
+ end
124
+
125
+ puts task.formatted_usage(namespace)
126
+ puts task.description
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
135
+ end
136
+ end
137
+
138
+ end
data/lib/thor/error.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Thor
2
+ class Error < Exception; end
3
+ end
@@ -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
@@ -0,0 +1,64 @@
1
+ class Thor
2
+ # This class is based on the Ruby 1.9 ordered hashes.
3
+ # It keeps the semantics and most of the efficiency of normal hashes
4
+ # while also keeping track of the order in which elements were set.
5
+ class OrderedHash
6
+ Node = Struct.new(:key, :value, :next, :prev)
7
+ include Enumerable
8
+
9
+ def initialize
10
+ @hash = {}
11
+ end
12
+
13
+ def initialize_copy(other)
14
+ @hash = other.instance_variable_get('@hash').clone
15
+ end
16
+
17
+ def [](key)
18
+ @hash[key] && @hash[key].value
19
+ end
20
+
21
+ def []=(key, value)
22
+ node = Node.new(key, value)
23
+
24
+ if old = @hash[key]
25
+ if old.prev
26
+ old.prev.next = old.next
27
+ else # old is @first and @last
28
+ @first = @last = nil
29
+ end
30
+ end
31
+
32
+ if @first.nil?
33
+ @first = @last = node
34
+ else
35
+ node.prev = @last
36
+ @last.next = node
37
+ @last = node
38
+ end
39
+
40
+ @hash[key] = node
41
+ value
42
+ end
43
+
44
+ def each
45
+ return unless @first
46
+ yield [@first.key, @first.value]
47
+ node = @first
48
+ yield [node.key, node.value] while node = node.next
49
+ self
50
+ end
51
+
52
+ def values
53
+ self.map { |k, v| v }
54
+ end
55
+
56
+ def +(other)
57
+ new = clone
58
+ other.each do |key, value|
59
+ new[key] = value unless self[key]
60
+ end
61
+ new
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,260 @@
1
+ require 'thor'
2
+ require "thor/util"
3
+ require "open-uri"
4
+ require "fileutils"
5
+ require "yaml"
6
+ require "digest/md5"
7
+ require "readline"
8
+
9
+ class Thor::Runner < Thor
10
+
11
+ def self.globs_for(path)
12
+ ["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"]
13
+ end
14
+
15
+ map "-T" => :list, "-i" => :install, "-u" => :update
16
+
17
+ desc "install NAME", "install a Thor file into your system tasks, optionally named for future updates"
18
+ method_options :as => :optional, :relative => :boolean
19
+ def install(name)
20
+ initialize_thorfiles
21
+ begin
22
+ contents = open(name).read
23
+ rescue OpenURI::HTTPError
24
+ raise Error, "Error opening URI `#{name}'"
25
+ rescue Errno::ENOENT
26
+ raise Error, "Error opening file `#{name}'"
27
+ end
28
+
29
+ is_uri = File.exist?(name) ? false : true
30
+
31
+ puts "Your Thorfile contains: "
32
+ puts contents
33
+ print "Do you wish to continue [y/N]? "
34
+ response = Readline.readline
35
+
36
+ return false unless response =~ /^\s*y/i
37
+
38
+ constants = Thor::Util.constants_in_contents(contents)
39
+
40
+ # name = name =~ /\.thor$/ || is_uri ? name : "#{name}.thor"
41
+
42
+ as = options["as"] || begin
43
+ first_line = contents.split("\n")[0]
44
+ (match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil
45
+ end
46
+
47
+ if !as
48
+ print "Please specify a name for #{name} in the system repository [#{name}]: "
49
+ as = Readline.readline
50
+ as = name if as.empty?
51
+ end
52
+
53
+ FileUtils.mkdir_p thor_root
54
+
55
+ yaml_file = File.join(thor_root, "thor.yml")
56
+ FileUtils.touch(yaml_file)
57
+ yaml = thor_yaml
58
+
59
+ location = (options[:relative] || is_uri) ? name : File.expand_path(name)
60
+ yaml[as] = {:filename => Digest::MD5.hexdigest(name + as), :location => location, :constants => constants}
61
+
62
+ save_yaml(yaml)
63
+
64
+ puts "Storing thor file in your system repository"
65
+
66
+ File.open(File.join(thor_root, yaml[as][:filename]), "w") do |file|
67
+ file.puts contents
68
+ end
69
+
70
+ yaml[as][:filename] # Indicate sucess
71
+ end
72
+
73
+ desc "uninstall NAME", "uninstall a named Thor module"
74
+ def uninstall(name)
75
+ yaml = thor_yaml
76
+ raise Error, "Can't find module `#{name}'" unless yaml[name]
77
+
78
+ puts "Uninstalling #{name}."
79
+
80
+ file = File.join(thor_root, "#{yaml[name][:filename]}")
81
+ File.delete(file)
82
+ yaml.delete(name)
83
+ save_yaml(yaml)
84
+
85
+ puts "Done."
86
+ end
87
+
88
+ desc "update NAME", "update a Thor file from its original location"
89
+ def update(name)
90
+ yaml = thor_yaml
91
+ raise Error, "Can't find module `#{name}'" if !yaml[name] || !yaml[name][:location]
92
+
93
+ puts "Updating `#{name}' from #{yaml[name][:location]}"
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
100
+ end
101
+
102
+ desc "installed", "list the installed Thor modules and tasks (--internal means list the built-in tasks as well)"
103
+ method_options :internal => :boolean
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))
108
+ end
109
+
110
+ klasses = Thor.subclasses
111
+ klasses -= [Thor, Thor::Runner] unless options['internal']
112
+ display_klasses(true, klasses)
113
+ end
114
+
115
+ desc "list [SEARCH]", "list the available thor tasks (--substring means SEARCH can be anywhere in the module)"
116
+ method_options :substring => :boolean
117
+ def list(search = "")
118
+ initialize_thorfiles
119
+ search = ".*#{search}" if options["substring"]
120
+ search = /^#{search}.*/i
121
+
122
+ display_klasses(false, Thor.subclasses.select {|k|
123
+ Thor::Util.constant_to_thor_path(k.name) =~ search})
124
+ end
125
+
126
+ # Override Thor#help so we can give info about not-yet-loaded tasks
127
+ def help(task = nil)
128
+ initialize_thorfiles(task) if task && task.include?(?:)
129
+ super
130
+ end
131
+
132
+ def method_missing(meth, *args)
133
+ meth = meth.to_s
134
+ super(meth.to_sym, *args) unless meth.include? ?:
135
+
136
+ initialize_thorfiles(meth)
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(/\\/, '/')}/**/*"]
152
+ end
153
+
154
+ private
155
+ def thor_root
156
+ self.class.thor_root
157
+ end
158
+
159
+ def thor_root_glob
160
+ self.class.thor_root_glob
161
+ end
162
+
163
+ def thor_yaml
164
+ yaml_file = File.join(thor_root, "thor.yml")
165
+ yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file)
166
+ yaml || {}
167
+ end
168
+
169
+ def save_yaml(yaml)
170
+ yaml_file = File.join(thor_root, "thor.yml")
171
+ File.open(yaml_file, "w") {|f| f.puts yaml.to_yaml }
172
+ end
173
+
174
+ def display_klasses(with_modules = false, klasses = Thor.subclasses)
175
+ klasses -= [Thor, Thor::Runner] unless with_modules
176
+ raise Error, "No Thor tasks available" if klasses.empty?
177
+
178
+ if with_modules && !(yaml = thor_yaml).empty?
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
183
+
184
+ print "%-#{column_width}s" % modules_label
185
+ puts namespaces_label
186
+ print "%-#{column_width}s" % ("-" * modules_label.size)
187
+ puts "-" * namespaces_label.size
188
+
189
+ yaml.each do |name, info|
190
+ print "%-#{column_width}s" % name
191
+ puts info[:constants].map {|c| Thor::Util.constant_to_thor_path(c)}.join(", ")
192
+ end
193
+
194
+ puts
195
+ end
196
+
197
+ puts "Tasks"
198
+ puts "-----"
199
+
200
+ # Calculate the largest base class name
201
+ max_base = klasses.max do |x,y|
202
+ Thor::Util.constant_to_thor_path(x.name).size <=> Thor::Util.constant_to_thor_path(y.name).size
203
+ end.name.size
204
+
205
+ # Calculate the size of the largest option description
206
+ max_left_item = klasses.max do |x,y|
207
+ (x.maxima.usage + x.maxima.opt).to_i <=> (y.maxima.usage + y.maxima.opt).to_i
208
+ end
209
+
210
+ max_left = max_left_item.maxima.usage + max_left_item.maxima.opt
211
+
212
+ klasses.each {|k| display_tasks(k, max_base, max_left)}
213
+ end
214
+
215
+ def display_tasks(klass, max_base, max_left)
216
+ base = Thor::Util.constant_to_thor_path(klass.name)
217
+ klass.tasks.each true do |name, task|
218
+ format_string = "%-#{max_left + max_base + 5}s"
219
+ print format_string % task.formatted_usage(true)
220
+ puts task.description
221
+ end
222
+ end
223
+
224
+ def initialize_thorfiles(relevant_to = nil)
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
234
+ end
235
+
236
+ def thorfiles(relevant_to = nil)
237
+ path = Dir.pwd
238
+ thorfiles = []
239
+
240
+ # Look for Thorfile or *.thor in the current directory or a parent directory, until the root
241
+ while thorfiles.empty?
242
+ thorfiles = Thor::Runner.globs_for(path).map {|g| Dir[g]}.flatten
243
+ path = File.dirname(path)
244
+ break if path == "/"
245
+ end
246
+
247
+ # We want to load system-wide Thorfiles first
248
+ # so the local Thorfiles will override them.
249
+ (relevant_to ? thorfiles_relevant_to(relevant_to) :
250
+ thor_root_glob) + thorfiles - ["#{thor_root}/thor.yml"]
251
+ end
252
+
253
+ def thorfiles_relevant_to(meth)
254
+ klass_str = Thor::Util.to_constant(meth.split(":")[0...-1].join(":"))
255
+ thor_yaml.select do |k, v|
256
+ v[:constants] && v[:constants].include?(klass_str)
257
+ end.map { |k, v| File.join(thor_root, "#{v[:filename]}") }
258
+ end
259
+
260
+ end
data/lib/thor/task.rb ADDED
@@ -0,0 +1,68 @@
1
+ require 'thor/error'
2
+ require 'thor/util'
3
+
4
+ class Thor
5
+ class Task < Struct.new(:meth, :description, :usage, :opts, :klass)
6
+ def self.dynamic(meth, klass)
7
+ new(meth, "A dynamically-generated task", meth.to_s, nil, klass)
8
+ end
9
+
10
+ def parse(obj, args)
11
+ list, hash = parse_args(args)
12
+ obj.options = hash
13
+ run(obj, *list)
14
+ end
15
+
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)
21
+ rescue ArgumentError => e
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
29
+ raise Error, "`#{meth}' was called incorrectly. Call as `#{formatted_usage}'"
30
+ rescue NoMethodError => e
31
+ begin
32
+ raise e unless e.message =~ /^undefined method `#{meth}' for #{Regexp.escape(obj.inspect)}$/
33
+ rescue
34
+ raise e
35
+ end
36
+ raise Error, "The #{namespace false} namespace doesn't have a `#{meth}' task"
37
+ end
38
+
39
+ def namespace(remove_default = true)
40
+ Thor::Util.constant_to_thor_path(klass, remove_default)
41
+ end
42
+
43
+ def with_klass(klass)
44
+ new = self.dup
45
+ new.klass = klass
46
+ new
47
+ end
48
+
49
+ def opts
50
+ return super unless super.kind_of? Hash
51
+ self.opts = Options.new(super)
52
+ end
53
+
54
+ def formatted_usage(namespace = false)
55
+ (namespace ? self.namespace + ':' : '') + usage +
56
+ (opts ? " " + opts.formatted_usage : "")
57
+ end
58
+
59
+ protected
60
+
61
+ def parse_args(args)
62
+ return [args, {}] unless opts
63
+ hash = opts.parse(args)
64
+ list = opts.non_opts
65
+ [list, hash]
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,22 @@
1
+ require 'thor/ordered_hash'
2
+ require 'thor/task'
3
+
4
+ class Thor::TaskHash < Thor::OrderedHash
5
+ def initialize(klass)
6
+ super()
7
+ @klass = klass
8
+ end
9
+
10
+ def each(local = false, &block)
11
+ super() { |k, t| yield k, t.with_klass(@klass) }
12
+ @klass.superclass.tasks.each { |k, t| yield k, t.with_klass(@klass) } unless local || @klass == Thor
13
+ end
14
+
15
+ def [](name)
16
+ if task = super(name) || (@klass == Thor && @klass.superclass.tasks[name])
17
+ return task.with_klass(@klass)
18
+ end
19
+
20
+ Thor::Task.dynamic(name, @klass)
21
+ end
22
+ end
data/lib/thor/tasks.rb ADDED
@@ -0,0 +1,77 @@
1
+ require "thor"
2
+ require "fileutils"
3
+
4
+ class Thor
5
+ def self.package_task(spec)
6
+ desc "package", "package up the gem"
7
+ define_method :package do
8
+ FileUtils.mkdir_p(File.join(Dir.pwd, "pkg"))
9
+ Gem::Builder.new(spec).build
10
+ FileUtils.mv(spec.file_name, File.join(Dir.pwd, "pkg", spec.file_name))
11
+ end
12
+ end
13
+
14
+ def self.install_task(spec)
15
+ package_task spec
16
+
17
+ null, sudo, gem = RUBY_PLATFORM =~ /w(in)?32$/ ? ['NUL', '', 'gem.bat'] :
18
+ ['/dev/null', 'sudo', 'gem']
19
+
20
+ desc "install", "install the gem"
21
+ define_method :install do
22
+ old_stderr, $stderr = $stderr.dup, File.open(null, "w")
23
+ package
24
+ $stderr = old_stderr
25
+ system %{#{sudo} #{gem} install pkg/#{spec.name}-#{spec.version} --no-rdoc --no-ri --no-update-sources}
26
+ end
27
+ end
28
+
29
+ def self.spec_task(file_list, opts = {})
30
+ name = opts.delete(:name) || "spec"
31
+ rcov_dir = opts.delete(:rcov_dir) || "coverage"
32
+ file_list = file_list.map {|f| %["#{f}"]}.join(" ")
33
+ verbose = opts.delete(:verbose)
34
+ opts = {:format => "specdoc", :color => true}.merge(opts)
35
+
36
+ rcov_opts = convert_task_options(opts.delete(:rcov) || {})
37
+ rcov = !rcov_opts.empty?
38
+ options = convert_task_options(opts)
39
+
40
+ if rcov
41
+ FileUtils.rm_rf(File.join(Dir.pwd, rcov_dir))
42
+ end
43
+
44
+ desc(name, "spec task")
45
+ define_method(name) do
46
+ cmd = "ruby "
47
+ if rcov
48
+ cmd << "-S rcov -o #{rcov_dir} #{rcov_opts} "
49
+ end
50
+ cmd << `which spec`.chomp
51
+ cmd << " -- " if rcov
52
+ cmd << " "
53
+ cmd << file_list
54
+ cmd << " "
55
+ cmd << options
56
+ puts cmd if verbose
57
+ system(cmd)
58
+ exit($?.exitstatus)
59
+ end
60
+ end
61
+
62
+ private
63
+ def self.convert_task_options(opts)
64
+ opts.map do |key, value|
65
+ case value
66
+ when true
67
+ "--#{key}"
68
+ when Array
69
+ value.map {|v| "--#{key} #{v.inspect}"}.join(" ")
70
+ when nil, false
71
+ ""
72
+ else
73
+ "--#{key} #{value.inspect}"
74
+ end
75
+ end.join(" ")
76
+ end
77
+ 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
data/lib/thor/util.rb ADDED
@@ -0,0 +1,43 @@
1
+ require 'thor/error'
2
+
3
+ class Thor
4
+ module Util
5
+
6
+ def self.constant_to_thor_path(str, remove_default = true)
7
+ str = snake_case(str.to_s).squeeze(":")
8
+ str.gsub!(/^default/, '') if remove_default
9
+ str
10
+ end
11
+
12
+ def self.constant_from_thor_path(str)
13
+ make_constant(to_constant(str))
14
+ rescue NameError => e
15
+ raise e unless e.message =~ /^uninitialized constant (.*)$/
16
+ raise Error, "There was no available namespace `#{str}'."
17
+ end
18
+
19
+ def self.to_constant(str)
20
+ str = 'default' if str.empty?
21
+ str.gsub(/:(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
22
+ end
23
+
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
30
+ end
31
+
32
+ def self.make_constant(str)
33
+ list = str.split("::").inject(Object) {|obj, x| obj.const_get(x)}
34
+ end
35
+
36
+ def self.snake_case(str)
37
+ return str.downcase if str =~ /^[A-Z_]+$/
38
+ str.gsub(/\B[A-Z]/, '_\&').squeeze('_') =~ /_*(.*)/
39
+ return $+.downcase
40
+ end
41
+
42
+ end
43
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jherdman-thor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.5
5
+ platform: ruby
6
+ authors:
7
+ - Yehuda Katz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-08-27 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: A gem that maps options to a class
17
+ email: wycats@gmail.com
18
+ executables:
19
+ - thor
20
+ - rake2thor
21
+ extensions: []
22
+
23
+ extra_rdoc_files:
24
+ - README.markdown
25
+ - CHANGELOG.rdoc
26
+ - LICENSE
27
+ files:
28
+ - README.markdown
29
+ - LICENSE
30
+ - CHANGELOG.rdoc
31
+ - Rakefile
32
+ - bin/rake2thor
33
+ - bin/thor
34
+ - lib/thor
35
+ - lib/thor/error.rb
36
+ - lib/thor/options.rb
37
+ - lib/thor/ordered_hash.rb
38
+ - lib/thor/runner.rb
39
+ - lib/thor/task.rb
40
+ - lib/thor/task_hash.rb
41
+ - lib/thor/tasks
42
+ - lib/thor/tasks/package.rb
43
+ - lib/thor/tasks.rb
44
+ - lib/thor/util.rb
45
+ - lib/thor.rb
46
+ has_rdoc: true
47
+ homepage: http://yehudakatz.com
48
+ post_install_message:
49
+ rdoc_options: []
50
+
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ requirements: []
66
+
67
+ rubyforge_project: thor
68
+ rubygems_version: 1.2.0
69
+ signing_key:
70
+ specification_version: 2
71
+ summary: A gem that maps options to a class
72
+ test_files: []
73
+