thor 0.19.4 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 5fd71663b46487af27e6f0b6a0206f5140d0d196
4
- data.tar.gz: d67e482d506417552648630f8a7bc4b1886fc06c
2
+ SHA256:
3
+ metadata.gz: 30e79d2b0a96e87c8e6348467db577c6ad1e9acbbbac5d375417bc3e5a2b7698
4
+ data.tar.gz: 701f1ab842da90e599b96bd00d90481d716eb29e39e0283b5ff527c2033fc742
5
5
  SHA512:
6
- metadata.gz: f15702a93adea15d623fd708962193b42bc38bf2b86dff66fe43f1078971fc38f5dedff0d6a78bd1bf9241315e83ede0c216ffedea8c452c55b57aff17d0d051
7
- data.tar.gz: 1e7093648f0913e9c7e32c796ea795e819ca59331588885059bfcae9867172ea50144033c342d22c4d5c8ab628f135821bb9a0ab8645f7784c396482370b98c0
6
+ metadata.gz: 73b1ac80575d4422204cd8072950b5594739db3b6f3fde0f2f04359d51b1d4428524d25b9a3003ae9ec3f6be615cf635f3057bbc65558e6a17ba490ff045988b
7
+ data.tar.gz: eb7761a5e6f3674cb3231398145978b0eb53a6fa2c10e4cb9e99d8d523988efcefc8cae5dd163b8a3d6ead7424422d58b92311c200790ad6e2416e3f9757a90e
data/README.md CHANGED
@@ -2,16 +2,8 @@ Thor
2
2
  ====
3
3
 
