josevalim-thor 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.rdoc +73 -0
- data/LICENSE +20 -0
- data/README.markdown +76 -0
- data/Rakefile +6 -0
- data/bin/rake2thor +87 -0
- data/bin/thor +7 -0
- data/lib/thor.rb +229 -0
- data/lib/thor/actions.rb +147 -0
- data/lib/thor/actions/commands.rb +61 -0
- data/lib/thor/actions/copy_file.rb +32 -0
- data/lib/thor/actions/create_file.rb +48 -0
- data/lib/thor/actions/directory.rb +36 -0
- data/lib/thor/actions/empty_directory.rb +30 -0
- data/lib/thor/actions/get.rb +58 -0
- data/lib/thor/actions/gsub_file.rb +77 -0
- data/lib/thor/actions/inject_into_file.rb +93 -0
- data/lib/thor/actions/template.rb +37 -0
- data/lib/thor/actions/templater.rb +163 -0
- data/lib/thor/base.rb +447 -0
- data/lib/thor/core_ext/hash_with_indifferent_access.rb +59 -0
- data/lib/thor/core_ext/ordered_hash.rb +133 -0
- data/lib/thor/error.rb +27 -0
- data/lib/thor/group.rb +90 -0
- data/lib/thor/option.rb +210 -0
- data/lib/thor/options.rb +282 -0
- data/lib/thor/runner.rb +296 -0
- data/lib/thor/shell/basic.rb +198 -0
- data/lib/thor/task.rb +85 -0
- data/lib/thor/tasks.rb +3 -0
- data/lib/thor/tasks/install.rb +35 -0
- data/lib/thor/tasks/package.rb +31 -0
- data/lib/thor/tasks/spec.rb +70 -0
- data/lib/thor/util.rb +209 -0
- metadata +93 -0
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
class Thor
|
4
|
+
module Shell
|
5
|
+
class Basic
|
6
|
+
attr_accessor :base
|
7
|
+
|
8
|
+
# Ask something to the user and receives a response.
|
9
|
+
#
|
10
|
+
# ==== Example
|
11
|
+
# ask("What is your name?")
|
12
|
+
#
|
13
|
+
def ask(statement, color=nil)
|
14
|
+
say("#{statement} ", color)
|
15
|
+
$stdin.gets.strip
|
16
|
+
end
|
17
|
+
|
18
|
+
# Say (print) something to the user. If the sentence ends with a whitespace
|
19
|
+
# or tab character, a new line is not appended (print + flush). Otherwise
|
20
|
+
# are passed straight to puts (behavior got from Highline).
|
21
|
+
#
|
22
|
+
# ==== Example
|
23
|
+
# say("I know you knew that.")
|
24
|
+
#
|
25
|
+
def say(statement="", color=nil)
|
26
|
+
statement = statement.to_s
|
27
|
+
|
28
|
+
if statement[-1, 1] == " " || statement[-1, 1] == "\t"
|
29
|
+
$stdout.print(statement)
|
30
|
+
$stdout.flush
|
31
|
+
else
|
32
|
+
$stdout.puts(statement)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Say a status with the given color and appends the message.
|
37
|
+
# It does not show the status if the base is set to quiet.
|
38
|
+
#
|
39
|
+
def say_status(status, message, color=nil)
|
40
|
+
return if base && base.options[:quiet]
|
41
|
+
|
42
|
+
status_flag = "[#{status.to_s.upcase}]".rjust(12)
|
43
|
+
say "#{status_flag} #{message}"
|
44
|
+
end
|
45
|
+
|
46
|
+
# Make a question the to user and returns true if the user replies "y" or
|
47
|
+
# "yes".
|
48
|
+
#
|
49
|
+
def yes?(statement, color=nil)
|
50
|
+
ask(statement, color) =~ is?(:yes)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Make a question the to user and returns true if the user replies "n" or
|
54
|
+
# "no".
|
55
|
+
#
|
56
|
+
def no?(statement, color=nil)
|
57
|
+
!yes?(statement, color)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Prints a list of items.
|
61
|
+
#
|
62
|
+
# ==== Parameters
|
63
|
+
# list<Array[String, String, ...]>
|
64
|
+
# mode<Symbol>:: Can be :rows or :inline. Defaults to :rows.
|
65
|
+
#
|
66
|
+
def print_list(list, mode=:rows)
|
67
|
+
return if list.empty?
|
68
|
+
|
69
|
+
content = case mode
|
70
|
+
when :inline
|
71
|
+
last = list.pop
|
72
|
+
"#{list.join(", ")}, and #{last}"
|
73
|
+
else # rows
|
74
|
+
list.join("\n")
|
75
|
+
end
|
76
|
+
|
77
|
+
$stdout.puts content
|
78
|
+
end
|
79
|
+
|
80
|
+
# Prints a table.
|
81
|
+
#
|
82
|
+
# ==== Parameters
|
83
|
+
# Array[Array[String, String, ...]]
|
84
|
+
#
|
85
|
+
# ==== Options
|
86
|
+
# ident<Integer>:: Ident the first column by ident value.
|
87
|
+
# emphasize_last<Boolean>:: When true, add a different behavior to the last column.
|
88
|
+
#
|
89
|
+
def print_table(table, options={})
|
90
|
+
return if table.empty?
|
91
|
+
|
92
|
+
formats = []
|
93
|
+
0.upto(table.first.length - 2) do |i|
|
94
|
+
maxima = table.max{ |a,b| a[i].size <=> b[i].size }[i].size
|
95
|
+
formats << "%-#{maxima + 2}s"
|
96
|
+
end
|
97
|
+
|
98
|
+
formats[0] = formats[0].insert(0, " " * options[:ident]) if options[:ident]
|
99
|
+
formats << "%s"
|
100
|
+
|
101
|
+
if options[:emphasize_last]
|
102
|
+
table.each do |row|
|
103
|
+
next if row[-1].empty?
|
104
|
+
row[-1] = "# #{row[-1]}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
table.each do |row|
|
109
|
+
row.each_with_index do |column, i|
|
110
|
+
$stdout.print formats[i] % column.to_s
|
111
|
+
end
|
112
|
+
$stdout.puts
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Deals with file collision and returns true if the file should be
|
117
|
+
# overwriten and false otherwise. If a block is given, it uses the block
|
118
|
+
# response as the content for the diff.
|
119
|
+
#
|
120
|
+
# ==== Parameters
|
121
|
+
# destination<String>:: the destination file to solve conflicts
|
122
|
+
# block<Proc>:: an optional proc that returns the value to be used in diff
|
123
|
+
#
|
124
|
+
def file_collision(destination)
|
125
|
+
return true if @always_force
|
126
|
+
|
127
|
+
options = block_given? ? "[Ynaqdh]" : "[Ynaqh]"
|
128
|
+
answer = ask %[Overwrite #{destination}? (enter "h" for help) #{options}]
|
129
|
+
|
130
|
+
case answer
|
131
|
+
when is?(:yes), is?(:force)
|
132
|
+
true
|
133
|
+
when is?(:no), is?(:skip)
|
134
|
+
false
|
135
|
+
when is?(:always)
|
136
|
+
@always_force = true
|
137
|
+
when is?(:quit)
|
138
|
+
say 'Aborting...'
|
139
|
+
raise SystemExit
|
140
|
+
when is?(:diff)
|
141
|
+
show_diff(destination, yield) if block_given?
|
142
|
+
say 'Retrying...'
|
143
|
+
raise ScriptError
|
144
|
+
else
|
145
|
+
say file_collision_help
|
146
|
+
raise ScriptError
|
147
|
+
end
|
148
|
+
rescue ScriptError
|
149
|
+
retry
|
150
|
+
end
|
151
|
+
|
152
|
+
# Called if something goes wrong during the execution. This is used by Thor
|
153
|
+
# internally and should not be used inside your scripts. If someone went
|
154
|
+
# wrong, you can always raise an exception. If you raise a Thor::Error, it
|
155
|
+
# will be rescued and wrapped in the method below.
|
156
|
+
#
|
157
|
+
def error(statement) #:nodoc:
|
158
|
+
$stderr.puts statement
|
159
|
+
end
|
160
|
+
|
161
|
+
protected
|
162
|
+
|
163
|
+
def is?(value)
|
164
|
+
value = value.to_s
|
165
|
+
|
166
|
+
if value.size == 1
|
167
|
+
/\A#{value}\z/i
|
168
|
+
else
|
169
|
+
/\A(#{value}|#{value[0,1]})\z/i
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def file_collision_help
|
174
|
+
<<HELP
|
175
|
+
Y - yes, overwrite
|
176
|
+
n - no, do not overwrite
|
177
|
+
a - all, overwrite this and all others
|
178
|
+
q - quit, abort
|
179
|
+
d - diff, show the differences between the old and the new
|
180
|
+
h - help, show this help
|
181
|
+
HELP
|
182
|
+
end
|
183
|
+
|
184
|
+
def show_diff(destination, content)
|
185
|
+
Tempfile.open(File.basename(destination), File.dirname(destination)) do |temp|
|
186
|
+
temp.write content
|
187
|
+
temp.rewind
|
188
|
+
say `#{diff_cmd} "#{destination}" "#{temp.path}"`
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def diff_cmd
|
193
|
+
ENV['THOR_DIFF'] || ENV['RAILS_DIFF'] || 'diff -u'
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
data/lib/thor/task.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
class Thor
|
2
|
+
class Task < Struct.new(:name, :description, :usage, :options)
|
3
|
+
|
4
|
+
# Creates a dynamic task. Dynamic tasks are created on demand to allow method
|
5
|
+
# missing calls (since a method missing does not have a task object for it).
|
6
|
+
#
|
7
|
+
def self.dynamic(name)
|
8
|
+
new(name, "A dynamically-generated task", name.to_s, nil)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(name, description, usage, options)
|
12
|
+
super(name, description, usage, options || {})
|
13
|
+
end
|
14
|
+
|
15
|
+
# Dup the options hash on clone.
|
16
|
+
#
|
17
|
+
def initialize_copy(other)
|
18
|
+
super(other)
|
19
|
+
self.options = other.options.dup if other.options
|
20
|
+
end
|
21
|
+
|
22
|
+
# By default, a task invokes a method in the thor class. You can change this
|
23
|
+
# implementation to create custom tasks.
|
24
|
+
#
|
25
|
+
def run(instance, *args)
|
26
|
+
raise UndefinedTaskError, "the '#{name}' task of #{instance.class} is private" unless public_method?(instance)
|
27
|
+
instance.send(name, *args)
|
28
|
+
rescue ArgumentError => e
|
29
|
+
backtrace = sans_backtrace(e.backtrace, caller)
|
30
|
+
|
31
|
+
if instance.is_a?(Thor) && backtrace.empty?
|
32
|
+
raise InvocationError, "'#{name}' was called incorrectly. Call as '#{formatted_usage(instance.class)}'"
|
33
|
+
else
|
34
|
+
raise e
|
35
|
+
end
|
36
|
+
rescue NoMethodError => e
|
37
|
+
if e.message =~ /^undefined method `#{name}' for #{Regexp.escape(instance.to_s)}$/
|
38
|
+
raise UndefinedTaskError, "The #{instance.class.namespace} namespace doesn't have a '#{name}' task"
|
39
|
+
else
|
40
|
+
raise e
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the first line of the given description.
|
45
|
+
#
|
46
|
+
def short_description
|
47
|
+
description.split("\n").first if description
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the formatted usage. If a klass is given, the klass default options
|
51
|
+
# are merged with the task options providinf a full description.
|
52
|
+
#
|
53
|
+
def formatted_usage(klass=nil)
|
54
|
+
formatted = ''
|
55
|
+
formatted << "#{klass.namespace.gsub(/^default/,'')}:" if klass
|
56
|
+
formatted << usage.to_s
|
57
|
+
formatted << " #{formatted_options}"
|
58
|
+
formatted.strip!
|
59
|
+
formatted
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the options usage for this task.
|
63
|
+
#
|
64
|
+
def formatted_options
|
65
|
+
@formatted_options ||= options.values.sort.map{ |o| o.usage }.join(" ")
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
|
70
|
+
# Given a target, checks if this class name is not a private/protected method.
|
71
|
+
#
|
72
|
+
def public_method?(instance)
|
73
|
+
!(instance.private_methods + instance.protected_methods).include?(name.to_s)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Clean everything that comes from the Thor gempath and remove the caller.
|
77
|
+
#
|
78
|
+
def sans_backtrace(backtrace, caller)
|
79
|
+
dirname = /^#{Regexp.escape(File.dirname(__FILE__))}/
|
80
|
+
saned = backtrace.reject { |frame| frame =~ dirname }
|
81
|
+
saned -= caller
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
data/lib/thor/tasks.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
class Thor
|
2
|
+
# Creates an install task.
|
3
|
+
#
|
4
|
+
# ==== Parameters
|
5
|
+
# spec<Gem::Specification>
|
6
|
+
#
|
7
|
+
# ==== Options
|
8
|
+
# :dir - The directory where the package is hold before installation. Defaults to ./pkg.
|
9
|
+
#
|
10
|
+
def self.install_task(spec, options={})
|
11
|
+
package_task(spec, options)
|
12
|
+
tasks['install'] = Thor::InstallTask.new(spec, options)
|
13
|
+
end
|
14
|
+
|
15
|
+
class InstallTask < Task
|
16
|
+
attr_accessor :spec, :config
|
17
|
+
|
18
|
+
def initialize(gemspec, config={})
|
19
|
+
super(:install, "Install the gem", "install", {})
|
20
|
+
@spec = gemspec
|
21
|
+
@config = { :dir => File.join(Dir.pwd, "pkg") }.merge(config)
|
22
|
+
end
|
23
|
+
|
24
|
+
def run(instance, args=[])
|
25
|
+
null, sudo, gem = RUBY_PLATFORM =~ /mswin|mingw/ ? ['NUL', '', 'gem.bat'] :
|
26
|
+
['/dev/null', 'sudo', 'gem']
|
27
|
+
|
28
|
+
old_stderr, $stderr = $stderr.dup, File.open(null, "w")
|
29
|
+
instance.invoke(:package)
|
30
|
+
$stderr = old_stderr
|
31
|
+
|
32
|
+
system %{#{sudo} #{Gem.ruby} -S #{gem} install #{config[:dir]}/#{spec.name}-#{spec.version} --no-rdoc --no-ri --no-update-sources}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
class Thor
|
4
|
+
# Creates a package task.
|
5
|
+
#
|
6
|
+
# ==== Parameters
|
7
|
+
# spec<Gem::Specification>
|
8
|
+
#
|
9
|
+
# ==== Options
|
10
|
+
# :dir - The package directory. Defaults to ./pkg.
|
11
|
+
#
|
12
|
+
def self.package_task(spec, options={})
|
13
|
+
tasks['package'] = Thor::PackageTask.new(spec, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
class PackageTask < Task
|
17
|
+
attr_accessor :spec, :config
|
18
|
+
|
19
|
+
def initialize(gemspec, config={})
|
20
|
+
super(:package, "Build a gem package", "package", {})
|
21
|
+
@spec = gemspec
|
22
|
+
@config = {:dir => File.join(Dir.pwd, "pkg")}.merge(config)
|
23
|
+
end
|
24
|
+
|
25
|
+
def run(instance, args=[])
|
26
|
+
FileUtils.mkdir_p(config[:dir])
|
27
|
+
Gem::Builder.new(spec).build
|
28
|
+
FileUtils.mv(spec.file_name, File.join(config[:dir], spec.file_name))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
class Thor
|
4
|
+
# Creates a spec task.
|
5
|
+
#
|
6
|
+
# ==== Parameters
|
7
|
+
# files<Array> - Array of files to spec
|
8
|
+
#
|
9
|
+
# ==== Options
|
10
|
+
# :name - The name of the task. It can be rcov or spec. Spec is the default.
|
11
|
+
# :rcov - A hash with rcov specific options.
|
12
|
+
# :rcov_dir - Where rcov reports should be printed.
|
13
|
+
# :verbose - Sets the default value for verbose, although it can be specified
|
14
|
+
# also through the command line.
|
15
|
+
#
|
16
|
+
# All other options are added to rspec.
|
17
|
+
#
|
18
|
+
def self.spec_task(files, options={})
|
19
|
+
name = (options.delete(:name) || 'spec').to_s
|
20
|
+
tasks[name] = Thor::SpecTask.new(name, files, options)
|
21
|
+
end
|
22
|
+
|
23
|
+
class SpecTask < Task
|
24
|
+
attr_accessor :name, :files, :rcov_dir, :rcov_config, :spec_config
|
25
|
+
|
26
|
+
def initialize(name, files, config={})
|
27
|
+
options = { :verbose => Thor::Option.parse(:verbose, config.delete(:verbose) || false) }
|
28
|
+
super(name, "#{name.capitalize} task", name, options)
|
29
|
+
|
30
|
+
@name = name
|
31
|
+
@files = files.map{ |f| %["#{f}"] }.join(" ")
|
32
|
+
@rcov_dir = config.delete(:rdoc_dir) || File.join(Dir.pwd, 'coverage')
|
33
|
+
@rcov_config = config.delete(:rcov) || {}
|
34
|
+
@spec_config = { :format => 'specdoc', :color => true }.merge(config)
|
35
|
+
end
|
36
|
+
|
37
|
+
def run(instance, args=[])
|
38
|
+
rcov_opts = Thor::Options.to_switches(rcov_config)
|
39
|
+
spec_opts = Thor::Options.to_switches(spec_config)
|
40
|
+
|
41
|
+
require 'rbconfig'
|
42
|
+
cmd = RbConfig::CONFIG['ruby_install_name'] << " "
|
43
|
+
|
44
|
+
if rcov?
|
45
|
+
FileUtils.rm_rf(rcov_dir)
|
46
|
+
cmd << "-S #{where('rcov')} -o #{rcov_dir} #{rcov_opts} "
|
47
|
+
end
|
48
|
+
|
49
|
+
cmd << [where('spec'), rcov? ? " -- " : nil, files, spec_opts].join(" ")
|
50
|
+
|
51
|
+
puts cmd if instance.options.verbose?
|
52
|
+
system(cmd)
|
53
|
+
exit($?.exitstatus)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def rcov?
|
59
|
+
name == "rcov"
|
60
|
+
end
|
61
|
+
|
62
|
+
def where(file)
|
63
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
64
|
+
file_with_path = File.join(path, file)
|
65
|
+
next unless File.exist?(file_with_path) && File.executable?(file_with_path)
|
66
|
+
return File.expand_path(file_with_path)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/thor/util.rb
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
class Thor
|
2
|
+
module Sandbox; end
|
3
|
+
|
4
|
+
# This module holds several utilities:
|
5
|
+
#
|
6
|
+
# 1) Methods to convert thor namespaces to constants and vice-versa.
|
7
|
+
#
|
8
|
+
# Thor::Utils.constant_to_namespace(Foo::Bar::Baz) #=> "foo:bar:baz"
|
9
|
+
# Thor::Utils.namespace_to_constant("foo:bar:baz") #=> Foo::Bar::Baz
|
10
|
+
#
|
11
|
+
# 2) Loading thor files and sandboxing:
|
12
|
+
#
|
13
|
+
# Thor::Utils.load_thorfile("~/.thor/foo")
|
14
|
+
#
|
15
|
+
module Util
|
16
|
+
|
17
|
+
# Receives a namespace and search for it in the Thor::Base subclasses.
|
18
|
+
#
|
19
|
+
# ==== Parameters
|
20
|
+
# namespace<String>:: The namespace to search for.
|
21
|
+
#
|
22
|
+
def self.find_by_namespace(namespace)
|
23
|
+
namespace = 'default' if namespace.empty?
|
24
|
+
|
25
|
+
Thor::Base.subclasses.find do |klass|
|
26
|
+
klass.namespace == namespace
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Receives a constant and converts it to a Thor namespace. Since Thor tasks
|
31
|
+
# can be added to a sandbox, this method is also responsable for removing
|
32
|
+
# the sandbox namespace.
|
33
|
+
#
|
34
|
+
# This method should not be used in general because it's used to deal with
|
35
|
+
# older versions of Thor. On current versions, if you need to get the
|
36
|
+
# namespace from a class, just call namespace on it.
|
37
|
+
#
|
38
|
+
# TODO Deprecate this method in the future.
|
39
|
+
#
|
40
|
+
# ==== Parameters
|
41
|
+
# constant<Object>:: The constant to be converted to the thor path.
|
42
|
+
#
|
43
|
+
# ==== Returns
|
44
|
+
# String:: If we receive Foo::Bar::Baz it returns "foo:bar:baz"
|
45
|
+
#
|
46
|
+
def self.constant_to_namespace(constant, remove_default=true)
|
47
|
+
constant = constant.to_s.gsub(/^Thor::Sandbox::/, "")
|
48
|
+
constant = snake_case(constant).squeeze(":")
|
49
|
+
constant.gsub!(/^default/, '') if remove_default
|
50
|
+
constant
|
51
|
+
end
|
52
|
+
|
53
|
+
# Given the contents, evaluate it inside the sandbox and returns the thor
|
54
|
+
# classes defined in the sandbox.
|
55
|
+
#
|
56
|
+
# ==== Parameters
|
57
|
+
# contents<String>
|
58
|
+
#
|
59
|
+
# ==== Returns
|
60
|
+
# Array[Object]
|
61
|
+
#
|
62
|
+
def self.namespaces_in_contents(contents, file=__FILE__)
|
63
|
+
old_constants = Thor::Base.subclasses.dup
|
64
|
+
Thor::Base.subclasses.clear
|
65
|
+
|
66
|
+
load_thorfile(file, contents)
|
67
|
+
|
68
|
+
new_constants = Thor::Base.subclasses.dup
|
69
|
+
Thor::Base.subclasses.replace(old_constants)
|
70
|
+
|
71
|
+
new_constants.map!{ |c| c.namespace }
|
72
|
+
new_constants.compact!
|
73
|
+
new_constants
|
74
|
+
end
|
75
|
+
|
76
|
+
# Receives a string and convert it to snake case. SnakeCase returns snake_case.
|
77
|
+
#
|
78
|
+
# ==== Parameters
|
79
|
+
# String
|
80
|
+
#
|
81
|
+
# ==== Returns
|
82
|
+
# String
|
83
|
+
#
|
84
|
+
def self.snake_case(str)
|
85
|
+
return str.downcase if str =~ /^[A-Z_]+$/
|
86
|
+
str.gsub(/\B[A-Z]/, '_\&').squeeze('_') =~ /_*(.*)/
|
87
|
+
return $+.downcase
|
88
|
+
end
|
89
|
+
|
90
|
+
# Receives a namespace and tries to retrieve a Thor or Thor::Group class
|
91
|
+
# from it. It first searches for a class using the all the given namespace,
|
92
|
+
# if it's not found, removes the highest entry and searches for the class
|
93
|
+
# again. If found, returns the highest entry as the class name.
|
94
|
+
#
|
95
|
+
# ==== Examples
|
96
|
+
#
|
97
|
+
# class Foo::Bar < Thor
|
98
|
+
# def baz
|
99
|
+
# end
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# class Baz::Foo < Thor::Group
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# Thor::Util.namespace_to_thor_class("foo:bar") #=> Foo::Bar, nil # will invoke default task
|
106
|
+
# Thor::Util.namespace_to_thor_class("baz:foo") #=> Baz::Foo, nil
|
107
|
+
# Thor::Util.namespace_to_thor_class("foo:bar:baz") #=> Foo::Bar, "baz"
|
108
|
+
#
|
109
|
+
# ==== Parameters
|
110
|
+
# namespace<String>
|
111
|
+
#
|
112
|
+
# ==== Errors
|
113
|
+
# Thor::Error:: raised if the namespace cannot be found.
|
114
|
+
#
|
115
|
+
# Thor::Error:: raised if the namespace evals to a class which does not
|
116
|
+
# inherit from Thor or Thor::Group.
|
117
|
+
#
|
118
|
+
def self.namespace_to_thor_class(namespace)
|
119
|
+
klass, task_name = Thor::Util.find_by_namespace(namespace), nil
|
120
|
+
|
121
|
+
if klass.nil? && namespace.include?(?:)
|
122
|
+
namespace = namespace.split(":")
|
123
|
+
task_name = namespace.pop
|
124
|
+
klass = Thor::Util.find_by_namespace(namespace.join(":"))
|
125
|
+
end
|
126
|
+
|
127
|
+
raise Error, "could not find Thor class or task '#{namespace}'" unless klass
|
128
|
+
|
129
|
+
return klass, task_name
|
130
|
+
end
|
131
|
+
|
132
|
+
# Receives a path and load the thor file in the path. The file is evaluated
|
133
|
+
# inside the sandbox to avoid namespacing conflicts.
|
134
|
+
#
|
135
|
+
def self.load_thorfile(path, content=nil)
|
136
|
+
content ||= File.read(path)
|
137
|
+
|
138
|
+
begin
|
139
|
+
Thor::Sandbox.class_eval(content, path)
|
140
|
+
rescue Exception => e
|
141
|
+
$stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Receives a yaml (hash) and updates all constants entries to namespace.
|
146
|
+
# This was added to deal with deprecated versions of Thor.
|
147
|
+
#
|
148
|
+
# TODO Deprecate this method in the future.
|
149
|
+
#
|
150
|
+
# ==== Returns
|
151
|
+
# TrueClass|FalseClass:: Returns true if any change to the yaml file was made.
|
152
|
+
#
|
153
|
+
def self.convert_constants_to_namespaces(yaml)
|
154
|
+
yaml_changed = false
|
155
|
+
|
156
|
+
yaml.each do |k, v|
|
157
|
+
next unless v[:constants] && v[:namespaces].nil?
|
158
|
+
yaml_changed = true
|
159
|
+
yaml[k][:namespaces] = v[:constants].map{|c| Thor::Util.constant_to_namespace(c)}
|
160
|
+
end
|
161
|
+
|
162
|
+
yaml_changed
|
163
|
+
end
|
164
|
+
|
165
|
+
# Returns the root where thor files are located, dependending on the OS.
|
166
|
+
#
|
167
|
+
def self.thor_root
|
168
|
+
return File.join(ENV["HOME"], '.thor') if ENV["HOME"]
|
169
|
+
|
170
|
+
if ENV["HOMEDRIVE"] && ENV["HOMEPATH"]
|
171
|
+
return File.join(ENV["HOMEDRIVE"], ENV["HOMEPATH"], '.thor')
|
172
|
+
end
|
173
|
+
|
174
|
+
return File.join(ENV["APPDATA"], '.thor') if ENV["APPDATA"]
|
175
|
+
|
176
|
+
begin
|
177
|
+
File.expand_path("~")
|
178
|
+
rescue
|
179
|
+
if File::ALT_SEPARATOR
|
180
|
+
"C:/"
|
181
|
+
else
|
182
|
+
"/"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Returns the files in the thor root. On Windows thor_root will be something
|
188
|
+
# like this:
|
189
|
+
#
|
190
|
+
# C:\Documents and Settings\james\.thor
|
191
|
+
#
|
192
|
+
# If we don't #gsub the \ character, Dir.glob will fail.
|
193
|
+
#
|
194
|
+
def self.thor_root_glob
|
195
|
+
files = Dir["#{thor_root.gsub(/\\/, '/')}/*"]
|
196
|
+
|
197
|
+
files.map! do |file|
|
198
|
+
File.directory?(file) ? File.join(file, "main.thor") : file
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Where to look for Thor files.
|
203
|
+
#
|
204
|
+
def self.globs_for(path)
|
205
|
+
["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"]
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
end
|