thor 0.16.0 → 1.2.1

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 (93) hide show
  1. checksums.yaml +7 -0
  2. data/CONTRIBUTING.md +15 -0
  3. data/README.md +23 -6
  4. data/bin/thor +1 -1
  5. data/lib/thor/actions/create_file.rb +34 -35
  6. data/lib/thor/actions/create_link.rb +9 -5
  7. data/lib/thor/actions/directory.rb +33 -23
  8. data/lib/thor/actions/empty_directory.rb +75 -85
  9. data/lib/thor/actions/file_manipulation.rb +103 -36
  10. data/lib/thor/actions/inject_into_file.rb +46 -36
  11. data/lib/thor/actions.rb +90 -68
  12. data/lib/thor/base.rb +302 -244
  13. data/lib/thor/command.rb +142 -0
  14. data/lib/thor/core_ext/hash_with_indifferent_access.rb +52 -24
  15. data/lib/thor/error.rb +90 -10
  16. data/lib/thor/group.rb +70 -74
  17. data/lib/thor/invocation.rb +63 -55
  18. data/lib/thor/line_editor/basic.rb +37 -0
  19. data/lib/thor/line_editor/readline.rb +88 -0
  20. data/lib/thor/line_editor.rb +17 -0
  21. data/lib/thor/nested_context.rb +29 -0
  22. data/lib/thor/parser/argument.rb +24 -28
  23. data/lib/thor/parser/arguments.rb +110 -102
  24. data/lib/thor/parser/option.rb +53 -15
  25. data/lib/thor/parser/options.rb +174 -97
  26. data/lib/thor/parser.rb +4 -4
  27. data/lib/thor/rake_compat.rb +12 -11
  28. data/lib/thor/runner.rb +159 -155
  29. data/lib/thor/shell/basic.rb +216 -93
  30. data/lib/thor/shell/color.rb +53 -40
  31. data/lib/thor/shell/html.rb +61 -58
  32. data/lib/thor/shell.rb +29 -36
  33. data/lib/thor/util.rb +231 -213
  34. data/lib/thor/version.rb +1 -1
  35. data/lib/thor.rb +303 -166
  36. data/thor.gemspec +27 -24
  37. metadata +36 -226
  38. data/.gitignore +0 -44
  39. data/.rspec +0 -2
  40. data/.travis.yml +0 -7
  41. data/CHANGELOG.rdoc +0 -134
  42. data/Gemfile +0 -15
  43. data/Thorfile +0 -30
  44. data/bin/rake2thor +0 -86
  45. data/lib/thor/core_ext/dir_escape.rb +0 -0
  46. data/lib/thor/core_ext/file_binary_read.rb +0 -9
  47. data/lib/thor/core_ext/ordered_hash.rb +0 -100
  48. data/lib/thor/task.rb +0 -132
  49. data/spec/actions/create_file_spec.rb +0 -170
  50. data/spec/actions/create_link_spec.rb +0 -81
  51. data/spec/actions/directory_spec.rb +0 -149
  52. data/spec/actions/empty_directory_spec.rb +0 -130
  53. data/spec/actions/file_manipulation_spec.rb +0 -370
  54. data/spec/actions/inject_into_file_spec.rb +0 -135
  55. data/spec/actions_spec.rb +0 -331
  56. data/spec/base_spec.rb +0 -279
  57. data/spec/core_ext/hash_with_indifferent_access_spec.rb +0 -43
  58. data/spec/core_ext/ordered_hash_spec.rb +0 -115
  59. data/spec/exit_condition_spec.rb +0 -19
  60. data/spec/fixtures/application.rb +0 -2
  61. data/spec/fixtures/app{1}/README +0 -3
  62. data/spec/fixtures/bundle/execute.rb +0 -6
  63. data/spec/fixtures/bundle/main.thor +0 -1
  64. data/spec/fixtures/doc/%file_name%.rb.tt +0 -1
  65. data/spec/fixtures/doc/COMMENTER +0 -10
  66. data/spec/fixtures/doc/README +0 -3
  67. data/spec/fixtures/doc/block_helper.rb +0 -3
  68. data/spec/fixtures/doc/components/.empty_directory +0 -0
  69. data/spec/fixtures/doc/config.rb +0 -1
  70. data/spec/fixtures/doc/config.yaml.tt +0 -1
  71. data/spec/fixtures/enum.thor +0 -10
  72. data/spec/fixtures/group.thor +0 -114
  73. data/spec/fixtures/invoke.thor +0 -112
  74. data/spec/fixtures/path with spaces +0 -0
  75. data/spec/fixtures/script.thor +0 -190
  76. data/spec/fixtures/task.thor +0 -10
  77. data/spec/group_spec.rb +0 -216
  78. data/spec/invocation_spec.rb +0 -100
  79. data/spec/parser/argument_spec.rb +0 -53
  80. data/spec/parser/arguments_spec.rb +0 -66
  81. data/spec/parser/option_spec.rb +0 -202
  82. data/spec/parser/options_spec.rb +0 -330
  83. data/spec/rake_compat_spec.rb +0 -72
  84. data/spec/register_spec.rb +0 -135
  85. data/spec/runner_spec.rb +0 -241
  86. data/spec/shell/basic_spec.rb +0 -300
  87. data/spec/shell/color_spec.rb +0 -81
  88. data/spec/shell/html_spec.rb +0 -32
  89. data/spec/shell_spec.rb +0 -47
  90. data/spec/spec_helper.rb +0 -59
  91. data/spec/task_spec.rb +0 -80
  92. data/spec/thor_spec.rb +0 -418
  93. data/spec/util_spec.rb +0 -196
