thor 0.16.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
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