4
4
  [![Gem Version](http://img.shields.io/gem/v/thor.svg)][gem]
5
- [![Build Status](http://img.shields.io/travis/erikhuda/thor.svg)][travis]
6
- [![Dependency Status](http://img.shields.io/gemnasium/erikhuda/thor.svg)][gemnasium]
7
- [![Code Climate](http://img.shields.io/codeclimate/github/erikhuda/thor.svg)][codeclimate]
8
- [![Coverage Status](http://img.shields.io/coveralls/erikhuda/thor.svg)][coveralls]
9
5
 
10
6
  [gem]: https://rubygems.org/gems/thor
11
- [travis]: http://travis-ci.org/erikhuda/thor
12
- [gemnasium]: https://gemnasium.com/erikhuda/thor
13
- [codeclimate]: https://codeclimate.com/github/erikhuda/thor
14
- [coveralls]: https://coveralls.io/r/erikhuda/thor
15
7
 
16
8
  Description
17
9
  -----------
@@ -21,7 +13,13 @@ utilities. It removes the pain of parsing command line options, writing
21
13
  build tool. The syntax is Rake-like, so it should be familiar to most Rake
22
14
  users.
23
15
 
16
+ Please note: Thor, by design, is a system tool created to allow seamless file and url
17
+ access, which should not receive application user input. It relies on [open-uri][open-uri],
18
+ which combined with application user input would provide a command injection attack
19
+ vector.
20
+
24
21
  [rake]: https://github.com/ruby/rake
22
+ [open-uri]: https://ruby-doc.org/stdlib-2.5.1/libdoc/open-uri/rdoc/index.html
25
23
 
26
24
  Installation
27
25
  ------------
@@ -31,7 +29,7 @@ Usage and documentation
31
29
  -----------------------
32
30
  Please see the [wiki][] for basic usage and other documentation on using Thor. You can also checkout the [official homepage][homepage].
33
31
 
34
- [wiki]: https://github.com/erikhuda/thor/wiki
32
+ [wiki]: https://github.com/rails/thor/wiki
35
33
  [homepage]: http://whatisthor.com/
36
34
 
37
35
  Contributing
@@ -1,4 +1,4 @@
1
- require "thor/actions/empty_directory"
1
+ require_relative "empty_directory"
2
2
 
3
3
  class Thor
4
4
  module Actions
@@ -58,6 +58,7 @@ class Thor
58
58
 
59
59
  def invoke!
60
60
  invoke_with_conflict_check do
61
+ require "fileutils"
61
62
  FileUtils.mkdir_p(File.dirname(destination))
62
63
  File.open(destination, "wb") { |f| f.write render }
63
64
  end
@@ -1,4 +1,4 @@
1
- require "thor/actions/create_file"
1
+ require_relative "create_file"
2
2
 
3
3
  class Thor
4
4
  module Actions
@@ -33,11 +33,13 @@ class Thor
33
33
  # Boolean:: true if it is identical, false otherwise.
34
34
  #
35
35
  def identical?
36
- exists? && File.identical?(render, destination)
36
+ source = File.expand_path(render, File.dirname(destination))
37
+ exists? && File.identical?(source, destination)
37
38
  end
38
39
 
39
40
  def invoke!
40
41
  invoke_with_conflict_check do
42
+ require "fileutils"
41
43
  FileUtils.mkdir_p(File.dirname(destination))
42
44
  # Create a symlink by default
43
45
  config[:symbolic] = true if config[:symbolic].nil?
@@ -1,4 +1,4 @@
1
- require "thor/actions/empty_directory"
1
+ require_relative "empty_directory"
2
2
 
3
3
  class Thor
4
4
  module Actions
@@ -56,7 +56,7 @@ class Thor
56
56
  attr_reader :source
57
57
 
58
58
  def initialize(base, source, destination = nil, config = {}, &block)
59
- @source = File.expand_path(base.find_in_source_paths(source.to_s))
59
+ @source = File.expand_path(Dir[Util.escape_globs(base.find_in_source_paths(source.to_s))].first)
60
60
  @block = block
61
61
  super(base, destination, {:recursive => true}.merge(config))
62
62
  end
@@ -96,22 +96,12 @@ class Thor
96
96
  end
97
97
  end
98
98
 
99
- if RUBY_VERSION < "2.0"
100
- def file_level_lookup(previous_lookup)
101
- File.join(previous_lookup, "{*,.[a-z]*}")
102
- end
103
-
104
- def files(lookup)
105
- Dir[lookup]
106
- end
107
- else
108
- def file_level_lookup(previous_lookup)
109
- File.join(previous_lookup, "*")
110
- end
99
+ def file_level_lookup(previous_lookup)
100
+ File.join(previous_lookup, "*")
101
+ end
111
102
 
112
- def files(lookup)
113
- Dir.glob(lookup, File::FNM_DOTMATCH)
114
- end
103
+ def files(lookup)
104
+ Dir.glob(lookup, File::FNM_DOTMATCH)
115
105
  end
116
106
  end
117
107
  end
@@ -48,12 +48,14 @@ class Thor
48
48
 
49
49
  def invoke!
50
50
  invoke_with_conflict_check do
51
+ require "fileutils"
51
52
  ::FileUtils.mkdir_p(destination)
52
53
  end
53
54
  end
54
55
 
55
56
  def revoke!
56
57
  say_status :remove, :red
58
+ require "fileutils"
57
59
  ::FileUtils.rm_rf(destination) if !pretend? && exists?
58
60
  given_destination
59
61
  end
@@ -112,11 +114,17 @@ class Thor
112
114
  if exists?
113
115
  on_conflict_behavior(&block)
114
116
  else
115
- say_status :create, :green
116
117
  yield unless pretend?
118
+ say_status :create, :green
117
119
  end
118
120
 
119
121
  destination
122
+ rescue Errno::EISDIR, Errno::EEXIST
123
+ on_file_clash_behavior
124
+ end
125
+
126
+ def on_file_clash_behavior
127
+ say_status :file_clash, :red
120
128
  end
121
129
 
122
130
  # What to do when the destination file already exists.
@@ -1,5 +1,4 @@
1
1
  require "erb"
2
- require "open-uri"
3
2
 
4
3
  class Thor
5
4
  module Actions
@@ -24,14 +23,14 @@ class Thor
24
23
  destination = args.first || source
25
24
  source = File.expand_path(find_in_source_paths(source.to_s))
26
25
 
27
- create_file destination, nil, config do
26
+ resulting_destination = create_file destination, nil, config do
28
27
  content = File.binread(source)
29
28
  content = yield(content) if block
30
29
  content
31
30
  end
32
31
  if config[:mode] == :preserve
33
32
  mode = File.stat(source).mode
34
- chmod(destination, mode, config)
33
+ chmod(resulting_destination, mode, config)
35
34
  end
36
35
  end
37
36
 
@@ -61,6 +60,9 @@ class Thor
61
60
  # destination. If a block is given instead of destination, the content of
62
61
  # the url is yielded and used as location.
63
62
  #
63
+ # +get+ relies on open-uri, so passing application user input would provide
64
+ # a command injection attack vector.
65
+ #
64
66
  # ==== Parameters
65
67
  # source<String>:: the address of the given content.
66
68
  # destination<String>:: the relative path to the destination root.
@@ -78,8 +80,13 @@ class Thor
78
80
  config = args.last.is_a?(Hash) ? args.pop : {}
79
81
  destination = args.first
80
82
 
81
- source = File.expand_path(find_in_source_paths(source.to_s)) unless source =~ %r{^https?\://}
82
- 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
83
90
 
84
91
  destination ||= if block_given?
85
92
  block.arity == 1 ? yield(render) : yield
@@ -113,7 +120,15 @@ class Thor
113
120
  context = config.delete(:context) || instance_eval("binding")
114
121
 
115
122
  create_file destination, nil, config do
116
- content = CapturableERB.new(::File.binread(source), nil, "-", "@output_buffer").result(context)
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)
117
132
  content = yield(content) if block
118
133
  content
119
134
  end
@@ -134,7 +149,10 @@ class Thor
134
149
  return unless behavior == :invoke
135
150
  path = File.expand_path(path, destination_root)
136
151
  say_status :chmod, relative_to_original_destination_root(path), config.fetch(:verbose, true)
137
- FileUtils.chmod_R(mode, path) unless options[:pretend]
152
+ unless options[:pretend]
153
+ require "fileutils"
154
+ FileUtils.chmod_R(mode, path)
155
+ end
138
156
  end
139
157
 
140
158
  # Prepend text to a file. Since it depends on insert_into_file, it's reversible.
@@ -192,9 +210,9 @@ class Thor
192
210
  #
193
211
  # ==== Examples
194
212
  #
195
- # 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"
196
214
  #
197
- # inject_into_class "app/controllers/application_controller.rb", ApplicationController do
215
+ # inject_into_class "app/controllers/application_controller.rb", "ApplicationController" do
198
216
  # " filter_parameter :password\n"
199
217
  # end
200
218
  #
@@ -204,13 +222,37 @@ class Thor
204
222
  insert_into_file(path, *(args << config), &block)
205
223
  end
206
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/
245
+ insert_into_file(path, *(args << config), &block)
246
+ end
247
+
207
248
  # Run a regular expression replacement on a file.
208
249
  #
209
250
  # ==== Parameters
210
251
  # path<String>:: path of the file to be changed
211
252
  # flag<Regexp|String>:: the regexp or string to be replaced
212
253
  # replacement<String>:: the replacement, can be also given as a block
213
- # 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.
214
256
  #
215
257
  # ==== Example
216
258
  #
@@ -221,9 +263,10 @@ class Thor
221
263
  # end
222
264
  #
223
265
  def gsub_file(path, flag, *args, &block)
224
- return unless behavior == :invoke
225
266
  config = args.last.is_a?(Hash) ? args.pop : {}
226
267
 
268
+ return unless behavior == :invoke || config.fetch(:force, false)
269
+
227
270
  path = File.expand_path(path, destination_root)
228
271
  say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
229
272
 
@@ -269,7 +312,7 @@ class Thor
269
312
  def comment_lines(path, flag, *args)
270
313
  flag = flag.respond_to?(:source) ? flag.source : flag
271
314
 
272
- gsub_file(path, /^(\s*)([^#|\n]*#{flag})/, '\1# \2', *args)
315
+ gsub_file(path, /^(\s*)([^#\n]*#{flag})/, '\1# \2', *args)
273
316
  end
274
317
 
275
318
  # Removes a file at the given location.
@@ -288,7 +331,10 @@ class Thor
288
331
  path = File.expand_path(path, destination_root)
289
332
 
290
333
  say_status :remove, relative_to_original_destination_root(path), config.fetch(:verbose, true)
291
- ::FileUtils.rm_rf(path) if !options[:pretend] && File.exist?(path)
334
+ if !options[:pretend] && (File.exist?(path) || File.symlink?(path))
335
+ require "fileutils"
336
+ ::FileUtils.rm_rf(path)
337
+ end
292
338
  end
293
339
  alias_method :remove_dir, :remove_file
294
340
 
@@ -305,8 +351,10 @@ class Thor
305
351
  with_output_buffer { yield(*args) }
306
352
  end
307
353
 
308
- def with_output_buffer(buf = "") #:nodoc:
309
- 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
310
358
  yield
311
359
  output_buffer
312
360
  ensure
@@ -319,7 +367,7 @@ class Thor
319
367
  def set_eoutvar(compiler, eoutvar = "_erbout")
320
368
  compiler.put_cmd = "#{eoutvar}.concat"
321
369
  compiler.insert_cmd = "#{eoutvar}.concat"
322
- compiler.pre_cmd = ["#{eoutvar} = ''"]
370
+ compiler.pre_cmd = ["#{eoutvar} = ''.dup"]
323
371
  compiler.post_cmd = [eoutvar]
324
372
  end
325
373
  end
@@ -1,4 +1,4 @@
1
- require "thor/actions/empty_directory"
1
+ require_relative "empty_directory"
2
2
 
3
3
  class Thor
4
4
  module Actions
@@ -21,9 +21,14 @@ class Thor
21
21
  # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n")
22
22
  # end
23
23
  #
24
+ WARNINGS = { unchanged_no_flag: 'File unchanged! The supplied flag value not found!' }
25
+
24
26
  def insert_into_file(destination, *args, &block)
25
27
  data = block_given? ? block : args.shift
26
- config = args.shift
28
+
29
+ config = args.shift || {}
30
+ config[:after] = /\z/ unless config.key?(:before) || config.key?(:after)
31
+
27
32
  action InjectIntoFile.new(self, destination, data, config)
28
33
  end
29
34
  alias_method :inject_into_file, :insert_into_file
@@ -45,15 +50,23 @@ class Thor
45
50
  end
46
51
 
47
52
  def invoke!
48
- say_status :invoke
49
-
50
53
  content = if @behavior == :after
51
54
  '\0' + replacement
52
55
  else
53
56
  replacement + '\0'
54
57
  end
55
58
 
56
- 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
57
70
  end
58
71
 
59
72
  def revoke!
@@ -72,7 +85,7 @@ class Thor
72
85
 
73
86
  protected
74
87
 
75
- def say_status(behavior)
88
+ def say_status(behavior, warning: nil, color: nil)
76
89
  status = if behavior == :invoke
77
90
  if flag == /\A/
78
91
  :prepend
@@ -81,21 +94,24 @@ class Thor
81
94
  else
82
95
  :insert
83
96
  end
97
+ elsif warning
98
+ warning
84
99
  else
85
100
  :subtract
86
101
  end
87
102
 
88
- super(status, config[:verbose])
103
+ super(status, (color || config[:verbose]))
89
104
  end
90
105
 
91
106
  # Adds the content to the file.
92
107
  #
93
108
  def replace!(regexp, string, force)
94
- return if base.options[:pretend]
95
- content = File.binread(destination)
109
+ content = File.read(destination)
96
110
  if force || !content.include?(replacement)
97
- content.gsub!(regexp, string)
98
- File.open(destination, "wb") { |file| file.write(content) }
111
+ success = content.gsub!(regexp, string)
112
+
113
+ File.open(destination, "wb") { |file| file.write(content) } unless pretend?
114
+ success
99
115
  end
100
116
  end
101
117
  end
data/lib/thor/actions.rb CHANGED
@@ -1,18 +1,16 @@
1
- require "fileutils"
2
- require "uri"
3
- require "thor/core_ext/io_binary_read"
4
- require "thor/actions/create_file"
5
- require "thor/actions/create_link"
6
- require "thor/actions/directory"
7
- require "thor/actions/empty_directory"
8
- require "thor/actions/file_manipulation"
9
- require "thor/actions/inject_into_file"
1
+ require_relative "actions/create_file"
2
+ require_relative "actions/create_link"
3
+ require_relative "actions/directory"
4
+ require_relative "actions/empty_directory"
5
+ require_relative "actions/file_manipulation"
6
+ require_relative "actions/inject_into_file"
10
7
 
11
8
  class Thor
12
9
  module Actions
13
10
  attr_accessor :behavior
14
11
 
15
12
  def self.included(base) #:nodoc:
13
+ super(base)
16
14
  base.extend ClassMethods
17
15
  end
18
16
 
@@ -114,8 +112,10 @@ class Thor
114
112
  # the script started).
115
113
  #
116
114
  def relative_to_original_destination_root(path, remove_dot = true)
117
- path = path.dup
118
- if path.gsub!(@destination_stack[0], ".")
115
+ root = @destination_stack[0]
116
+ if path.start_with?(root) && [File::SEPARATOR, File::ALT_SEPARATOR, nil, ''].include?(path[root.size..root.size])
117
+ path = path.dup
118
+ path[0...root.size] = '.'
119
119
  remove_dot ? (path[2..-1] || "") : path
120
120
  else
121
121
  path
@@ -141,7 +141,7 @@ class Thor
141
141
  end
142
142
  end
143
143
 
144
- message = "Could not find #{file.inspect} in any of your source paths. "
144
+ message = "Could not find #{file.inspect} in any of your source paths. ".dup
145
145
 
146
146
  unless self.class.source_root
147
147
  message << "Please invoke #{self.class.name}.source_root(PATH) with the PATH containing your templates. "
@@ -161,6 +161,8 @@ class Thor
161
161
  # to the block you provide. The path is set back to the previous path when
162
162
  # the method exits.
163
163
  #
164
+ # Returns the value yielded by the block.
165
+ #
164
166
  # ==== Parameters
165
167
  # dir<String>:: the directory to move to.
166
168
  # config<Hash>:: give :verbose => true to log and use padding.
@@ -175,18 +177,22 @@ class Thor
175
177
 
176
178
  # If the directory doesnt exist and we're not pretending
177
179
  if !File.exist?(destination_root) && !pretend
180
+ require "fileutils"
178
181
  FileUtils.mkdir_p(destination_root)
179
182
  end
180
183
 
184
+ result = nil
181
185
  if pretend
182
186
  # In pretend mode, just yield down to the block
183
- block.arity == 1 ? yield(destination_root) : yield
187
+ result = block.arity == 1 ? yield(destination_root) : yield
184
188
  else
185
- FileUtils.cd(destination_root) { block.arity == 1 ? yield(destination_root) : yield }
189
+ require "fileutils"
190
+ FileUtils.cd(destination_root) { result = block.arity == 1 ? yield(destination_root) : yield }
186
191
  end
187
192
 
188
193
  @destination_stack.pop
189
194
  shell.padding -= 1 if verbose
195
+ result
190
196
  end
191
197
 
192
198
  # Goes to the root and execute the given block.
@@ -216,7 +222,8 @@ class Thor
216
222
  shell.padding += 1 if verbose
217
223
 
218
224
  contents = if is_uri
219
- open(path, "Accept" => "application/x-thor-template", &:read)
225
+ require "open-uri"
226
+ URI.open(path, "Accept" => "application/x-thor-template", &:read)
220
227
  else
221
228
  open(path, &:read)
222
229
  end
@@ -251,7 +258,22 @@ class Thor
251
258
 
252
259
  say_status :run, desc, config.fetch(:verbose, true)
253
260
 
254
- !options[:pretend] && config[:capture] ? `#{command}` : system(command.to_s)
261
+ return if options[:pretend]
262
+
263
+ env_splat = [config[:env]] if config[:env]
264
+
265
+ if config[:capture]
266
+ require "open3"
267
+ result, status = Open3.capture2e(*env_splat, command.to_s)
268
+ success = status.success?
269
+ else
270
+ result = system(*env_splat, command.to_s)
271
+ success = result
272
+ end
273
+
274
+ abort if !success && config.fetch(:abort_on_failure, self.class.exit_on_failure?)
275
+
276
+ result
255
277
  end
256
278
 
257
279
  # Executes a ruby script (taking into account WIN32 platform quirks).