diff --git a/lib/thor/actions/inject_into_file.rb b/lib/thor/actions/inject_into_file.rb index be13ddb2..51aed970 100644 --- a/lib/thor/actions/inject_into_file.rb +++ b/lib/thor/actions/inject_into_file.rb @@ -2,6 +2,38 @@ class Thor module Actions + WARNINGS = {unchanged_no_flag: "File unchanged! Either the supplied flag value not found or the content has already been inserted!"} + + # Injects the given content into a file, raising an error if the contents of + # the file are not changed. Different from gsub_file, this method is reversible. + # + # ==== Parameters + # destination:: Relative path to the destination root + # data:: Data to add to the file. Can be given as a block. + # config:: give :verbose => false to not log the status and the flag + # for injection (:after or :before) or :force => true for + # insert two or more times the same content. + # + # ==== Examples + # + # insert_into_file "config/environment.rb", "config.gem :thor", :after => "Rails::Initializer.run do |config|\n" + # + # insert_into_file "config/environment.rb", :after => "Rails::Initializer.run do |config|\n" do + # gems = ask "Which gems would you like to add?" + # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n") + # end + # + def insert_into_file!(destination, *args, &block) + data = block_given? ? block : args.shift + + config = args.shift || {} + config[:after] = /\z/ unless config.key?(:before) || config.key?(:after) + config = config.merge({error_on_no_change: true}) + + action InjectIntoFile.new(self, destination, data, config) + end + alias_method :inject_into_file!, :insert_into_file! + # Injects the given content into a file. Different from gsub_file, this # method is reversible. # @@ -21,8 +53,6 @@ module Actions # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n") # end # - WARNINGS = {unchanged_no_flag: "File unchanged! Either the supplied flag value not found or the content has already been inserted!"} - def insert_into_file(destination, *args, &block) data = block_given? ? block : args.shift @@ -47,6 +77,7 @@ def initialize(base, destination, data, config) @replacement = data.is_a?(Proc) ? data.call : data @flag = Regexp.escape(@flag) unless @flag.is_a?(Regexp) + @error_on_no_change = @config.fetch(:error_on_no_change, false) end def invoke! @@ -59,6 +90,8 @@ def invoke! if exists? if replace!(/#{flag}/, content, config[:force]) say_status(:invoke) + elsif @error_on_no_change + raise Thor::Error, "The content of #{destination} did not change" elsif replacement_present? say_status(:unchanged, color: :blue) else diff --git a/spec/actions/inject_into_file_spec.rb b/spec/actions/inject_into_file_spec.rb index 6ba6dcd6..e46a60c9 100644 --- a/spec/actions/inject_into_file_spec.rb +++ b/spec/actions/inject_into_file_spec.rb @@ -20,6 +20,10 @@ def invoke!(*args, &block) capture(:stdout) { invoker.insert_into_file(*args, &block) } end + def invoke_with_a_bang!(*args, &block) + capture(:stdout) { invoker.insert_into_file!(*args, &block) } + end + def revoke!(*args, &block) capture(:stdout) { revoker.insert_into_file(*args, &block) } end @@ -170,6 +174,164 @@ def file end end end + + context "with a bang" do + it "changes the file adding content after the flag" do + invoke_with_a_bang! "doc/README", "\nmore content", after: "__start__" + expect(File.read(file)).to eq("__start__\nmore content\nREADME\n__end__\n") + end + + it "changes the file adding content before the flag" do + invoke_with_a_bang! "doc/README", "more content\n", before: "__end__" + expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n") + end + + it "appends content to the file if before and after arguments not provided" do + invoke_with_a_bang!("doc/README", "more content\n") + expect(File.read(file)).to eq("__start__\nREADME\n__end__\nmore content\n") + end + + it "does not change the file and raises an error if replacement present in the file" do + invoke_with_a_bang!("doc/README", "more specific content\n") + expect do + invoke_with_a_bang!("doc/README", "more specific content\n") + end.to raise_error(Thor::Error, "The content of #{destination_root}/doc/README did not change") + end + + it "does not change the file and raises an error if flag not found in the file" do + expect do + invoke_with_a_bang!("doc/README", "more content\n", after: "whatever") + end.to raise_error(Thor::Error, "The content of #{destination_root}/doc/README did not change") + end + + it "accepts data as a block" do + invoke_with_a_bang! "doc/README", before: "__end__" do + "more content\n" + end + + expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n") + end + + it "logs status" do + expect(invoke_with_a_bang!("doc/README", "\nmore content", after: "__start__")).to eq(" insert doc/README\n") + end + + it "logs status if pretending" do + invoker(pretend: true) + expect(invoke_with_a_bang!("doc/README", "\nmore content", after: "__start__")).to eq(" insert doc/README\n") + end + + it "does not change the file if pretending" do + invoker pretend: true + invoke_with_a_bang! "doc/README", "\nmore content", after: "__start__" + expect(File.read(file)).to eq("__start__\nREADME\n__end__\n") + end + + it "does not change the file and raises an error if already includes content" do + invoke_with_a_bang! "doc/README", before: "__end__" do + "more content\n" + end + + expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n") + + expect do + invoke_with_a_bang! "doc/README", before: "__end__" do + "more content\n" + end + end.to raise_error(Thor::Error, "The content of #{destination_root}/doc/README did not change") + + expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n") + end + + it "does not change the file and raises an error if already includes content using before with capture" do + invoke_with_a_bang! "doc/README", before: /(__end__)/ do + "more content\n" + end + + expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n") + + expect do + invoke_with_a_bang! "doc/README", before: /(__end__)/ do + "more content\n" + end + end.to raise_error(Thor::Error, "The content of #{destination_root}/doc/README did not change") + + expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n") + end + + it "does not change the file and raises an error if already includes content using after with capture" do + invoke_with_a_bang! "doc/README", after: /(README\n)/ do + "more content\n" + end + + expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n") + + expect do + invoke_with_a_bang! "doc/README", after: /(README\n)/ do + "more content\n" + end + end.to raise_error(Thor::Error, "The content of #{destination_root}/doc/README did not change") + + expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n") + end + + it "does not attempt to change the file if it doesn't exist - instead raises Thor::Error" do + expect do + invoke_with_a_bang! "idontexist", before: "something" do + "any content" + end + end.to raise_error(Thor::Error, /does not appear to exist/) + expect(File.exist?("idontexist")).to be_falsey + end + + it "does not attempt to change the file if it doesn't exist and pretending" do + expect do + invoker pretend: true + invoke_with_a_bang! "idontexist", before: "something" do + "any content" + end + end.not_to raise_error + expect(File.exist?("idontexist")).to be_falsey + end + + it "does change the file if already includes content and :force is true" do + invoke_with_a_bang! "doc/README", before: "__end__" do + "more content\n" + end + + expect(File.read(file)).to eq("__start__\nREADME\nmore content\n__end__\n") + + invoke_with_a_bang! "doc/README", before: "__end__", force: true do + "more content\n" + end + + expect(File.read(file)).to eq("__start__\nREADME\nmore content\nmore content\n__end__\n") + end + + it "can insert chinese" do + encoding_original = Encoding.default_external + + begin + silence_warnings do + Encoding.default_external = Encoding.find("UTF-8") + end + invoke_with_a_bang! "doc/README.zh", "\n中文", after: "__start__" + expect(File.read(File.join(destination_root, "doc/README.zh"))).to eq("__start__\n中文\n说明\n__end__\n") + ensure + silence_warnings do + Encoding.default_external = encoding_original + end + end + end + + it "does not mutate the provided config" do + config = {after: "__start__"} + invoke_with_a_bang! "doc/README", "\nmore content", config + expect(File.read(file)).to eq("__start__\nmore content\nREADME\n__end__\n") + + expect(config).to eq({after: "__start__"}) + end + end end describe "#revoke!" do