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
@@ -0,0 +1,272 @@
1
+ require 'fileutils'
2
+
3
+ Dir[File.join(File.dirname(__FILE__), "actions", "*.rb")].each do |action|
4
+ require action
5
+ end
6
+
7
+ class Thor
8
+ module Actions
9
+ attr_accessor :behavior
10
+
11
+ # On inclusion, add some options to base.
12
+ #
13
+ def self.included(base) #:nodoc:
14
+ base.extend ClassMethods
15
+ return unless base.respond_to?(:class_option)
16
+
17
+ base.class_option :pretend, :type => :boolean, :aliases => "-p", :group => :runtime,
18
+ :desc => "Run but do not make any changes"
19
+
20
+ base.class_option :force, :type => :boolean, :aliases => "-f", :group => :runtime,
21
+ :desc => "Overwrite files that already exist"
22
+
23
+ base.class_option :skip, :type => :boolean, :aliases => "-s", :group => :runtime,
24
+ :desc => "Skip files that already exist"
25
+
26
+ base.class_option :quiet, :type => :boolean, :aliases => "-q", :group => :runtime,
27
+ :desc => "Supress status output"
28
+ end
29
+
30
+ module ClassMethods
31
+ # Hold source paths for one Thor instance. source_paths_for_search is the
32
+ # method responsible to gather source_paths from this current class,
33
+ # inherited paths and the source root.
34
+ #
35
+ def source_paths
36
+ @source_paths ||= []
37
+ end
38
+
39
+ # Returns the source paths in the following order:
40
+ #
41
+ # 1) This class source paths
42
+ # 2) Source root
43
+ # 3) Parents source paths
44
+ #
45
+ def source_paths_for_search
46
+ paths = []
47
+ paths += self.source_paths
48
+ paths << self.source_root if self.respond_to?(:source_root)
49
+ paths += from_superclass(:source_paths, [])
50
+ paths
51
+ end
52
+ end
53
+
54
+ # Extends initializer to add more configuration options.
55
+ #
56
+ # ==== Configuration
57
+ # behavior<Symbol>:: The actions default behavior. Can be :invoke or :revoke.
58
+ # It also accepts :force, :skip and :pretend to set the behavior
59
+ # and the respective option.
60
+ #
61
+ # destination_root<String>:: The root directory needed for some actions.
62
+ #
63
+ def initialize(args=[], options={}, config={})
64
+ self.behavior = case config[:behavior].to_s
65
+ when "force", "skip"
66
+ _cleanup_options_and_set(options, config[:behavior])
67
+ :invoke
68
+ when "revoke"
69
+ :revoke
70
+ else
71
+ :invoke
72
+ end
73
+
74
+ super
75
+ self.destination_root = config[:destination_root]
76
+ end
77
+
78
+ # Wraps an action object and call it accordingly to the thor class behavior.
79
+ #
80
+ def action(instance) #:nodoc:
81
+ if behavior == :revoke
82
+ instance.revoke!
83
+ else
84
+ instance.invoke!
85
+ end
86
+ end
87
+
88
+ # Returns the root for this thor class (also aliased as destination root).
89
+ #
90
+ def destination_root
91
+ @destination_stack.last
92
+ end
93
+
94
+ # Sets the root for this thor class. Relatives path are added to the
95
+ # directory where the script was invoked and expanded.
96
+ #
97
+ def destination_root=(root)
98
+ @destination_stack ||= []
99
+ @destination_stack[0] = File.expand_path(root || '')
100
+ end
101
+
102
+ # Returns the given path relative to the absolute root (ie, root where
103
+ # the script started).
104
+ #
105
+ def relative_to_original_destination_root(path, remove_dot=true)
106
+ path = path.gsub(@destination_stack[0], '.')
107
+ remove_dot ? (path[2..-1] || '') : path
108
+ end
109
+
110
+ # Holds source paths in instance so they can be manipulated.
111
+ #
112
+ def source_paths
113
+ @source_paths ||= self.class.source_paths_for_search
114
+ end
115
+
116
+ # Receives a file or directory and search for it in the source paths.
117
+ #
118
+ def find_in_source_paths(file)
119
+ relative_root = relative_to_original_destination_root(destination_root, false)
120
+
121
+ source_paths.each do |source|
122
+ source_file = File.expand_path(file, File.join(source, relative_root))
123
+ return source_file if File.exists?(source_file)
124
+ end
125
+
126
+ if source_paths.empty?
127
+ raise Error, "You don't have any source path defined for class #{self.class.name}. To fix this, " <<
128
+ "you can define a source_root in your class."
129
+ else
130
+ raise Error, "Could not find #{file.inspect} in source paths."
131
+ end
132
+ end
133
+
134
+ # Do something in the root or on a provided subfolder. If a relative path
135
+ # is given it's referenced from the current root. The full path is yielded
136
+ # to the block you provide. The path is set back to the previous path when
137
+ # the method exits.
138
+ #
139
+ # ==== Parameters
140
+ # dir<String>:: the directory to move to.
141
+ # config<Hash>:: give :verbose => true to log and use padding.
142
+ #
143
+ def inside(dir='', config={}, &block)
144
+ verbose = config.fetch(:verbose, false)
145
+
146
+ say_status :inside, dir, verbose
147
+ shell.padding += 1 if verbose
148
+ @destination_stack.push File.expand_path(dir, destination_root)
149
+
150
+ FileUtils.mkdir_p(destination_root) unless File.exist?(destination_root)
151
+ FileUtils.cd(destination_root) { block.arity == 1 ? yield(destination_root) : yield }
152
+
153
+ @destination_stack.pop
154
+ shell.padding -= 1 if verbose
155
+ end
156
+
157
+ # Goes to the root and execute the given block.
158
+ #
159
+ def in_root
160
+ inside(@destination_stack.first) { yield }
161
+ end
162
+
163
+ # Loads an external file and execute it in the instance binding.
164
+ #
165
+ # ==== Parameters
166
+ # path<String>:: The path to the file to execute. Can be a web address or
167
+ # a relative path from the source root.
168
+ #
169
+ # ==== Examples
170
+ #
171
+ # apply "http://gist.github.com/103208"
172
+ #
173
+ # apply "recipes/jquery.rb"
174
+ #
175
+ def apply(path, config={})
176
+ verbose = config.fetch(:verbose, true)
177
+ path = find_in_source_paths(path) unless path =~ /^http\:\/\//
178
+
179
+ say_status :apply, path, verbose
180
+ shell.padding += 1 if verbose
181
+ instance_eval(open(path).read)
182
+ shell.padding -= 1 if verbose
183
+ end
184
+
185
+ # Executes a command.
186
+ #
187
+ # ==== Parameters
188
+ # command<String>:: the command to be executed.
189
+ # config<Hash>:: give :verbose => false to not log the status. Specify :with
190
+ # to append an executable to command executation.
191
+ #
192
+ # ==== Example
193
+ #
194
+ # inside('vendor') do
195
+ # run('ln -s ~/edge rails')
196
+ # end
197
+ #
198
+ def run(command, config={})
199
+ return unless behavior == :invoke
200
+
201
+ destination = relative_to_original_destination_root(destination_root, false)
202
+ desc = "#{command} from #{destination.inspect}"
203
+
204
+ if config[:with]
205
+ desc = "#{File.basename(config[:with].to_s)} #{desc}"
206
+ command = "#{config[:with]} #{command}"
207
+ end
208
+
209
+ say_status :run, desc, config.fetch(:verbose, true)
210
+ system(command) unless options[:pretend]
211
+ end
212
+
213
+ # Executes a ruby script (taking into account WIN32 platform quirks).
214
+ #
215
+ # ==== Parameters
216
+ # command<String>:: the command to be executed.
217
+ # config<Hash>:: give :verbose => false to not log the status.
218
+ #
219
+ def run_ruby_script(command, config={})
220
+ return unless behavior == :invoke
221
+ run "#{command}", config.merge(:with => Thor::Util.ruby_command)
222
+ end
223
+
224
+ # Run a thor command. A hash of options can be given and it's converted to
225
+ # switches.
226
+ #
227
+ # ==== Parameters
228
+ # task<String>:: the task to be invoked
229
+ # args<Array>:: arguments to the task
230
+ # config<Hash>:: give :verbose => false to not log the status. Other options
231
+ # are given as parameter to Thor.
232
+ #
233
+ # ==== Examples
234
+ #
235
+ # thor :install, "http://gist.github.com/103208"
236
+ # #=> thor install http://gist.github.com/103208
237
+ #
238
+ # thor :list, :all => true, :substring => 'rails'
239
+ # #=> thor list --all --substring=rails
240
+ #
241
+ def thor(task, *args)
242
+ config = args.last.is_a?(Hash) ? args.pop : {}
243
+ verbose = config.key?(:verbose) ? config.delete(:verbose) : true
244
+
245
+ args.unshift task
246
+ args.push Thor::Options.to_switches(config)
247
+ command = args.join(' ').strip
248
+
249
+ run command, :with => :thor, :verbose => verbose
250
+ end
251
+
252
+ protected
253
+
254
+ # Allow current root to be shared between invocations.
255
+ #
256
+ def _shared_configuration #:nodoc:
257
+ super.merge!(:destination_root => self.destination_root)
258
+ end
259
+
260
+ def _cleanup_options_and_set(options, key) #:nodoc:
261
+ case options
262
+ when Array
263
+ %w(--force -f --skip -s).each { |i| options.delete(i) }
264
+ options << "--#{key}"
265
+ when Hash
266
+ [:force, :skip, "force", "skip"].each { |i| options.delete(i) }
267
+ options.merge!(key => true)
268
+ end
269
+ end
270
+
271
+ end
272
+ end
@@ -0,0 +1,102 @@
1
+ require 'thor/actions/empty_directory'
2
+
3
+ class Thor
4
+ module Actions
5
+
6
+ # Create a new file relative to the destination root with the given data,
7
+ # which is the return value of a block or a data string.
8
+ #
9
+ # ==== Parameters
10
+ # destination<String>:: the relative path to the destination root.
11
+ # data<String|NilClass>:: the data to append to the file.
12
+ # config<Hash>:: give :verbose => false to not log the status.
13
+ #
14
+ # ==== Examples
15
+ #
16
+ # create_file "lib/fun_party.rb" do
17
+ # hostname = ask("What is the virtual hostname I should use?")
18
+ # "vhost.name = #{hostname}"
19
+ # end
20
+ #
21
+ # create_file "config/apach.conf", "your apache config"
22
+ #
23
+ def create_file(destination, data=nil, config={}, &block)
24
+ action CreateFile.new(self, destination, block || data.to_s, config)
25
+ end
26
+ alias :add_file :create_file
27
+
28
+ # AddFile is a subset of Template, which instead of rendering a file with
29
+ # ERB, it gets the content from the user.
30
+ #
31
+ class CreateFile < EmptyDirectory #:nodoc:
32
+ attr_reader :data
33
+
34
+ def initialize(base, destination, data, config={})
35
+ @data = data
36
+ super(base, destination, config)
37
+ end
38
+
39
+ # Checks if the content of the file at the destination is identical to the rendered result.
40
+ #
41
+ # ==== Returns
42
+ # Boolean:: true if it is identical, false otherwise.
43
+ #
44
+ def identical?
45
+ exists? && File.read(destination) == render
46
+ end
47
+
48
+ # Holds the content to be added to the file.
49
+ #
50
+ def render
51
+ @render ||= if data.is_a?(Proc)
52
+ data.call
53
+ else
54
+ data
55
+ end
56
+ end
57
+
58
+ def invoke!
59
+ invoke_with_conflict_check do
60
+ FileUtils.mkdir_p(File.dirname(destination))
61
+ File.open(destination, 'w'){ |f| f.write render }
62
+ end
63
+ end
64
+
65
+ protected
66
+
67
+ # Now on conflict we check if the file is identical or not.
68
+ #
69
+ def on_conflict_behavior(&block)
70
+ if identical?
71
+ say_status :identical, :blue
72
+ else
73
+ options = base.options.merge(config)
74
+ force_or_skip_or_conflict(options[:force], options[:skip], &block)
75
+ end
76
+ end
77
+
78
+ # If force is true, run the action, otherwise check if it's not being
79
+ # skipped. If both are false, show the file_collision menu, if the menu
80
+ # returns true, force it, otherwise skip.
81
+ #
82
+ def force_or_skip_or_conflict(force, skip, &block)
83
+ if force
84
+ say_status :force, :yellow
85
+ block.call unless pretend?
86
+ elsif skip
87
+ say_status :skip, :yellow
88
+ else
89
+ say_status :conflict, :red
90
+ force_or_skip_or_conflict(force_on_collision?, true, &block)
91
+ end
92
+ end
93
+
94
+ # Shows the file collision menu to the user and gets the result.
95
+ #
96
+ def force_on_collision?
97
+ base.shell.file_collision(destination){ render }
98
+ end
99
+
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,87 @@
1
+ require 'thor/actions/empty_directory'
2
+
3
+ class Thor
4
+ module Actions
5
+
6
+ # Copies recursively the files from source directory to root directory.
7
+ # If any of the files finishes with .tt, it's considered to be a template
8
+ # and is placed in the destination without the extension .tt. If any
9
+ # empty directory is found, it's copied and all .empty_directory files are
10
+ # ignored. Remember that file paths can also be encoded, let's suppose a doc
11
+ # directory with the following files:
12
+ #
13
+ # doc/
14
+ # components/.empty_directory
15
+ # README
16
+ # rdoc.rb.tt
17
+ # %app_name%.rb
18
+ #
19
+ # When invoked as:
20
+ #
21
+ # directory "doc"
22
+ #
23
+ # It will create a doc directory in the destination with the following
24
+ # files (assuming that the app_name is "blog"):
25
+ #
26
+ # doc/
27
+ # components/
28
+ # README
29
+ # rdoc.rb
30
+ # blog.rb
31
+ #
32
+ # ==== Parameters
33
+ # source<String>:: the relative path to the source root.
34
+ # destination<String>:: the relative path to the destination root.
35
+ # config<Hash>:: give :verbose => false to not log the status.
36
+ # If :recursive => false, does not look for paths recursively.
37
+ #
38
+ # ==== Examples
39
+ #
40
+ # directory "doc"
41
+ # directory "doc", "docs", :recursive => false
42
+ #
43
+ def directory(source, destination=nil, config={})
44
+ action Directory.new(self, source, destination || source, config)
45
+ end
46
+
47
+ class Directory < EmptyDirectory #:nodoc:
48
+ attr_reader :source
49
+
50
+ def initialize(base, source, destination=nil, config={})
51
+ @source = File.expand_path(base.find_in_source_paths(source.to_s))
52
+ super(base, destination, { :recursive => true }.merge(config))
53
+ end
54
+
55
+ def invoke!
56
+ base.empty_directory given_destination, config
57
+ execute!
58
+ end
59
+
60
+ def revoke!
61
+ execute!
62
+ end
63
+
64
+ protected
65
+
66
+ def execute!
67
+ lookup = config[:recursive] ? File.join(source, '**') : source
68
+ lookup = File.join(lookup, '{*,.[a-z]*}')
69
+
70
+ Dir[lookup].each do |file_source|
71
+ next if File.directory?(file_source)
72
+ file_destination = File.join(given_destination, file_source.gsub(source, '.'))
73
+
74
+ case file_source
75
+ when /\.empty_directory$/
76
+ base.empty_directory(File.dirname(file_destination), config)
77
+ when /\.tt$/
78
+ base.template(file_source, file_destination[0..-4], config)
79
+ else
80
+ base.copy_file(file_source, file_destination, config)
81
+ end
82
+ end
83
+ end
84
+
85
+ end
86
+ end
87
+ end