Skip to content

Commit 048bbdd

Browse files
authored
Merge pull request #877 from G-Rath/gsub_file-error-on-no-change
feat: support `gsub_file` erroring if gsub doesn't change anything, and add `gsub_file!`
2 parents 13bd825 + ae14cb1 commit 048bbdd

File tree

2 files changed

+144
-5
lines changed

2 files changed

+144
-5
lines changed

lib/thor/actions/file_manipulation.rb

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,35 @@ def inject_into_module(path, module_name, *args, &block)
242242
insert_into_file(path, *(args << config), &block)
243243
end
244244

245+
# Run a regular expression replacement on a file, raising an error if the
246+
# contents of the file are not changed.
247+
#
248+
# ==== Parameters
249+
# path<String>:: path of the file to be changed
250+
# flag<Regexp|String>:: the regexp or string to be replaced
251+
# replacement<String>:: the replacement, can be also given as a block
252+
# config<Hash>:: give :verbose => false to not log the status, and
253+
# :force => true, to force the replacement regardless of runner behavior.
254+
#
255+
# ==== Example
256+
#
257+
# gsub_file! 'app/controllers/application_controller.rb', /#\s*(filter_parameter_logging :password)/, '\1'
258+
#
259+
# gsub_file! 'README', /rake/, :green do |match|
260+
# match << " no more. Use thor!"
261+
# end
262+
#
263+
def gsub_file!(path, flag, *args, &block)
264+
config = args.last.is_a?(Hash) ? args.pop : {}
265+
266+
return unless behavior == :invoke || config.fetch(:force, false)
267+
268+
path = File.expand_path(path, destination_root)
269+
say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
270+
271+
actually_gsub_file(path, flag, args, true, &block) unless options[:pretend]
272+
end
273+
245274
# Run a regular expression replacement on a file.
246275
#
247276
# ==== Parameters
@@ -267,11 +296,7 @@ def gsub_file(path, flag, *args, &block)
267296
path = File.expand_path(path, destination_root)
268297
say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
269298

270-
unless options[:pretend]
271-
content = File.binread(path)
272-
content.gsub!(flag, *args, &block)
273-
File.open(path, "wb") { |file| file.write(content) }
274-
end
299+
actually_gsub_file(path, flag, args, false, &block) unless options[:pretend]
275300
end
276301

277302
# Uncomment all lines matching a given regex. Preserves indentation before
@@ -357,6 +382,17 @@ def with_output_buffer(buf = "".dup) #:nodoc:
357382
self.output_buffer = old_buffer
358383
end
359384

385+
def actually_gsub_file(path, flag, args, error_on_no_change, &block)
386+
content = File.binread(path)
387+
success = content.gsub!(flag, *args, &block)
388+
389+
if success.nil? && error_on_no_change
390+
raise Thor::Error, "The content of #{path} did not change"
391+
end
392+
393+
File.open(path, "wb") { |file| file.write(content) }
394+
end
395+
360396
# Thor::Actions#capture depends on what kind of buffer is used in ERB.
361397
# Thus CapturableERB fixes ERB to use String buffer.
362398
class CapturableERB < ERB

spec/actions/file_manipulation_spec.rb

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,104 @@ def file
316316
end
317317
end
318318

