wycats-thor 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
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,59 @@
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
+ Examples:
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]
17
+ # do something
18
+ end
19
+ end
20
+
21
+ desc "list [SEARCH]", "list all of the available apps, limited by SEARCH"
22
+ def list(search = "")
23
+ # list everything
24
+ end
25
+
26
+ end
27
+
28
+ MyApp.start
29
+
30
+ Thor automatically maps commands as follows:
31
+
32
+ app install name --force
33
+
34
+ That gets converted to:
35
+
36
+ MyApp.new.install("name", :force => true)
37
+
38
+ [1] Inherit from Thor to turn a class into an option mapper
39
+
40
+ [2] Map additional non-valid identifiers to specific methods. In this case,
41
+ convert -L to :list
42
+
43
+ [3] Describe the method immediately below. The first parameter is the usage information,
44
+ 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.
48
+
49
+ Types for `method_options`
50
+ --------------------------
51
+
52
+ <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>
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/getopt.rb ADDED
@@ -0,0 +1,238 @@
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
data/lib/thor.rb ADDED
@@ -0,0 +1,121 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__))
2
+ require "getopt"
3
+ require "thor/util"
4
+ require "thor/task"
5
+ require "thor/task_hash"
6
+
7
+ class Thor
8
+ def self.map(map)
9
+ @map ||= superclass.instance_variable_get("@map") || {}
10
+ map.each do |key, value|
11
+ if key.respond_to?(:each)
12
+ key.each {|subkey| @map[subkey] = value}
13
+ else
14
+ @map[key] = value
15
+ end
16
+ end
17
+ end
18
+
19
+ def self.desc(usage, description)
20
+ @usage, @desc = usage, description
21
+ end
22
+
23
+ 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
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.[](task)
42
+ namespaces = task.split(":")
43
+ klass = Thor::Util.constant_from_thor_path(namespaces[0...-1].join(":"))
44
+ raise Error, "`#{klass}' is not a Thor class" unless klass <= Thor
45
+ klass.tasks[namespaces.last]
46
+ end
47
+
48
+ def self.maxima
49
+ @maxima ||= begin
50
+ max_usage = tasks.map {|_, t| t.usage}.max {|x,y| x.to_s.size <=> y.to_s.size}.size
51
+ 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
53
+ Struct.new(:description, :usage, :opt).new(max_desc, max_usage, max_opts)
54
+ end
55
+ end
56
+
57
+ def self.start(args = ARGV)
58
+ meth = args.first
59
+ meth = @map[meth].to_s if @map && @map[meth]
60
+ meth ||= "help"
61
+
62
+ tasks[meth].parse args[1..-1]
63
+ rescue Thor::Error => e
64
+ $stderr.puts e.message
65
+ end
66
+
67
+ class << self
68
+ protected
69
+ def inherited(klass)
70
+ register_klass_file klass
71
+ end
72
+
73
+ def method_added(meth)
74
+ meth = meth.to_s
75
+ return if !public_instance_methods.include?(meth) || !@usage
76
+ register_klass_file self
77
+
78
+ tasks[meth] = Task.new(meth, @desc, @usage, @method_options)
79
+
80
+ @usage, @desc, @method_options = nil
81
+ end
82
+
83
+ def register_klass_file(klass, file = caller[1].split(":")[0])
84
+ unless self == Thor
85
+ superclass.register_klass_file(klass, file)
86
+ return
87
+ end
88
+
89
+ file_subclasses = subclass_files[File.expand_path(file)]
90
+ file_subclasses << klass unless file_subclasses.include?(klass)
91
+ subclasses << klass unless subclasses.include?(klass)
92
+ end
93
+ end
94
+
95
+ map ["-h", "-?", "--help", "-D"] => :help
96
+
97
+ desc "help [TASK]", "describe available tasks or one specific task"
98
+ def help(task = nil)
99
+ if task
100
+ if task.include? ?:
101
+ task = self.class[task]
102
+ namespace = true
103
+ else
104
+ task = self.class.tasks[task]
105
+ end
106
+
107
+ puts task.formatted_usage(namespace)
108
+ 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
118
+ end
119
+ end
120
+
121
+ end
data/lib/thor/error.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Thor
2
+ class Error < Exception; end
3
+ 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,222 @@
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
19
+ def install(name, opts = {})
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 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 = opts["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
+ yaml[as] = {:filename => Digest::MD5.hexdigest(name + as), :location => name, :constants => constants}
60
+
61
+ save_yaml(yaml)
62
+
63
+ puts "Storing thor file in your system repository"
64
+
65
+ File.open(File.join(thor_root, yaml[as][:filename] + ".thor"), "w") do |file|
66
+ file.puts contents
67
+ end
68
+ end
69
+
70
+ desc "uninstall NAME", "uninstall a named Thor module"
71
+ def uninstall(name)
72
+ yaml = thor_yaml
73
+ raise Error, "Can't find module `#{name}'" unless yaml[name]
74
+
75
+ puts "Uninstalling #{name}."
76
+
77
+ file = File.join(thor_root, "#{yaml[name][:filename]}.thor")
78
+ File.delete(file)
79
+ yaml.delete(name)
80
+ save_yaml(yaml)
81
+
82
+ puts "Done."
83
+ end
84
+
85
+ desc "update NAME", "update a Thor file from its original location"
86
+ def update(name)
87
+ yaml = thor_yaml
88
+ raise Error, "Can't find module `#{name}'" if !yaml[name] || !yaml[name][:location]
89
+
90
+ puts "Updating `#{name}' from #{yaml[name][:location]}"
91
+ install(yaml[name][:location], "as" => name)
92
+ end
93
+
94
+ desc "installed", "list the installed Thor modules and tasks (--internal means list the built-in tasks as well)"
95
+ 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))
99
+ end
100
+
101
+ klasses = Thor.subclasses
102
+ klasses -= [Thor, Thor::Runner] unless opts['internal']
103
+ display_klasses(true, klasses)
104
+ end
105
+
106
+ desc "list [SEARCH]", "list the available thor tasks (--substring means SEARCH can be anywhere in the module)"
107
+ method_options :substring => :boolean
108
+ def list(search = "", options = {})
109
+ initialize_thorfiles
110
+ search = ".*#{search}" if options["substring"]
111
+ search = /^#{search}.*/i
112
+
113
+ display_klasses(false, Thor.subclasses.select {|k|
114
+ Thor::Util.constant_to_thor_path(k.name) =~ search})
115
+ end
116
+
117
+ # Override Thor#help so we can give info about not-yet-loaded tasks
118
+ def help(task = nil)
119
+ initialize_thorfiles(task) if task && task.include?(?:)
120
+ super
121
+ end
122
+
123
+ def method_missing(meth, *args)
124
+ meth = meth.to_s
125
+ super(meth.to_sym, *args) unless meth.include? ?:
126
+
127
+ initialize_thorfiles(meth)
128
+ Thor[meth].parse ARGV[1..-1]
129
+ end
130
+
131
+ private
132
+ def thor_root
133
+ File.join(ENV["HOME"], ".thor")
134
+ end
135
+
136
+ def thor_yaml
137
+ yaml_file = File.join(thor_root, "thor.yml")
138
+ yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file)
139
+ yaml || {}
140
+ end
141
+
142
+ def save_yaml(yaml)
143
+ yaml_file = File.join(thor_root, "thor.yml")
144
+ File.open(yaml_file, "w") {|f| f.puts yaml.to_yaml }
145
+ end
146
+
147
+ def display_klasses(with_modules = false, klasses = Thor.subclasses)
148
+ klasses -= [Thor, Thor::Runner] unless with_modules
149
+ raise Error, "No Thor tasks available" if klasses.empty?
150
+
151
+ if with_modules && !(yaml = thor_yaml).empty?
152
+ max_name = yaml.max {|(xk,xv),(yk,yv)| xk.size <=> yk.size }.first.size
153
+
154
+ print "%-#{max_name + 4}s" % "Modules"
155
+ puts "Namespaces"
156
+ print "%-#{max_name + 4}s" % "-------"
157
+ puts "----------"
158
+
159
+ yaml.each do |name, info|
160
+ print "%-#{max_name + 4}s" % name
161
+ puts info[:constants].map {|c| Thor::Util.constant_to_thor_path(c)}.join(", ")
162
+ end
163
+
164
+ puts
165
+ end
166
+
167
+ puts "Tasks"
168
+ puts "-----"
169
+
170
+ # Calculate the largest base class name
171
+ max_base = klasses.max do |x,y|
172
+ Thor::Util.constant_to_thor_path(x.name).size <=> Thor::Util.constant_to_thor_path(y.name).size
173
+ end.name.size
174
+
175
+ # Calculate the size of the largest option description
176
+ max_left_item = klasses.max do |x,y|
177
+ (x.maxima.usage + x.maxima.opt).to_i <=> (y.maxima.usage + y.maxima.opt).to_i
178
+ end
179
+
180
+ max_left = max_left_item.maxima.usage + max_left_item.maxima.opt
181
+
182
+ klasses.each {|k| display_tasks(k, max_base, max_left)}
183
+ end
184
+
185
+ def display_tasks(klass, max_base, max_left)
186
+ base = Thor::Util.constant_to_thor_path(klass.name)
187
+ klass.tasks.each true do |name, task|
188
+ format_string = "%-#{max_left + max_base + 5}s"
189
+ print format_string % task.formatted_usage(true)
190
+ puts task.description
191
+ end
192
+ end
193
+
194
+ def initialize_thorfiles(relevant_to = nil)
195
+ thorfiles(relevant_to).each {|f| load f unless Thor.subclass_files.keys.include?(File.expand_path(f))}
196
+ end
197
+
198
+ def thorfiles(relevant_to = nil)
199
+ path = Dir.pwd
200
+ thorfiles = []
201
+
202
+ # Look for Thorfile or *.thor in the current directory or a parent directory, until the root
203
+ while thorfiles.empty?
204
+ thorfiles = Dir[*Thor::Runner.globs_for(path)]
205
+ path = File.dirname(path)
206
+ break if path == "/"
207
+ end
208
+
209
+ # We want to load system-wide Thorfiles first
210
+ # so the local Thorfiles will override them.
211
+ (relevant_to ? thorfiles_relevant_to(relevant_to) :
212
+ Dir["#{ENV["HOME"]}/.thor/**/*.thor"]) + thorfiles
213
+ end
214
+
215
+ def thorfiles_relevant_to(meth)
216
+ klass_str = Thor::Util.to_constant(meth.split(":")[0...-1].join(":"))
217
+ thor_yaml.select do |k, v|
218
+ v[:constants] && v[:constants].include?(klass_str)
219
+ end.map { |k, v| File.join(thor_root, "#{v[:filename]}.thor") }
220
+ end
221
+
222
+ end
data/lib/thor/task.rb ADDED
@@ -0,0 +1,74 @@
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(args)
11
+ run(*parse_args(args))
12
+ end
13
+
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)
20
+ rescue ArgumentError => e
21
+ raise e unless e.backtrace.first =~ /:in `#{meth}'$/
22
+ raise Error, "`#{meth}' was called incorrectly. Call as `#{formatted_usage}'"
23
+ rescue NoMethodError => e
24
+ raise e unless e.message =~ /^undefined method `#{meth}' for #<#{klass}:.*>$/
25
+ raise Error, "The #{namespace false} namespace doesn't have a `#{meth}' task"
26
+ end
27
+
28
+ def namespace(remove_default = true)
29
+ Thor::Util.constant_to_thor_path(klass, remove_default)
30
+ end
31
+
32
+ def with_klass(klass)
33
+ new = self.dup
34
+ new.klass = klass
35
+ new
36
+ 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(" ")
49
+ end
50
+
51
+ def formatted_usage(namespace = false)
52
+ (namespace ? self.namespace + ':' : '') + usage +
53
+ (opts ? " " + formatted_opts : "")
54
+ end
55
+
56
+ protected
57
+
58
+ 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]
72
+ end
73
+ end
74
+ 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)
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,70 @@
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
+ desc "install", "install the gem"
18
+ define_method :install do
19
+ old_stderr, $stderr = $stderr.dup, File.open("/dev/null", "w")
20
+ package
21
+ $stderr = old_stderr
22
+ system %{sudo gem install pkg/#{spec.name}-#{spec.version} --no-rdoc --no-ri --no-update-sources}
23
+ end
24
+ end
25
+
26
+ def self.spec_task(file_list, opts = {})
27
+ name = opts.delete(:name) || "spec"
28
+ rcov_dir = opts.delete(:rcov_dir) || "coverage"
29
+ file_list = file_list.map {|f| %["#{f}"]}.join(" ")
30
+ verbose = opts.delete(:verbose)
31
+ opts = {:format => "specdoc", :color => true}.merge(opts)
32
+
33
+ rcov_opts = convert_task_options(opts.delete(:rcov) || {})
34
+ rcov = !rcov_opts.empty?
35
+ options = convert_task_options(opts)
36
+
37
+ if rcov
38
+ FileUtils.rm_rf(File.join(Dir.pwd, rcov_dir))
39
+ end
40
+
41
+ desc(name, "spec task")
42
+ define_method(name) do
43
+ cmd = "ruby "
44
+ if rcov
45
+ cmd << "-S rcov -o #{rcov_dir} #{rcov_opts} "
46
+ end
47
+ cmd << `which spec`.chomp
48
+ cmd << " -- " if rcov
49
+ cmd << " "
50
+ cmd << file_list
51
+ cmd << " "
52
+ cmd << options
53
+ puts cmd if verbose
54
+ system(cmd)
55
+ end
56
+ end
57
+
58
+ private
59
+ def self.convert_task_options(opts)
60
+ opts.map do |key, value|
61
+ if value == true
62
+ "--#{key}"
63
+ elsif value.is_a?(Array)
64
+ value.map {|v| "--#{key} #{v.inspect}"}.join(" ")
65
+ else
66
+ "--#{key} #{value.inspect}"
67
+ end
68
+ end.join(" ")
69
+ end
70
+ 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,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wycats-thor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.2
5
+ platform: ruby
6
+ authors:
7
+ - Yehuda Katz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-05-19 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
+ - LICENSE
26
+ files:
27
+ - LICENSE
28
+ - README.markdown
29
+ - Rakefile
30
+ - bin/rake2thor
31
+ - bin/thor
32
+ - lib/getopt.rb
33
+ - lib/thor
34
+ - lib/thor/error.rb
35
+ - lib/thor/ordered_hash.rb
36
+ - lib/thor/runner.rb
37
+ - lib/thor/task.rb
38
+ - lib/thor/task_hash.rb
39
+ - lib/thor/tasks.rb
40
+ - lib/thor/util.rb
41
+ - lib/thor.rb
42
+ has_rdoc: true
43
+ homepage: http://yehudakatz.com
44
+ post_install_message:
45
+ rdoc_options: []
46
+
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ requirements: []
62
+
63
+ rubyforge_project: thor
64
+ rubygems_version: 1.0.1
65
+ signing_key:
66
+ specification_version: 2
67
+ summary: A gem that maps options to a class
68
+ test_files: []
69
+