thor 0.9.9 → 0.11.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/CHANGELOG.rdoc +29 -4
  2. data/README.rdoc +234 -0
  3. data/Thorfile +57 -0
  4. data/VERSION +1 -0
  5. data/bin/rake2thor +4 -0
  6. data/bin/thor +1 -1
  7. data/lib/thor.rb +216 -119
  8. data/lib/thor/actions.rb +272 -0
  9. data/lib/thor/actions/create_file.rb +102 -0
  10. data/lib/thor/actions/directory.rb +87 -0
  11. data/lib/thor/actions/empty_directory.rb +133 -0
  12. data/lib/thor/actions/file_manipulation.rb +195 -0
  13. data/lib/thor/actions/inject_into_file.rb +78 -0
  14. data/lib/thor/base.rb +510 -0
  15. data/lib/thor/core_ext/hash_with_indifferent_access.rb +75 -0
  16. data/lib/thor/core_ext/ordered_hash.rb +100 -0
  17. data/lib/thor/error.rb +25 -1
  18. data/lib/thor/group.rb +263 -0
  19. data/lib/thor/invocation.rb +178 -0
  20. data/lib/thor/parser.rb +4 -0
  21. data/lib/thor/parser/argument.rb +67 -0
  22. data/lib/thor/parser/arguments.rb +145 -0
  23. data/lib/thor/parser/option.rb +132 -0
  24. data/lib/thor/parser/options.rb +142 -0
  25. data/lib/thor/rake_compat.rb +67 -0
  26. data/lib/thor/runner.rb +232 -242
  27. data/lib/thor/shell.rb +72 -0
  28. data/lib/thor/shell/basic.rb +220 -0
  29. data/lib/thor/shell/color.rb +108 -0
  30. data/lib/thor/task.rb +97 -60
  31. data/lib/thor/util.rb +230 -55
  32. data/spec/actions/create_file_spec.rb +170 -0
  33. data/spec/actions/directory_spec.rb +118 -0
  34. data/spec/actions/empty_directory_spec.rb +91 -0
  35. data/spec/actions/file_manipulation_spec.rb +242 -0
  36. data/spec/actions/inject_into_file_spec.rb +80 -0
  37. data/spec/actions_spec.rb +291 -0
  38. data/spec/base_spec.rb +236 -0
  39. data/spec/core_ext/hash_with_indifferent_access_spec.rb +43 -0
  40. data/spec/core_ext/ordered_hash_spec.rb +115 -0
  41. data/spec/fixtures/bundle/execute.rb +6 -0
  42. data/spec/fixtures/doc/config.rb +1 -0
  43. data/spec/group_spec.rb +177 -0
  44. data/spec/invocation_spec.rb +107 -0
  45. data/spec/parser/argument_spec.rb +47 -0
  46. data/spec/parser/arguments_spec.rb +64 -0
  47. data/spec/parser/option_spec.rb +212 -0
  48. data/spec/parser/options_spec.rb +255 -0
  49. data/spec/rake_compat_spec.rb +64 -0
  50. data/spec/runner_spec.rb +204 -0
  51. data/spec/shell/basic_spec.rb +206 -0
  52. data/spec/shell/color_spec.rb +41 -0
  53. data/spec/shell_spec.rb +25 -0
  54. data/spec/spec_helper.rb +52 -0
  55. data/spec/task_spec.rb +82 -0
  56. data/spec/thor_spec.rb +234 -0
  57. data/spec/util_spec.rb +196 -0
  58. metadata +69 -25
  59. data/README.markdown +0 -76
  60. data/Rakefile +0 -6
  61. data/lib/thor/options.rb +0 -242
  62. data/lib/thor/ordered_hash.rb +0 -64
  63. data/lib/thor/task_hash.rb +0 -22
  64. data/lib/thor/tasks.rb +0 -77
  65. data/lib/thor/tasks/package.rb +0 -18
data/lib/thor/util.rb CHANGED
@@ -1,75 +1,250 @@
1
- require 'thor/error'
1
+ require 'rbconfig'
2
2
 
3
- module ObjectSpace
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
- class Thor
18
- module Tasks; end
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
- def self.full_const_get(obj, name)
23
- list = name.split("::")
24
- list.shift if list.first.empty?
25
- list.each do |x|
26
- # This is required because const_get tries to look for constants in the
27
- # ancestor chain, but we only want constants that are HERE
28
- obj = obj.const_defined?(x) ? obj.const_get(x) : obj.const_missing(x)
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
- def self.constant_from_thor_path(str)
41
- make_constant(to_constant(str))
42
- rescue NameError => e
43
- raise e unless e.message =~ /^uninitialized constant (.*)$/
44
- raise Error, "There was no available namespace `#{str}'."
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
- def self.to_constant(str)
48
- str = 'default' if str.empty?
49
- str.gsub(/:(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
50
- end
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
- def self.constants_in_contents(str, file = __FILE__)
53
- klasses = ObjectSpace.classes.dup
54
- Module.new.class_eval(str, file)
55
- klasses = ObjectSpace.classes - klasses
56
- klasses = klasses.select {|k| k < Thor }
57
- klasses.map! {|k| k.to_s.gsub(/#<Module:\w+>::/, '')}
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
- def self.make_constant(str, base = [Thor::Tasks, Object])
61
- which = base.find do |obj|
62
- full_const_get(obj, str) rescue nil
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