thor 0.19.4 → 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.
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).