thor 0.9.9 → 0.11.5

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.
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