@@ -1,16 +1,16 @@
1
- require 'erb'
2
- require 'open-uri'
1
+ require "erb"
3
2
 
4
3
  class Thor
5
4
  module Actions
6
-
7
5
  # Copies the file from the relative source to the relative destination. If
8
6
  # the destination is not given it's assumed to be equal to the source.
9
7
  #
10
8
  # ==== Parameters
11
9
  # source<String>:: the relative path to the source root.
12
10
  # destination<String>:: the relative path to the destination root.
13
- # config<Hash>:: give :verbose => false to not log the status.
11
+ # config<Hash>:: give :verbose => false to not log the status, and
12
+ # :mode => :preserve, to preserve the file mode from the source.
13
+
14
14
  #
15
15
  # ==== Examples
16
16
  #
@@ -23,11 +23,15 @@ class Thor
23
23
  destination = args.first || source
24
24
  source = File.expand_path(find_in_source_paths(source.to_s))
25
25
 
26
- create_file destination, nil, config do
26
+ resulting_destination = create_file destination, nil, config do
27
27
  content = File.binread(source)
28
- content = block.call(content) if block
28
+ content = yield(content) if block
29
29
  content
30
30
  end
31
+ if config[:mode] == :preserve
32
+ mode = File.stat(source).mode
33
+ chmod(resulting_destination, mode, config)
34
+ end
31
35
  end
32
36
 
33
37
  # Links the file from the relative source to the relative destination. If
@@ -44,7 +48,7 @@ class Thor
44
48
  #
45
49
  # link_file "doc/README"
46
50
  #
47
- def link_file(source, *args, &block)
51
+ def link_file(source, *args)
48
52
  config = args.last.is_a?(Hash) ? args.pop : {}
49
53
  destination = args.first || source
50
54
  source = File.expand_path(find_in_source_paths(source.to_s))
@@ -56,6 +60,9 @@ class Thor
56
60
  # destination. If a block is given instead of destination, the content of
57
61
  # the url is yielded and used as location.
58
62
  #
63
+ # +get+ relies on open-uri, so passing application user input would provide
64
+ # a command injection attack vector.
65
+ #
59
66
  # ==== Parameters
60
67
  # source<String>:: the address of the given content.
61
68
  # destination<String>:: the relative path to the destination root.
@@ -73,11 +80,16 @@ class Thor
73
80
  config = args.last.is_a?(Hash) ? args.pop : {}
74
81
  destination = args.first
75
82
 
