diff options
author | hsbt <hsbt@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2018-11-02 23:07:56 +0000 |
---|---|---|
committer | hsbt <hsbt@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2018-11-02 23:07:56 +0000 |
commit | 59c8d50653480bef3f24517296e6ddf937fdf6bc (patch) | |
tree | df10aaf4f3307837fe3d1d129d66f6c0c7586bc5 /lib/bundler/vendor/thor/lib/thor/actions | |
parent | 7deb37777a230837e865e0a11fb8d7c1dc6d03ce (diff) |
Added bundler as default gems. Revisit [Feature #12733]
* bin/*, lib/bundler/*, lib/bundler.rb, spec/bundler, man/*:
Merge from latest stable branch of bundler/bundler repository and
added workaround patches. I will backport them into upstream.
* common.mk, defs/gmake.mk: Added `test-bundler` task for test suite
of bundler.
* tool/sync_default_gems.rb: Added sync task for bundler.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@65509 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'lib/bundler/vendor/thor/lib/thor/actions')
6 files changed, 898 insertions, 0 deletions
diff --git a/lib/bundler/vendor/thor/lib/thor/actions/create_file.rb b/lib/bundler/vendor/thor/lib/thor/actions/create_file.rb new file mode 100644 index 0000000000..97d22d9bbd --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/actions/create_file.rb @@ -0,0 +1,104 @@ +require "bundler/vendor/thor/lib/thor/actions/empty_directory" + +class Bundler::Thor + module Actions + # Create a new file relative to the destination root with the given data, + # which is the return value of a block or a data string. + # + # ==== Parameters + # destination<String>:: the relative path to the destination root. + # data<String|NilClass>:: the data to append to the file. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # create_file "lib/fun_party.rb" do + # hostname = ask("What is the virtual hostname I should use?") + # "vhost.name = #{hostname}" + # end + # + # create_file "config/apache.conf", "your apache config" + # + def create_file(destination, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + data = args.first + action CreateFile.new(self, destination, block || data.to_s, config) + end + alias_method :add_file, :create_file + + # CreateFile is a subset of Template, which instead of rendering a file with + # ERB, it gets the content from the user. + # + class CreateFile < EmptyDirectory #:nodoc: + attr_reader :data + + def initialize(base, destination, data, config = {}) + @data = data + super(base, destination, config) + end + + # Checks if the content of the file at the destination is identical to the rendered result. + # + # ==== Returns + # Boolean:: true if it is identical, false otherwise. + # + def identical? + exists? && File.binread(destination) == render + end + + # Holds the content to be added to the file. + # + def render + @render ||= if data.is_a?(Proc) + data.call + else + data + end + end + + def invoke! + invoke_with_conflict_check do + require "fileutils" + FileUtils.mkdir_p(File.dirname(destination)) + File.open(destination, "wb") { |f| f.write render } + end + given_destination + end + + protected + + # Now on conflict we check if the file is identical or not. + # + def on_conflict_behavior(&block) + if identical? + say_status :identical, :blue + else + options = base.options.merge(config) + force_or_skip_or_conflict(options[:force], options[:skip], &block) + end + end + + # If force is true, run the action, otherwise check if it's not being + # skipped. If both are false, show the file_collision menu, if the menu + # returns true, force it, otherwise skip. + # + def force_or_skip_or_conflict(force, skip, &block) + if force + say_status :force, :yellow + yield unless pretend? + elsif skip + say_status :skip, :yellow + else + say_status :conflict, :red + force_or_skip_or_conflict(force_on_collision?, true, &block) + end + end + + # Shows the file collision menu to the user and gets the result. + # + def force_on_collision? + base.shell.file_collision(destination) { render } + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/actions/create_link.rb b/lib/bundler/vendor/thor/lib/thor/actions/create_link.rb new file mode 100644 index 0000000000..3a664401b4 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/actions/create_link.rb @@ -0,0 +1,60 @@ +require "bundler/vendor/thor/lib/thor/actions/create_file" + +class Bundler::Thor + module Actions + # Create a new file relative to the destination root from the given source. + # + # ==== Parameters + # destination<String>:: the relative path to the destination root. + # source<String|NilClass>:: the relative path to the source root. + # config<Hash>:: give :verbose => false to not log the status. + # :: give :symbolic => false for hard link. + # + # ==== Examples + # + # create_link "config/apache.conf", "/etc/apache.conf" + # + def create_link(destination, *args) + config = args.last.is_a?(Hash) ? args.pop : {} + source = args.first + action CreateLink.new(self, destination, source, config) + end + alias_method :add_link, :create_link + + # CreateLink is a subset of CreateFile, which instead of taking a block of + # data, just takes a source string from the user. + # + class CreateLink < CreateFile #:nodoc: + attr_reader :data + + # Checks if the content of the file at the destination is identical to the rendered result. + # + # ==== Returns + # Boolean:: true if it is identical, false otherwise. + # + def identical? + exists? && File.identical?(render, destination) + end + + def invoke! + invoke_with_conflict_check do + require "fileutils" + FileUtils.mkdir_p(File.dirname(destination)) + # Create a symlink by default + config[:symbolic] = true if config[:symbolic].nil? + File.unlink(destination) if exists? + if config[:symbolic] + File.symlink(render, destination) + else + File.link(render, destination) + end + end + given_destination + end + + def exists? + super || File.symlink?(destination) + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/actions/directory.rb b/lib/bundler/vendor/thor/lib/thor/actions/directory.rb new file mode 100644 index 0000000000..f555f7b7e0 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/actions/directory.rb @@ -0,0 +1,118 @@ +require "bundler/vendor/thor/lib/thor/actions/empty_directory" + +class Bundler::Thor + module Actions + # Copies recursively the files from source directory to root directory. + # If any of the files finishes with .tt, it's considered to be a template + # and is placed in the destination without the extension .tt. If any + # empty directory is found, it's copied and all .empty_directory files are + # ignored. If any file name is wrapped within % signs, the text within + # the % signs will be executed as a method and replaced with the returned + # value. Let's suppose a doc directory with the following files: + # + # doc/ + # components/.empty_directory + # README + # rdoc.rb.tt + # %app_name%.rb + # + # When invoked as: + # + # directory "doc" + # + # It will create a doc directory in the destination with the following + # files (assuming that the `app_name` method returns the value "blog"): + # + # doc/ + # components/ + # README + # rdoc.rb + # blog.rb + # + # <b>Encoded path note:</b> Since Bundler::Thor internals use Object#respond_to? to check if it can + # expand %something%, this `something` should be a public method in the class calling + # #directory. If a method is private, Bundler::Thor stack raises PrivateMethodEncodedError. + # + # ==== Parameters + # source<String>:: the relative path to the source root. + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status. + # If :recursive => false, does not look for paths recursively. + # If :mode => :preserve, preserve the file mode from the source. + # If :exclude_pattern => /regexp/, prevents copying files that match that regexp. + # + # ==== Examples + # + # directory "doc" + # directory "doc", "docs", :recursive => false + # + def directory(source, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + destination = args.first || source + action Directory.new(self, source, destination || source, config, &block) + end + + class Directory < EmptyDirectory #:nodoc: + attr_reader :source + + def initialize(base, source, destination = nil, config = {}, &block) + @source = File.expand_path(base.find_in_source_paths(source.to_s)) + @block = block + super(base, destination, {:recursive => true}.merge(config)) + end + + def invoke! + base.empty_directory given_destination, config + execute! + end + + def revoke! + execute! + end + + protected + + def execute! + lookup = Util.escape_globs(source) + lookup = config[:recursive] ? File.join(lookup, "**") : lookup + lookup = file_level_lookup(lookup) + + files(lookup).sort.each do |file_source| + next if File.directory?(file_source) + next if config[:exclude_pattern] && file_source.match(config[:exclude_pattern]) + file_destination = File.join(given_destination, file_source.gsub(source, ".")) + file_destination.gsub!("/./", "/") + + case file_source + when /\.empty_directory$/ + dirname = File.dirname(file_destination).gsub(%r{/\.$}, "") + next if dirname == given_destination + base.empty_directory(dirname, config) + when /#{TEMPLATE_EXTNAME}$/ + base.template(file_source, file_destination[0..-4], config, &@block) + else + base.copy_file(file_source, file_destination, config, &@block) + end + end + end + + if RUBY_VERSION < "2.0" + def file_level_lookup(previous_lookup) + File.join(previous_lookup, "{*,.[a-z]*}") + end + + def files(lookup) + Dir[lookup] + end + else + def file_level_lookup(previous_lookup) + File.join(previous_lookup, "*") + end + + def files(lookup) + Dir.glob(lookup, File::FNM_DOTMATCH) + end + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb b/lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb new file mode 100644 index 0000000000..284d92c19a --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb @@ -0,0 +1,143 @@ +class Bundler::Thor + module Actions + # Creates an empty directory. + # + # ==== Parameters + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # empty_directory "doc" + # + def empty_directory(destination, config = {}) + action EmptyDirectory.new(self, destination, config) + end + + # Class which holds create directory logic. This is the base class for + # other actions like create_file and directory. + # + # This implementation is based in Templater actions, created by Jonas Nicklas + # and Michael S. Klishin under MIT LICENSE. + # + class EmptyDirectory #:nodoc: + attr_reader :base, :destination, :given_destination, :relative_destination, :config + + # Initializes given the source and destination. + # + # ==== Parameters + # base<Bundler::Thor::Base>:: A Bundler::Thor::Base instance + # source<String>:: Relative path to the source of this file + # destination<String>:: Relative path to the destination of this file + # config<Hash>:: give :verbose => false to not log the status. + # + def initialize(base, destination, config = {}) + @base = base + @config = {:verbose => true}.merge(config) + self.destination = destination + end + + # Checks if the destination file already exists. + # + # ==== Returns + # Boolean:: true if the file exists, false otherwise. + # + def exists? + ::File.exist?(destination) + end + + def invoke! + invoke_with_conflict_check do + require "fileutils" + ::FileUtils.mkdir_p(destination) + end + end + + def revoke! + say_status :remove, :red + require "fileutils" + ::FileUtils.rm_rf(destination) if !pretend? && exists? + given_destination + end + + protected + + # Shortcut for pretend. + # + def pretend? + base.options[:pretend] + end + + # Sets the absolute destination value from a relative destination value. + # It also stores the given and relative destination. Let's suppose our + # script is being executed on "dest", it sets the destination root to + # "dest". The destination, given_destination and relative_destination + # are related in the following way: + # + # inside "bar" do + # empty_directory "baz" + # end + # + # destination #=> dest/bar/baz + # relative_destination #=> bar/baz + # given_destination #=> baz + # + def destination=(destination) + return unless destination + @given_destination = convert_encoded_instructions(destination.to_s) + @destination = ::File.expand_path(@given_destination, base.destination_root) + @relative_destination = base.relative_to_original_destination_root(@destination) + end + + # Filenames in the encoded form are converted. If you have a file: + # + # %file_name%.rb + # + # It calls #file_name from the base and replaces %-string with the + # return value (should be String) of #file_name: + # + # user.rb + # + # The method referenced can be either public or private. + # + def convert_encoded_instructions(filename) + filename.gsub(/%(.*?)%/) do |initial_string| + method = $1.strip + base.respond_to?(method, true) ? base.send(method) : initial_string + end + end + + # Receives a hash of options and just execute the block if some + # conditions are met. + # + def invoke_with_conflict_check(&block) + if exists? + on_conflict_behavior(&block) + else + yield unless pretend? + say_status :create, :green + end + + destination + rescue Errno::EISDIR, Errno::EEXIST + on_file_clash_behavior + end + + def on_file_clash_behavior + say_status :file_clash, :red + end + + # What to do when the destination file already exists. + # + def on_conflict_behavior + say_status :exist, :blue + end + + # Shortcut to say_status shell method. + # + def say_status(status, color) + base.shell.say_status status, relative_destination, color if config[:verbose] + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb b/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb new file mode 100644 index 0000000000..4c83bebc86 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb @@ -0,0 +1,364 @@ +require "erb" + +class Bundler::Thor + module Actions + # Copies the file from the relative source to the relative destination. If + # the destination is not given it's assumed to be equal to the source. + # + # ==== Parameters + # source<String>:: the relative path to the source root. + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status, and + # :mode => :preserve, to preserve the file mode from the source. + + # + # ==== Examples + # + # copy_file "README", "doc/README" + # + # copy_file "doc/README" + # + def copy_file(source, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + destination = args.first || source + source = File.expand_path(find_in_source_paths(source.to_s)) + + create_file destination, nil, config do + content = File.binread(source) + content = yield(content) if block + content + end + if config[:mode] == :preserve + mode = File.stat(source).mode + chmod(destination, mode, config) + end + end + + # Links the file from the relative source to the relative destination. If + # the destination is not given it's assumed to be equal to the source. + # + # ==== Parameters + # source<String>:: the relative path to the source root. + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # link_file "README", "doc/README" + # + # link_file "doc/README" + # + def link_file(source, *args) + config = args.last.is_a?(Hash) ? args.pop : {} + destination = args.first || source + source = File.expand_path(find_in_source_paths(source.to_s)) + + create_link destination, source, config + end + + # Gets the content at the given address and places it at the given relative + # destination. If a block is given instead of destination, the content of + # the url is yielded and used as location. + # + # ==== Parameters + # source<String>:: the address of the given content. + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # get "http://gist.github.com/103208", "doc/README" + # + # get "http://gist.github.com/103208" do |content| + # content.split("\n").first + # end + # + def get(source, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + destination = args.first + + if source =~ %r{^https?\://} + require "open-uri" + else + source = File.expand_path(find_in_source_paths(source.to_s)) + end + + render = open(source) { |input| input.binmode.read } + + destination ||= if block_given? + block.arity == 1 ? yield(render) : yield + else + File.basename(source) + end + + create_file destination, render, config + end + + # Gets an ERB template at the relative source, executes it and makes a copy + # at the relative destination. If the destination is not given it's assumed + # to be equal to the source removing .tt from the filename. + # + # ==== Parameters + # source<String>:: the relative path to the source root. + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # template "README", "doc/README" + # + # template "doc/README" + # + def template(source, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + destination = args.first || source.sub(/#{TEMPLATE_EXTNAME}$/, "") + + source = File.expand_path(find_in_source_paths(source.to_s)) + context = config.delete(:context) || instance_eval("binding") + + create_file destination, nil, config do + content = CapturableERB.new(::File.binread(source), nil, "-", "@output_buffer").tap do |erb| + erb.filename = source + end.result(context) + content = yield(content) if block + content + end + end + + # Changes the mode of the given file or directory. + # + # ==== Parameters + # mode<Integer>:: the file mode + # path<String>:: the name of the file to change mode + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # chmod "script/server", 0755 + # + def chmod(path, mode, config = {}) + return unless behavior == :invoke + path = File.expand_path(path, destination_root) + say_status :chmod, relative_to_original_destination_root(path), config.fetch(:verbose, true) + unless options[:pretend] + require "fileutils" + FileUtils.chmod_R(mode, path) + end + end + + # Prepend text to a file. Since it depends on insert_into_file, it's reversible. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # data<String>:: the data to prepend to the file, can be also given as a block. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # prepend_to_file 'config/environments/test.rb', 'config.gem "rspec"' + # + # prepend_to_file 'config/environments/test.rb' do + # 'config.gem "rspec"' + # end + # + def prepend_to_file(path, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + config[:after] = /\A/ + insert_into_file(path, *(args << config), &block) + end + alias_method :prepend_file, :prepend_to_file + + # Append text to a file. Since it depends on insert_into_file, it's reversible. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # data<String>:: the data to append to the file, can be also given as a block. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # append_to_file 'config/environments/test.rb', 'config.gem "rspec"' + # + # append_to_file 'config/environments/test.rb' do + # 'config.gem "rspec"' + # end + # + def append_to_file(path, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + config[:before] = /\z/ + insert_into_file(path, *(args << config), &block) + end + alias_method :append_file, :append_to_file + + # Injects text right after the class definition. Since it depends on + # insert_into_file, it's reversible. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # klass<String|Class>:: the class to be manipulated + # data<String>:: the data to append to the class, can be also given as a block. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # inject_into_class "app/controllers/application_controller.rb", ApplicationController, " filter_parameter :password\n" + # + # inject_into_class "app/controllers/application_controller.rb", ApplicationController do + # " filter_parameter :password\n" + # end + # + def inject_into_class(path, klass, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + config[:after] = /class #{klass}\n|class #{klass} .*\n/ + insert_into_file(path, *(args << config), &block) + end + + # Injects text right after the module definition. Since it depends on + # insert_into_file, it's reversible. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # module_name<String|Class>:: the module to be manipulated + # data<String>:: the data to append to the class, can be also given as a block. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # inject_into_module "app/helpers/application_helper.rb", ApplicationHelper, " def help; 'help'; end\n" + # + # inject_into_module "app/helpers/application_helper.rb", ApplicationHelper do + # " def help; 'help'; end\n" + # end + # + def inject_into_module(path, module_name, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + config[:after] = /module #{module_name}\n|module #{module_name} .*\n/ + insert_into_file(path, *(args << config), &block) + end + + # Run a regular expression replacement on a file. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # flag<Regexp|String>:: the regexp or string to be replaced + # replacement<String>:: the replacement, can be also given as a block + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # gsub_file 'app/controllers/application_controller.rb', /#\s*(filter_parameter_logging :password)/, '\1' + # + # gsub_file 'README', /rake/, :green do |match| + # match << " no more. Use thor!" + # end + # + def gsub_file(path, flag, *args, &block) + return unless behavior == :invoke + config = args.last.is_a?(Hash) ? args.pop : {} + + path = File.expand_path(path, destination_root) + say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true) + + unless options[:pretend] + content = File.binread(path) + content.gsub!(flag, *args, &block) + File.open(path, "wb") { |file| file.write(content) } + end + end + + # Uncomment all lines matching a given regex. It will leave the space + # which existed before the comment hash in tact but will remove any spacing + # between the comment hash and the beginning of the line. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # flag<Regexp|String>:: the regexp or string used to decide which lines to uncomment + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # uncomment_lines 'config/initializers/session_store.rb', /active_record/ + # + def uncomment_lines(path, flag, *args) + flag = flag.respond_to?(:source) ? flag.source : flag + + gsub_file(path, /^(\s*)#[[:blank:]]*(.*#{flag})/, '\1\2', *args) + end + + # Comment all lines matching a given regex. It will leave the space + # which existed before the beginning of the line in tact and will insert + # a single space after the comment hash. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # flag<Regexp|String>:: the regexp or string used to decide which lines to comment + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # comment_lines 'config/initializers/session_store.rb', /cookie_store/ + # + def comment_lines(path, flag, *args) + flag = flag.respond_to?(:source) ? flag.source : flag + + gsub_file(path, /^(\s*)([^#|\n]*#{flag})/, '\1# \2', *args) + end + + # Removes a file at the given location. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # remove_file 'README' + # remove_file 'app/controllers/application_controller.rb' + # + def remove_file(path, config = {}) + return unless behavior == :invoke + path = File.expand_path(path, destination_root) + + say_status :remove, relative_to_original_destination_root(path), config.fetch(:verbose, true) + if !options[:pretend] && File.exist?(path) + require "fileutils" + ::FileUtils.rm_rf(path) + end + end + alias_method :remove_dir, :remove_file + + attr_accessor :output_buffer + private :output_buffer, :output_buffer= + + private + + def concat(string) + @output_buffer.concat(string) + end + + def capture(*args) + with_output_buffer { yield(*args) } + end + + def with_output_buffer(buf = "".dup) #:nodoc: + raise ArgumentError, "Buffer can not be a frozen object" if buf.frozen? + old_buffer = output_buffer + self.output_buffer = buf + yield + output_buffer + ensure + self.output_buffer = old_buffer + end + + # Bundler::Thor::Actions#capture depends on what kind of buffer is used in ERB. + # Thus CapturableERB fixes ERB to use String buffer. + class CapturableERB < ERB + def set_eoutvar(compiler, eoutvar = "_erbout") + compiler.put_cmd = "#{eoutvar}.concat" + compiler.insert_cmd = "#{eoutvar}.concat" + compiler.pre_cmd = ["#{eoutvar} = ''.dup"] + compiler.post_cmd = [eoutvar] + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb b/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb new file mode 100644 index 0000000000..349b26ff65 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb @@ -0,0 +1,109 @@ +require "bundler/vendor/thor/lib/thor/actions/empty_directory" + +class Bundler::Thor + module Actions + # Injects the given content into a file. Different from gsub_file, this + # method is reversible. + # + # ==== Parameters + # destination<String>:: Relative path to the destination root + # data<String>:: Data to add to the file. Can be given as a block. + # config<Hash>:: 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 + action InjectIntoFile.new(self, destination, data, config) + end + alias_method :inject_into_file, :insert_into_file + + class InjectIntoFile < EmptyDirectory #:nodoc: + attr_reader :replacement, :flag, :behavior + + def initialize(base, destination, data, config) + super(base, destination, {:verbose => true}.merge(config)) + + @behavior, @flag = if @config.key?(:after) + [:after, @config.delete(:after)] + else + [:before, @config.delete(:before)] + end + + @replacement = data.is_a?(Proc) ? data.call : data + @flag = Regexp.escape(@flag) unless @flag.is_a?(Regexp) + end + + def invoke! + say_status :invoke + + content = if @behavior == :after + '\0' + replacement + else + replacement + '\0' + end + + if exists? + replace!(/#{flag}/, content, config[:force]) + else + unless pretend? + raise Bundler::Thor::Error, "The file #{ destination } does not appear to exist" + end + end + end + + def revoke! + say_status :revoke + + regexp = if @behavior == :after + content = '\1\2' + /(#{flag})(.*)(#{Regexp.escape(replacement)})/m + else + content = '\2\3' + /(#{Regexp.escape(replacement)})(.*)(#{flag})/m + end + + replace!(regexp, content, true) + end + + protected + + def say_status(behavior) + status = if behavior == :invoke + if flag == /\A/ + :prepend + elsif flag == /\z/ + :append + else + :insert + end + else + :subtract + end + + super(status, config[:verbose]) + end + + # Adds the content to the file. + # + def replace!(regexp, string, force) + return if pretend? + content = File.read(destination) + if force || !content.include?(replacement) + content.gsub!(regexp, string) + File.open(destination, "wb") { |file| file.write(content) } + end + end + end + end +end |