319+
describe "#gsub_file!" do
320+
context "with invoke behavior" do
321+
it "replaces the content in the file" do
322+
action :gsub_file!, "doc/README", "__start__", "START"
323+
expect(File.binread(file)).to eq("START\nREADME\n__end__\n")
324+
end
325+
326+
it "does not replace if pretending" do
327+
runner(pretend: true)
328+
action :gsub_file!, "doc/README", "__start__", "START"
329+
expect(File.binread(file)).to eq("__start__\nREADME\n__end__\n")
330+
end
331+
332+
it "accepts a block" do
333+
action(:gsub_file!, "doc/README", "__start__") { |match| match.gsub("__", "").upcase }
334+
expect(File.binread(file)).to eq("START\nREADME\n__end__\n")
335+
end
336+
337+
it "logs status" do
338+
expect(action(:gsub_file!, "doc/README", "__start__", "START")).to eq(" gsub doc/README\n")
339+
end
340+
341+
it "does not log status if required" do
342+
expect(action(:gsub_file!, file, "__", verbose: false) { |match| match * 2 }).to be_empty
343+
end
344+
345+
it "cares if the file contents did not change" do
346+
expect do
347+
action :gsub_file!, "doc/README", "___start___", "START"
348+
end.to raise_error(Thor::Error, "The content of #{destination_root}/doc/README did not change")
349+
350+
expect(File.binread(file)).to eq("__start__\nREADME\n__end__\n")
351+
end
352+
end
353+
354+
context "with revoke behavior" do
355+
context "and no force option" do
356+
it "does not replace the content in the file" do
357+
runner({}, :revoke)
358+
action :gsub_file!, "doc/README", "__start__", "START"
359+
expect(File.binread(file)).to eq("__start__\nREADME\n__end__\n")
360+
end
361+
362+
it "does not replace if pretending" do
363+
runner({pretend: true}, :revoke)
364+
action :gsub_file!, "doc/README", "__start__", "START"
365+
expect(File.binread(file)).to eq("__start__\nREADME\n__end__\n")
366+
end
367+
368+
it "does not replace the content in the file when given a block" do
369+
runner({}, :revoke)
370+
action(:gsub_file!, "doc/README", "__start__") { |match| match.gsub("__", "").upcase }
371+
expect(File.binread(file)).to eq("__start__\nREADME\n__end__\n")
372+
end
373+
374+
it "does not log status" do
375+
runner({}, :revoke)
376+
expect(action(:gsub_file!, "doc/README", "__start__", "START")).to be_empty
377+
end
378+
379+
it "does not log status if required" do
380+
runner({}, :revoke)
381+
expect(action(:gsub_file!, file, "__", verbose: false) { |match| match * 2 }).to be_empty
382+
end
383+
end
384+
385+
context "and force option" do
386+
it "replaces the content in the file" do
387+
runner({}, :revoke)
388+
action :gsub_file!, "doc/README", "__start__", "START", force: true
389+
expect(File.binread(file)).to eq("START\nREADME\n__end__\n")
390+
end
391+
392+
it "does not replace if pretending" do
393+
runner({pretend: true}, :revoke)
394+
action :gsub_file!, "doc/README", "__start__", "START", force: true
395+
expect(File.binread(file)).to eq("__start__\nREADME\n__end__\n")
396+
end
397+
398+
it "replaces the content in the file when given a block" do
399+
runner({}, :revoke)
400+
action(:gsub_file!, "doc/README", "__start__", force: true) { |match| match.gsub("__", "").upcase }
401+
expect(File.binread(file)).to eq("START\nREADME\n__end__\n")
402+
end
403+
404+
it "logs status" do
405+
runner({}, :revoke)
406+
expect(action(:gsub_file!, "doc/README", "__start__", "START", force: true)).to eq(" gsub doc/README\n")
407+
end
408+
409+
it "does not log status if required" do
410+
runner({}, :revoke)
411+
expect(action(:gsub_file!, file, "__", verbose: false, force: true) { |match| match * 2 }).to be_empty
412+
end
413+
end
414+
end
415+
end
416+
319417
describe "#gsub_file" do
320418
context "with invoke behavior" do
321419
it "replaces the content in the file" do
@@ -341,6 +439,11 @@ def file
341439
it "does not log status if required" do
342440
expect(action(:gsub_file, file, "__", verbose: false) { |match| match * 2 }).to be_empty
343441
end
442+
443+
it "does not care if the file contents did not change" do
444+
action :gsub_file, "doc/README", "___start___", "START"
445+
expect(File.binread(file)).to eq("__start__\nREADME\n__end__\n")
446+
end
344447
end
345448

346449
context "with revoke behavior" do

0 commit comments

Comments
 (0)