76
- source = File.expand_path(find_in_source_paths(source.to_s)) unless source =~ /^https?\:\/\//
77
- render = open(source) {|input| input.binmode.read }
83
+ render = if source =~ %r{^https?\://}
84
+ require "open-uri"
85
+ URI.send(:open, source) { |input| input.binmode.read }
86
+ else
87
+ source = File.expand_path(find_in_source_paths(source.to_s))
88
+ open(source) { |input| input.binmode.read }
89
+ end
78
90
 
79
91
  destination ||= if block_given?
80
- block.arity == 1 ? block.call(render) : block.call
92
+ block.arity == 1 ? yield(render) : yield
81
93
  else
82
94
  File.basename(source)
83
95
  end
@@ -102,14 +114,22 @@ class Thor
102
114
  #
103
115
  def template(source, *args, &block)
104
116
  config = args.last.is_a?(Hash) ? args.pop : {}
105
- destination = args.first || source.sub(/\.tt$/, '')
117
+ destination = args.first || source.sub(/#{TEMPLATE_EXTNAME}$/, "")
106
118
 
107
119
  source = File.expand_path(find_in_source_paths(source.to_s))
108
- context = instance_eval('binding')
120
+ context = config.delete(:context) || instance_eval("binding")
109
121
 
110
122
  create_file destination, nil, config do
111
- content = ERB.new(::File.binread(source), nil, '-', '@output_buffer').result(context)
112
- content = block.call(content) if block
123
+ match = ERB.version.match(/(\d+\.\d+\.\d+)/)
124
+ capturable_erb = if match && match[1] >= "2.2.0" # Ruby 2.6+
125
+ CapturableERB.new(::File.binread(source), :trim_mode => "-", :eoutvar => "@output_buffer")
126
+ else
127
+ CapturableERB.new(::File.binread(source), nil, "-", "@output_buffer")
128
+ end
129
+ content = capturable_erb.tap do |erb|
130
+ erb.filename = source
131
+ end.result(context)
132
+ content = yield(content) if block
113
133
  content
114
134
  end
115
135
  end
@@ -123,13 +143,16 @@ class Thor
123
143
  #
124
144
  # ==== Example
125
145
  #
126
- # chmod "script/*", 0755
146
+ # chmod "script/server", 0755
127
147
  #
128
- def chmod(path, mode, config={})
148
+ def chmod(path, mode, config = {})
129
149
  return unless behavior == :invoke
130
150
  path = File.expand_path(path, destination_root)
131
151
  say_status :chmod, relative_to_original_destination_root(path), config.fetch(:verbose, true)
132
- FileUtils.chmod_R(mode, path) unless options[:pretend]
152
+ unless options[:pretend]
153
+ require "fileutils"
154
+ FileUtils.chmod_R(mode, path)
155
+ end
133
156
  end
134
157
 
135
158
  # Prepend text to a file. Since it depends on insert_into_file, it's reversible.
@@ -149,7 +172,7 @@ class Thor
149
172
  #
150
173
  def prepend_to_file(path, *args, &block)
151
174
  config = args.last.is_a?(Hash) ? args.pop : {}
152
- config.merge!(:after => /\A/)
175
+ config[:after] = /\A/
153
176
  insert_into_file(path, *(args << config), &block)
154
177
  end
155
178
  alias_method :prepend_file, :prepend_to_file
@@ -171,7 +194,7 @@ class Thor
171
194
  #
172
195
  def append_to_file(path, *args, &block)
173
196
  config = args.last.is_a?(Hash) ? args.pop : {}
174
- config.merge!(:before => /\z/)
197
+ config[:before] = /\z/
175
198
  insert_into_file(path, *(args << config), &block)
176
199
  end
177
200
  alias_method :append_file, :append_to_file
@@ -187,15 +210,38 @@ class Thor
187
210
  #
188
211
  # ==== Examples
189
212
  #
190
- # inject_into_class "app/controllers/application_controller.rb", ApplicationController, " filter_parameter :password\n"
213
+ # inject_into_class "app/controllers/application_controller.rb", "ApplicationController", " filter_parameter :password\n"
191
214
  #
192
- # inject_into_class "app/controllers/application_controller.rb", ApplicationController do
215
+ # inject_into_class "app/controllers/application_controller.rb", "ApplicationController" do
193
216
  # " filter_parameter :password\n"
194
217
  # end
195
218
  #
196
219
  def inject_into_class(path, klass, *args, &block)
197
220
  config = args.last.is_a?(Hash) ? args.pop : {}
198
- config.merge!(:after => /class #{klass}\n|class #{klass} .*\n/)
221
+ config[:after] = /class #{klass}\n|class #{klass} .*\n/
222
+ insert_into_file(path, *(args << config), &block)
223
+ end
224
+
225
+ # Injects text right after the module definition. Since it depends on
226
+ # insert_into_file, it's reversible.
227
+ #
228
+ # ==== Parameters
229
+ # path<String>:: path of the file to be changed
230
+ # module_name<String|Class>:: the module to be manipulated
231
+ # data<String>:: the data to append to the class, can be also given as a block.
232
+ # config<Hash>:: give :verbose => false to not log the status.
233
+ #
234
+ # ==== Examples
235
+ #
236
+ # inject_into_module "app/helpers/application_helper.rb", "ApplicationHelper", " def help; 'help'; end\n"
237
+ #
238
+ # inject_into_module "app/helpers/application_helper.rb", "ApplicationHelper" do
239
+ # " def help; 'help'; end\n"
240
+ # end
241
+ #
242
+ def inject_into_module(path, module_name, *args, &block)
243
+ config = args.last.is_a?(Hash) ? args.pop : {}
244
+ config[:after] = /module #{module_name}\n|module #{module_name} .*\n/
199
245
  insert_into_file(path, *(args << config), &block)
200
246
  end
201
247
 
@@ -205,7 +251,8 @@ class Thor
205
251
  # path<String>:: path of the file to be changed
206
252
  # flag<Regexp|String>:: the regexp or string to be replaced
207
253
  # replacement<String>:: the replacement, can be also given as a block
208
- # config<Hash>:: give :verbose => false to not log the status.
254
+ # config<Hash>:: give :verbose => false to not log the status, and
255
+ # :force => true, to force the replacement regardles of runner behavior.
209
256
  #
210
257
  # ==== Example
211
258
  #
@@ -216,16 +263,17 @@ class Thor
216
263
  # end
217
264
  #
218
265
  def gsub_file(path, flag, *args, &block)
219
- return unless behavior == :invoke
220
266
  config = args.last.is_a?(Hash) ? args.pop : {}
221
267
 
268
+ return unless behavior == :invoke || config.fetch(:force, false)
269
+
222
270
  path = File.expand_path(path, destination_root)
223
271
  say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
224
272
 
225
273
  unless options[:pretend]
226
274
  content = File.binread(path)
227
275
  content.gsub!(flag, *args, &block)
228
- File.open(path, 'wb') { |file| file.write(content) }
276
+ File.open(path, "wb") { |file| file.write(content) }
229
277
  end
230
278
  end
231
279
 
@@ -245,7 +293,7 @@ class Thor
245
293
  def uncomment_lines(path, flag, *args)
246
294
  flag = flag.respond_to?(:source) ? flag.source : flag
247
295
 
248
- gsub_file(path, /^(\s*)#\s*(.*#{flag})/, '\1\2', *args)
296
+ gsub_file(path, /^(\s*)#[[:blank:]]*(.*#{flag})/, '\1\2', *args)
249
297
  end
250
298
 
251
299
  # Comment all lines matching a given regex. It will leave the space
@@ -264,7 +312,7 @@ class Thor
264
312
  def comment_lines(path, flag, *args)
265
313
  flag = flag.respond_to?(:source) ? flag.source : flag
266
314
 
267
- gsub_file(path, /^(\s*)([^#|\n]*#{flag})/, '\1# \2', *args)
315
+ gsub_file(path, /^(\s*)([^#\n]*#{flag})/, '\1# \2', *args)
268
316
  end
269
317
 
270
318
  # Removes a file at the given location.
@@ -278,31 +326,50 @@ class Thor
278
326
  # remove_file 'README'
279
327
  # remove_file 'app/controllers/application_controller.rb'
280
328
  #
281
- def remove_file(path, config={})
329
+ def remove_file(path, config = {})
282
330
  return unless behavior == :invoke
283
- path = File.expand_path(path, destination_root)
331
+ path = File.expand_path(path, destination_root)
284
332
 
285
333
  say_status :remove, relative_to_original_destination_root(path), config.fetch(:verbose, true)
286
- ::FileUtils.rm_rf(path) if !options[:pretend] && File.exists?(path)
334
+ if !options[:pretend] && (File.exist?(path) || File.symlink?(path))
335
+ require "fileutils"
336
+ ::FileUtils.rm_rf(path)
337
+ end
287
338
  end
288
- alias :remove_dir :remove_file
339
+ alias_method :remove_dir, :remove_file
289
340
 
290
- private
291
341
  attr_accessor :output_buffer
342
+ private :output_buffer, :output_buffer=
343
+
344
+ private
345
+
292
346
  def concat(string)
293
347
  @output_buffer.concat(string)
294
348
  end
295
349
 
296
- def capture(*args, &block)
297
- with_output_buffer { block.call(*args) }
350
+ def capture(*args)
351
+ with_output_buffer { yield(*args) }
298
352
  end
299
353
 
300
- def with_output_buffer(buf = '') #:nodoc:
301
- self.output_buffer, old_buffer = buf, output_buffer
354
+ def with_output_buffer(buf = "".dup) #:nodoc:
355
+ raise ArgumentError, "Buffer can not be a frozen object" if buf.frozen?
356
+ old_buffer = output_buffer
357
+ self.output_buffer = buf
302
358
  yield
303
359
  output_buffer
304
360
  ensure
305
361
  self.output_buffer = old_buffer
306
362
  end
363
+
364
+ # Thor::Actions#capture depends on what kind of buffer is used in ERB.
365
+ # Thus CapturableERB fixes ERB to use String buffer.
366
+ class CapturableERB < ERB
367
+ def set_eoutvar(compiler, eoutvar = "_erbout")
368
+ compiler.put_cmd = "#{eoutvar}.concat"
369
+ compiler.insert_cmd = "#{eoutvar}.concat"
370
+ compiler.pre_cmd = ["#{eoutvar} = ''.dup"]
371
+ compiler.post_cmd = [eoutvar]
372
+ end
373
+ end
307
374
  end
308
375
  end
@@ -1,8 +1,7 @@
1
- require 'thor/actions/empty_directory'
1
+ require_relative "empty_directory"
2
2
 
3
3
  class Thor
4
4
  module Actions
5
-
6
5
  # Injects the given content into a file. Different from gsub_file, this
7
6
  # method is reversible.
8
7
  #
@@ -22,12 +21,14 @@ class Thor
22
21
  # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n")
23
22
  # end
24
23
  #
24
+ WARNINGS = { unchanged_no_flag: 'File unchanged! The supplied flag value not found!' }
25
+
25
26
  def insert_into_file(destination, *args, &block)
26
- if block_given?
27
- data, config = block, args.shift
28
- else
29
- data, config = args.shift, args.shift
30
- end
27
+ data = block_given? ? block : args.shift
28
+
29
+ config = args.shift || {}
30
+ config[:after] = /\z/ unless config.key?(:before) || config.key?(:after)
31
+
31
32
  action InjectIntoFile.new(self, destination, data, config)
32
33
  end
33
34
  alias_method :inject_into_file, :insert_into_file
@@ -36,7 +37,7 @@ class Thor
36
37
  attr_reader :replacement, :flag, :behavior
37
38
 
38
39
  def initialize(base, destination, data, config)
39
- super(base, destination, { :verbose => true }.merge(config))
40
+ super(base, destination, {:verbose => true}.merge(config))
40
41
 
41
42
  @behavior, @flag = if @config.key?(:after)
42
43
  [:after, @config.delete(:after)]
@@ -49,15 +50,23 @@ class Thor
49
50
  end
50
51
 
51
52
  def invoke!
52
- say_status :invoke
53
-
54
53
  content = if @behavior == :after
55
54
  '\0' + replacement
56
55
  else
57
56
  replacement + '\0'
58
57
  end
59
58
 
60
- replace!(/#{flag}/, content, config[:force])
59
+ if exists?
60
+ if replace!(/#{flag}/, content, config[:force])
61
+ say_status(:invoke)
62
+ else
63
+ say_status(:unchanged, warning: WARNINGS[:unchanged_no_flag], color: :red)
64
+ end
65
+ else
66
+ unless pretend?
67
+ raise Thor::Error, "The file #{ destination } does not appear to exist"
68
+ end
69
+ end
61
70
  end
62
71
 
63
72
  def revoke!
@@ -74,36 +83,37 @@ class Thor
74
83
  replace!(regexp, content, true)
75
84
  end
76
85
 
77
- protected
78
-
79
- def say_status(behavior)
80
- status = if behavior == :invoke
81
- if flag == /\A/
82
- :prepend
83
- elsif flag == /\z/
84
- :append
85
- else
86
- :insert
87
- end
86
+ protected
87
+
88
+ def say_status(behavior, warning: nil, color: nil)
89
+ status = if behavior == :invoke
90
+ if flag == /\A/
91
+ :prepend
92
+ elsif flag == /\z/
93
+ :append
88
94
  else
89
- :subtract
95
+ :insert
90
96
  end
91
-
92
- super(status, config[:verbose])
97
+ elsif warning
98
+ warning
99
+ else
100
+ :subtract
93
101
  end
94
102
 
95
- # Adds the content to the file.
96
- #
97
- def replace!(regexp, string, force)
98
- unless base.options[:pretend]
99
- content = File.binread(destination)
100
- if force || !content.include?(replacement)
101
- content.gsub!(regexp, string)
102
- File.open(destination, 'wb') { |file| file.write(content) }
103
- end
104
- end
105
- end
103
+ super(status, (color || config[:verbose]))
104
+ end
105
+
106
+ # Adds the content to the file.
107
+ #
108
+ def replace!(regexp, string, force)
109
+ content = File.read(destination)
110
+ if force || !content.include?(replacement)
111
+ success = content.gsub!(regexp, string)
106
112
 
113
+ File.open(destination, "wb") { |file| file.write(content) } unless pretend?
114
+ success
115
+ end
116
+ end
107
117
  end
108
118
  end
109
119
  end