thor 0.9.9 → 0.11.5
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +29 -4
- data/README.rdoc +234 -0
- data/Thorfile +57 -0
- data/VERSION +1 -0
- data/bin/rake2thor +4 -0
- data/bin/thor +1 -1
- data/lib/thor.rb +216 -119
- data/lib/thor/actions.rb +272 -0
- data/lib/thor/actions/create_file.rb +102 -0
- data/lib/thor/actions/directory.rb +87 -0
- data/lib/thor/actions/empty_directory.rb +133 -0
- data/lib/thor/actions/file_manipulation.rb +195 -0
- data/lib/thor/actions/inject_into_file.rb +78 -0
- data/lib/thor/base.rb +510 -0
- data/lib/thor/core_ext/hash_with_indifferent_access.rb +75 -0
- data/lib/thor/core_ext/ordered_hash.rb +100 -0
- data/lib/thor/error.rb +25 -1
- data/lib/thor/group.rb +263 -0
- data/lib/thor/invocation.rb +178 -0
- data/lib/thor/parser.rb +4 -0
- data/lib/thor/parser/argument.rb +67 -0
- data/lib/thor/parser/arguments.rb +145 -0
- data/lib/thor/parser/option.rb +132 -0
- data/lib/thor/parser/options.rb +142 -0
- data/lib/thor/rake_compat.rb +67 -0
- data/lib/thor/runner.rb +232 -242
- data/lib/thor/shell.rb +72 -0
- data/lib/thor/shell/basic.rb +220 -0
- data/lib/thor/shell/color.rb +108 -0
- data/lib/thor/task.rb +97 -60
- data/lib/thor/util.rb +230 -55
- data/spec/actions/create_file_spec.rb +170 -0
- data/spec/actions/directory_spec.rb +118 -0
- data/spec/actions/empty_directory_spec.rb +91 -0
- data/spec/actions/file_manipulation_spec.rb +242 -0
- data/spec/actions/inject_into_file_spec.rb +80 -0
- data/spec/actions_spec.rb +291 -0
- data/spec/base_spec.rb +236 -0
- data/spec/core_ext/hash_with_indifferent_access_spec.rb +43 -0
- data/spec/core_ext/ordered_hash_spec.rb +115 -0
- data/spec/fixtures/bundle/execute.rb +6 -0
- data/spec/fixtures/doc/config.rb +1 -0
- data/spec/group_spec.rb +177 -0
- data/spec/invocation_spec.rb +107 -0
- data/spec/parser/argument_spec.rb +47 -0
- data/spec/parser/arguments_spec.rb +64 -0
- data/spec/parser/option_spec.rb +212 -0
- data/spec/parser/options_spec.rb +255 -0
- data/spec/rake_compat_spec.rb +64 -0
- data/spec/runner_spec.rb +204 -0
- data/spec/shell/basic_spec.rb +206 -0
- data/spec/shell/color_spec.rb +41 -0
- data/spec/shell_spec.rb +25 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/task_spec.rb +82 -0
- data/spec/thor_spec.rb +234 -0
- data/spec/util_spec.rb +196 -0
- metadata +69 -25
- data/README.markdown +0 -76
- data/Rakefile +0 -6
- data/lib/thor/options.rb +0 -242
- data/lib/thor/ordered_hash.rb +0 -64
- data/lib/thor/task_hash.rb +0 -22
- data/lib/thor/tasks.rb +0 -77
- data/lib/thor/tasks/package.rb +0 -18
data/lib/thor/util.rb
CHANGED
@@ -1,75 +1,250 @@
|
|
1
|
-
require '
|
1
|
+
require 'rbconfig'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
class << self
|
6
|
-
|
7
|
-
# @return <Array[Class]> All the classes in the object space.
|
8
|
-
def classes
|
9
|
-
klasses = []
|
10
|
-
ObjectSpace.each_object(Class) {|o| klasses << o}
|
11
|
-
klasses
|
12
|
-
end
|
3
|
+
class Thor
|
4
|
+
module Sandbox #:nodoc:
|
13
5
|
end
|
14
|
-
|
15
|
-
end
|
16
6
|
|
17
|
-
|
18
|
-
|
19
|
-
|
7
|
+
# This module holds several utilities:
|
8
|
+
#
|
9
|
+
# 1) Methods to convert thor namespaces to constants and vice-versa.
|
10
|
+
#
|
11
|
+
# Thor::Utils.namespace_from_thor_class(Foo::Bar::Baz) #=> "foo:bar:baz"
|
12
|
+
#
|
13
|
+
# 2) Loading thor files and sandboxing:
|
14
|
+
#
|
15
|
+
# Thor::Utils.load_thorfile("~/.thor/foo")
|
16
|
+
#
|
20
17
|
module Util
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
18
|
+
|
19
|
+
# Receives a namespace and search for it in the Thor::Base subclasses.
|
20
|
+
#
|
21
|
+
# ==== Parameters
|
22
|
+
# namespace<String>:: The namespace to search for.
|
23
|
+
#
|
24
|
+
def self.find_by_namespace(namespace)
|
25
|
+
namespace = "default#{namespace}" if namespace.empty? || namespace =~ /^:/
|
26
|
+
|
27
|
+
Thor::Base.subclasses.find do |klass|
|
28
|
+
klass.namespace == namespace
|
29
29
|
end
|
30
|
-
obj
|
31
|
-
end
|
32
|
-
|
33
|
-
def self.constant_to_thor_path(str, remove_default = true)
|
34
|
-
str = str.to_s.gsub(/^Thor::Tasks::/, "")
|
35
|
-
str = snake_case(str).squeeze(":")
|
36
|
-
str.gsub!(/^default/, '') if remove_default
|
37
|
-
str
|
38
30
|
end
|
39
31
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
32
|
+
# Receives a constant and converts it to a Thor namespace. Since Thor tasks
|
33
|
+
# can be added to a sandbox, this method is also responsable for removing
|
34
|
+
# the sandbox namespace.
|
35
|
+
#
|
36
|
+
# This method should not be used in general because it's used to deal with
|
37
|
+
# older versions of Thor. On current versions, if you need to get the
|
38
|
+
# namespace from a class, just call namespace on it.
|
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.namespace_from_thor_class(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
|
45
51
|
end
|
46
52
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
53
|
+
# Given the contents, evaluate it inside the sandbox and returns the
|
54
|
+
# namespaces defined in the sandbox.
|
55
|
+
#
|
56
|
+
# ==== Parameters
|
57
|
+
# contents<String>
|
58
|
+
#
|
59
|
+
# ==== Returns
|
60
|
+
# Array[Object]
|
61
|
+
#
|
62
|
+
def self.namespaces_in_content(contents, file=__FILE__)
|
63
|
+
old_constants = Thor::Base.subclasses.dup
|
64
|
+
Thor::Base.subclasses.clear
|
51
65
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
58
74
|
end
|
59
75
|
|
60
|
-
|
61
|
-
|
62
|
-
|
76
|
+
# Returns the thor classes declared inside the given class.
|
77
|
+
#
|
78
|
+
def self.thor_classes_in(klass)
|
79
|
+
Thor::Base.subclasses.select do |subclass|
|
80
|
+
klass.constants.include?(subclass.name.gsub("#{klass.name}::", ''))
|
63
81
|
end
|
64
|
-
return full_const_get(which, str) if which
|
65
|
-
raise NameError, "uninitialized constant #{str}"
|
66
82
|
end
|
67
|
-
|
83
|
+
|
84
|
+
# Receives a string and convert it to snake case. SnakeCase returns snake_case.
|
85
|
+
#
|
86
|
+
# ==== Parameters
|
87
|
+
# String
|
88
|
+
#
|
89
|
+
# ==== Returns
|
90
|
+
# String
|
91
|
+
#
|
68
92
|
def self.snake_case(str)
|
69
93
|
return str.downcase if str =~ /^[A-Z_]+$/
|
70
94
|
str.gsub(/\B[A-Z]/, '_\&').squeeze('_') =~ /_*(.*)/
|
71
95
|
return $+.downcase
|
72
|
-
end
|
73
|
-
|
96
|
+
end
|
97
|
+
|
98
|
+
# Receives a string and convert it to camel case. camel_case returns CamelCase.
|
99
|
+
#
|
100
|
+
# ==== Parameters
|
101
|
+
# String
|
102
|
+
#
|
103
|
+
# ==== Returns
|
104
|
+
# String
|
105
|
+
#
|
106
|
+
def self.camel_case(str)
|
107
|
+
return str if str !~ /_/ && str =~ /[A-Z]+.*/
|
108
|
+
str.split('_').map { |i| i.capitalize }.join
|
109
|
+
end
|
110
|
+
|
111
|
+
# Receives a namespace and tries to retrieve a Thor or Thor::Group class
|
112
|
+
# from it. It first searches for a class using the all the given namespace,
|
113
|
+
# if it's not found, removes the highest entry and searches for the class
|
114
|
+
# again. If found, returns the highest entry as the class name.
|
115
|
+
#
|
116
|
+
# ==== Examples
|
117
|
+
#
|
118
|
+
# class Foo::Bar < Thor
|
119
|
+
# def baz
|
120
|
+
# end
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# class Baz::Foo < Thor::Group
|
124
|
+
# end
|
125
|
+
#
|
126
|
+
# Thor::Util.namespace_to_thor_class("foo:bar") #=> Foo::Bar, nil # will invoke default task
|
127
|
+
# Thor::Util.namespace_to_thor_class("baz:foo") #=> Baz::Foo, nil
|
128
|
+
# Thor::Util.namespace_to_thor_class("foo:bar:baz") #=> Foo::Bar, "baz"
|
129
|
+
#
|
130
|
+
# ==== Parameters
|
131
|
+
# namespace<String>
|
132
|
+
#
|
133
|
+
# ==== Errors
|
134
|
+
# Thor::Error:: raised if the namespace cannot be found.
|
135
|
+
#
|
136
|
+
# Thor::Error:: raised if the namespace evals to a class which does not
|
137
|
+
# inherit from Thor or Thor::Group.
|
138
|
+
#
|
139
|
+
def self.namespace_to_thor_class_and_task(namespace, raise_if_nil=true)
|
140
|
+
klass, task_name = Thor::Util.find_by_namespace(namespace), nil
|
141
|
+
|
142
|
+
if klass.nil? && namespace.include?(?:)
|
143
|
+
namespace = namespace.split(":")
|
144
|
+
task_name = namespace.pop
|
145
|
+
klass = Thor::Util.find_by_namespace(namespace.join(":"))
|
146
|
+
end
|
147
|
+
|
148
|
+
raise Error, "could not find Thor class or task '#{namespace}'" if raise_if_nil && klass.nil?
|
149
|
+
|
150
|
+
return klass, task_name
|
151
|
+
end
|
152
|
+
|
153
|
+
# Receives a path and load the thor file in the path. The file is evaluated
|
154
|
+
# inside the sandbox to avoid namespacing conflicts.
|
155
|
+
#
|
156
|
+
def self.load_thorfile(path, content=nil)
|
157
|
+
content ||= File.read(path)
|
158
|
+
|
159
|
+
begin
|
160
|
+
Thor::Sandbox.class_eval(content, path)
|
161
|
+
rescue Exception => e
|
162
|
+
$stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Receives a yaml (hash) and updates all constants entries to namespace.
|
167
|
+
# This was added to deal with deprecated versions of Thor.
|
168
|
+
#
|
169
|
+
# TODO Deprecate this method in the future.
|
170
|
+
#
|
171
|
+
# ==== Returns
|
172
|
+
# TrueClass|FalseClass:: Returns true if any change to the yaml file was made.
|
173
|
+
#
|
174
|
+
def self.convert_constants_to_namespaces(yaml)
|
175
|
+
yaml_changed = false
|
176
|
+
|
177
|
+
yaml.each do |k, v|
|
178
|
+
next unless v[:constants] && v[:namespaces].nil?
|
179
|
+
yaml_changed = true
|
180
|
+
yaml[k][:namespaces] = v[:constants].map{|c| Thor::Util.namespace_from_thor_class(c)}
|
181
|
+
end
|
182
|
+
|
183
|
+
yaml_changed
|
184
|
+
end
|
185
|
+
|
186
|
+
def self.user_home
|
187
|
+
@@user_home ||= if ENV["HOME"]
|
188
|
+
ENV["HOME"]
|
189
|
+
elsif ENV["USERPROFILE"]
|
190
|
+
ENV["USERPROFILE"]
|
191
|
+
elsif ENV["HOMEDRIVE"] && ENV["HOMEPATH"]
|
192
|
+
File.join(ENV["HOMEDRIVE"], ENV["HOMEPATH"])
|
193
|
+
elsif ENV["APPDATA"]
|
194
|
+
ENV["APPDATA"]
|
195
|
+
else
|
196
|
+
begin
|
197
|
+
File.expand_path("~")
|
198
|
+
rescue
|
199
|
+
if File::ALT_SEPARATOR
|
200
|
+
"C:/"
|
201
|
+
else
|
202
|
+
"/"
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Returns the root where thor files are located, dependending on the OS.
|
209
|
+
#
|
210
|
+
def self.thor_root
|
211
|
+
File.join(user_home, ".thor")
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns the files in the thor root. On Windows thor_root will be something
|
215
|
+
# like this:
|
216
|
+
#
|
217
|
+
# C:\Documents and Settings\james\.thor
|
218
|
+
#
|
219
|
+
# If we don't #gsub the \ character, Dir.glob will fail.
|
220
|
+
#
|
221
|
+
def self.thor_root_glob
|
222
|
+
files = Dir["#{thor_root.gsub(/\\/, '/')}/*"]
|
223
|
+
|
224
|
+
files.map! do |file|
|
225
|
+
File.directory?(file) ? File.join(file, "main.thor") : file
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Where to look for Thor files.
|
230
|
+
#
|
231
|
+
def self.globs_for(path)
|
232
|
+
["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"]
|
233
|
+
end
|
234
|
+
|
235
|
+
# Return the path to the ruby interpreter taking into account multiple
|
236
|
+
# installations and windows extensions.
|
237
|
+
#
|
238
|
+
def self.ruby_command
|
239
|
+
@ruby_command ||= begin
|
240
|
+
ruby = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name'])
|
241
|
+
ruby << Config::CONFIG['EXEEXT']
|
242
|
+
|
243
|
+
# escape string in case path to ruby executable contain spaces.
|
244
|
+
ruby.sub!(/.*\s.*/m, '"\&"')
|
245
|
+
ruby
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
74
249
|
end
|
75
250
|
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
require 'thor/actions'
|
3
|
+
|
4
|
+
describe Thor::Actions::CreateFile do
|
5
|
+
before(:each) do
|
6
|
+
::FileUtils.rm_rf(destination_root)
|
7
|
+
end
|
8
|
+
|
9
|
+
def create_file(destination=nil, config={}, options={})
|
10
|
+
@base = MyCounter.new([1,2], options, { :destination_root => destination_root })
|
11
|
+
stub(@base).file_name { 'rdoc' }
|
12
|
+
|
13
|
+
@action = Thor::Actions::CreateFile.new(@base, destination, "CONFIGURATION",
|
14
|
+
{ :verbose => !@silence }.merge(config))
|
15
|
+
end
|
16
|
+
|
17
|
+
def invoke!
|
18
|
+
capture(:stdout){ @action.invoke! }
|
19
|
+
end
|
20
|
+
|
21
|
+
def revoke!
|
22
|
+
capture(:stdout){ @action.revoke! }
|
23
|
+
end
|
24
|
+
|
25
|
+
def silence!
|
26
|
+
@silence = true
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#invoke!" do
|
30
|
+
it "creates a file" do
|
31
|
+
create_file("doc/config.rb")
|
32
|
+
invoke!
|
33
|
+
File.exists?(File.join(destination_root, "doc/config.rb")).must be_true
|
34
|
+
end
|
35
|
+
|
36
|
+
it "does not create a file if pretending" do
|
37
|
+
create_file("doc/config.rb", {}, :pretend => true)
|
38
|
+
invoke!
|
39
|
+
File.exists?(File.join(destination_root, "doc/config.rb")).must be_false
|
40
|
+
end
|
41
|
+
|
42
|
+
it "shows created status to the user" do
|
43
|
+
create_file("doc/config.rb")
|
44
|
+
invoke!.must == " create doc/config.rb\n"
|
45
|
+
end
|
46
|
+
|
47
|
+
it "does not show any information if log status is false" do
|
48
|
+
silence!
|
49
|
+
create_file("doc/config.rb")
|
50
|
+
invoke!.must be_empty
|
51
|
+
end
|
52
|
+
|
53
|
+
it "returns the destination" do
|
54
|
+
capture(:stdout) do
|
55
|
+
create_file("doc/config.rb").invoke!.must == File.join(destination_root, "doc/config.rb")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
it "converts encoded instructions" do
|
60
|
+
create_file("doc/%file_name%.rb.tt")
|
61
|
+
invoke!
|
62
|
+
File.exists?(File.join(destination_root, "doc/rdoc.rb.tt")).must be_true
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "when file exists" do
|
66
|
+
before(:each) do
|
67
|
+
create_file("doc/config.rb")
|
68
|
+
invoke!
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "and is identical" do
|
72
|
+
it "shows identical status" do
|
73
|
+
create_file("doc/config.rb")
|
74
|
+
invoke!
|
75
|
+
invoke!.must == " identical doc/config.rb\n"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "and is not identical" do
|
80
|
+
before(:each) do
|
81
|
+
File.open(File.join(destination_root, 'doc/config.rb'), 'w'){ |f| f.write("FOO = 3") }
|
82
|
+
end
|
83
|
+
|
84
|
+
it "shows forced status to the user if force is given" do
|
85
|
+
create_file("doc/config.rb", {}, :force => true).must_not be_identical
|
86
|
+
invoke!.must == " force doc/config.rb\n"
|
87
|
+
end
|
88
|
+
|
89
|
+
it "shows skipped status to the user if skip is given" do
|
90
|
+
create_file("doc/config.rb", {}, :skip => true).must_not be_identical
|
91
|
+
invoke!.must == " skip doc/config.rb\n"
|
92
|
+
end
|
93
|
+
|
94
|
+
it "shows forced status to the user if force is configured" do
|
95
|
+
create_file("doc/config.rb", :force => true).must_not be_identical
|
96
|
+
invoke!.must == " force doc/config.rb\n"
|
97
|
+
end
|
98
|
+
|
99
|
+
it "shows skipped status to the user if skip is configured" do
|
100
|
+
create_file("doc/config.rb", :skip => true).must_not be_identical
|
101
|
+
invoke!.must == " skip doc/config.rb\n"
|
102
|
+
end
|
103
|
+
|
104
|
+
it "shows conflict status to ther user" do
|
105
|
+
create_file("doc/config.rb").must_not be_identical
|
106
|
+
mock($stdin).gets{ 's' }
|
107
|
+
file = File.join(destination_root, 'doc/config.rb')
|
108
|
+
|
109
|
+
content = invoke!
|
110
|
+
content.must =~ /conflict doc\/config\.rb/
|
111
|
+
content.must =~ /Overwrite #{file}\? \(enter "h" for help\) \[Ynaqdh\]/
|
112
|
+
content.must =~ /skip doc\/config\.rb/
|
113
|
+
end
|
114
|
+
|
115
|
+
it "creates the file if the file collision menu returns true" do
|
116
|
+
create_file("doc/config.rb")
|
117
|
+
mock($stdin).gets{ 'y' }
|
118
|
+
invoke!.must =~ /force doc\/config\.rb/
|
119
|
+
end
|
120
|
+
|
121
|
+
it "skips the file if the file collision menu returns false" do
|
122
|
+
create_file("doc/config.rb")
|
123
|
+
mock($stdin).gets{ 'n' }
|
124
|
+
invoke!.must =~ /skip doc\/config\.rb/
|
125
|
+
end
|
126
|
+
|
127
|
+
it "executes the block given to show file content" do
|
128
|
+
create_file("doc/config.rb")
|
129
|
+
mock($stdin).gets{ 'd' }
|
130
|
+
mock($stdin).gets{ 'n' }
|
131
|
+
mock(@base.shell).system(/diff -u/)
|
132
|
+
invoke!
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "#revoke!" do
|
139
|
+
it "removes the destination file" do
|
140
|
+
create_file("doc/config.rb")
|
141
|
+
invoke!
|
142
|
+
revoke!
|
143
|
+
File.exists?(@action.destination).must be_false
|
144
|
+
end
|
145
|
+
|
146
|
+
it "does not raise an error if the file does not exist" do
|
147
|
+
create_file("doc/config.rb")
|
148
|
+
revoke!
|
149
|
+
File.exists?(@action.destination).must be_false
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe "#exists?" do
|
154
|
+
it "returns true if the destination file exists" do
|
155
|
+
create_file("doc/config.rb")
|
156
|
+
@action.exists?.must be_false
|
157
|
+
invoke!
|
158
|
+
@action.exists?.must be_true
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
describe "#identical?" do
|
163
|
+
it "returns true if the destination file and is identical" do
|
164
|
+
create_file("doc/config.rb")
|
165
|
+
@action.identical?.must be_false
|
166
|
+
invoke!
|
167
|
+
@action.identical?.must be_true
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|