diff options
author | Lamont Granquist <lamont@scriptkiddie.org> | 2018-10-31 10:01:36 -0700 |
---|---|---|
committer | Lamont Granquist <lamont@scriptkiddie.org> | 2019-06-26 12:12:32 -0700 |
commit | 77ba7c39033513cf976825966eb9787597c9dfed (patch) | |
tree | 48f49921f9cb2d5be72a70d84eacd82c9d9bbe0d | |
parent | cb98174f352f145ec8d7cc379b018591b296f66a (diff) | |
download | chef-lcg/file-edit.tar.gz |
a new startlcg/file-edit
Signed-off-by: Lamont Granquist <lamont@scriptkiddie.org>
-rw-r--r-- | lib/chef/provider/file.rb | 3 | ||||
-rw-r--r-- | lib/chef/provider/file/edit_dsl.rb | 311 | ||||
-rw-r--r-- | lib/chef/provider/file/editable_file.rb | 223 |
3 files changed, 15 insertions, 522 deletions
diff --git a/lib/chef/provider/file.rb b/lib/chef/provider/file.rb index 36225fb8a8..3e2f97b9a3 100644 --- a/lib/chef/provider/file.rb +++ b/lib/chef/provider/file.rb @@ -148,6 +148,7 @@ class Chef do_acl_changes do_selinux load_resource_attributes_from_file(new_resource) unless Chef::Config[:why_run] + # FIXME: we may need to add a callback for remote_file to save the file-edited checksum for network-level idempotency end def action_create_if_missing @@ -336,7 +337,7 @@ class Chef def do_file_editing if new_resource.edit && tempfile - editor = new_resource.file_editor_class.from_file(tempfile.path) + editor = new_resource.editor_class.from_file(tempfile.path) editor.instance_exec(&new_resource.edit) editor.finish! end diff --git a/lib/chef/provider/file/edit_dsl.rb b/lib/chef/provider/file/edit_dsl.rb index 14461d66a7..de86a277da 100644 --- a/lib/chef/provider/file/edit_dsl.rb +++ b/lib/chef/provider/file/edit_dsl.rb @@ -22,13 +22,13 @@ class Chef class Provider class File < Chef::Provider class EditDSL + extend Forwardable + # Array<String> lines - attr_accessor :file_contents - attr_accessor :path + attr_accessor :editor def initialize(path) - @path = path - @file_contents = ::File.readlines(path) + editor = EditableFile.new(path) end # @@ -75,308 +75,11 @@ class Chef # ADD: # # - remove_if_empty (true/false) : remove the file if the contents are all deleted (default false) - # - # WARNING: Chef Software Inc owns all methods in this namespace, you MUST NOT monkeypatch or inject - # methods directly into this class. You may create your own module of helper functions and `extend` - # those directly into the blocks where you use the helpers. - # - # in e.g. libraries/my_helper.rb: - # - # module AcmeMyHelpers - # def acme_do_a_thing ... end - # end - # - # in e.g. recipes/default.rb: - # - # file "/tmp/foo.xyz" do - # edit do - # extend AcmeMyHelpers - # acme_do_a_thing - # [...] - # end - # end - # - # It is still recommended that you namespace your custom helpers so as not to have collisions with future - # methods added to this class. - # - # FIXME: it feels like we should add DSL sugar for this? - # - - def empty! - @file_contents = [] - end - - # set the eol string for the file - def eol(val) - # FIXME: yeah, windows stuff is all totes broken right now - end - - # repetetive "append_if_no_such_line" - # - # Examples: - # - # append_lines({ - # /NETWORLKING.*=/ => "NETWORKING=yes" - # /HOSTNAME.*=/ => "HOSTNAME=foo.acme.com" - # }, replace: true, unique: true) - # - # @param lines [ String, Array<String>, Hash{Regexp,String => String} ] lines to append - # @param replace [ true, false ] If set to true, all existing lines will be replaced - # @param unique [ true, false ] If unique is false, all lines are replaced. If unique is true only the - # last match is replaced and the other matches are deleted from the file. - # @return [ Array<String> ] the file_contets array, or nil if there was no modifications - # - def append_lines(lines, replace: false, unique: false) - chkarg 1, lines, [ Array, String, Hash ] - chkarg :replace, replace, [ true, false ] - chkarg :unique, unique, [ true, false ] - - unless lines.is_a?(Hash) - lines = lines.split("\n") unless lines.is_a?(Array) - lines.map(&:chomp!) - lines = lines.each_with_object({}) do |line, hash| - regex = /^#{Regexp.escape(line)}$/ - hash[regex] = line - end - end - modified = false - lines.each do |regex, line| - append_line_unless_match(regex, line, replace: replace, unique: unique) && modified = true - end - modified ? file_contents : nil - end - - # lower level one-line-at-a-time - # @return [ Array<String> ] the file_contets array, or nil if there was no modifications - # - def append_line_unless_match(pattern, line, replace: false, unique: false) - chkarg 1, pattern, [ String, Regexp ] - chkarg 2, line, String - chkarg :replace, replace, [ true, false ] - chkarg :unique, unique, [ true, false ] - - modified = false - regex = pattern.is_a?(String) ? /#{Regexp.escape(pattern)}/ : pattern - if file_contents.grep(regex).empty? - unless file_contents.empty? - file_contents[-1].chomp! - file_contents[-1] << "\n" - end - file_contents.push(line + "\n") - modified = true - else - if replace - replace_lines(regex, line, unique: unique ? :last : false) && modified = true - end - end - modified ? file_contents : nil - end - - # repetitive "prepend_if_no_such_line" - # @return [ Array<String> ] the file_contets array, or nil if there was no modifications - # - def prepend_lines(lines, replace: false, unique: false) - chkarg 1, lines, [ Array, String, Hash ] - chkarg :replace, replace, [ true, false ] - chkarg :unique, unique, [ true, false ] - - unless lines.is_a?(Hash) - lines = lines.split("\n") unless lines.is_a?(Array) - lines.map(&:chomp!) - lines = lines.reverse.each_with_object({}) do |line, hash| - regex = /^#{Regexp.escape(line)}$/ - hash[regex] = line - end - end - modified = false - lines.each do |regex, line| - prepend_line_unless_match(regex, line, replace: replace, unique: unique) && modified = true - end - end - - # @return [ Array<String> ] the file_contets array, or nil if there was no modifications - # - def prepend_line_unless_match(pattern, line, replace: false, unique: false) - chkarg 1, pattern, [ String, Regexp ] - chkarg 2, line, String - chkarg :replace, replace, [ true, false ] - chkarg :unique, unique, [ true, false ] - - modified = false - regex = pattern.is_a?(String) ? /#{Regexp.escape(pattern)}/ : pattern - if file_contents.grep(regex).empty? - file_contents.unshift(line + "\n") - modified = true - else - if replace - replace_lines(regex, line, unique: unique ? :first : false) && modified = true - end - end - modified ? file_contents : nil - end - - # mass delete - def delete_lines_matching(pattern) - regex = pattern.is_a?(String) ? /#{Regexp.escape(pattern)}/ : pattern - file_contents.reject! { |l| l =~ regex } - end - - # delimited delete - def delete_between(start:, finish:, inclusive: false) - start = start.is_a?(String) ? /#{Regexp.escape(start)}/ : start - finish = finish.is_a?(String) ? /#{Regexp.escape(finish)}/ : finish - # find the start - i_start = file_contents.find_index { |l| l =~ start } - return unless i_start - # find the finish - i_finish = nil - i_start.upto(file_contents.size - 1) do |i| - if i >= i_start && file_contents[i] =~ finish - i_finish = i - break - end - end - return unless i_finish - file_contents.slice!(i_start, i_finish) - end - - def replace_between(lines, start:, finish:) - end - - # Search the entire file for matches on each line. When the line matches, replace the line with the given string. Can also be - # used to assert that only one occurance of the match is kept in the file. - # - # @param match [ String, Regexp ] The regular expression or substring to match - # @param line [ String ] The line to replace matching lines with - # @param unique [ false, :first, :last ] If unique is false, all lines are replaced. If unique is set to :first or :last only the - # first or last match is replaced and the other matches are deleted from the file. - # @return [ Array<String> ] the file_contets array, or nil if there was no modifications - # - def replace_lines(match, line, unique: false) - chkarg 1, match, [ String, Regexp ] - chkarg 2, line, String - chkarg :unique, unique, [ false, :first, :last ] - regex = match.is_a?(String) ? /#{Regexp.escape(match)}/ : match - modified = false - file_contents.reverse! if unique == :last # FIXME: this is probably expensive - found = false - file_contents.map! do |l| - ret = if l != line + "\n" && regex.match?(l) - modified = true - if !(unique && found) - line + "\n" - else - nil - end - else - l - end - found = true if regex.match?(l) - ret - end.compact! - file_contents.reverse! if unique == :last - modified ? file_contents : nil - end - - # mass search-and-replace on substrings - def substitute_lines(match, replace, global: false) - chkarg 1, match, [ String, Regexp ] - chkarg 2, replace, String - chkarg :global, global, [ false, true ] - - regex = match.is_a?(String) ? /#{Regexp.escape(match)}/ : match - modified = false - if global - file_contents.each do |l| - old = l - l.gsub!(regex, replace) - modified = true if l != old - end - else - file_contents.each do |l| - old = l - l.sub!(regex, replace) - modified = true if l != old - end - end - modified ? file_contents : nil - end - - # delimited search-and-replace - def substitute_between(start:, finish:, match:, replace:, global: false, inclusive: false) - start = start.is_a?(String) ? /#{Regexp.escape(start)}/ : start - finish = finish.is_a?(String) ? /#{Regexp.escape(finish)}/ : finish - match = match.is_a?(String) ? /#{Regexp.escape(match)}/ : match - # find the start - i_start = file_contents.find_index { |l| l =~ start } - return unless i_start - # find the finish - i_finish = nil - i_start.upto(file_contents.size - 1) do |i| - if i >= i_start && file_contents[i] =~ finish - i_finish = i - break - end - end - return unless i_finish - # do the substitution on the block - if global - i_start.upto(i_finish) { |i| file_contents[i].gsub!(match, replace) } - else - i_start.upto(i_finish) { |i| file_contents[i].sub!(match, replace) } - end - end - - # NOTE: This is intented to be used only on a tempfile so we open, truncate and append - # because the file provider already has the machinery to atomically move a tempfile into place. - # If we crash in the middle it doesn't matter if we leave a corrupted tempfile to be - # garbage collected as ruby exits. If you feel you need to add atomicity here you probably - # want to use a file provider directly or fix your own code to provide a tempfile to this - # one and handle the atomicity yourself. - # - # This is not intended as a DSL method for end users, it has to be public visibility, but you - # should not use it. - # - # @api private - def finish! - ::File.open(path, "w") do |f| - f.write file_contents.join - end - end - - private - - # FIXME: make this a mixin in chef-helper - # @api private - def chkarg(what, arg, matches) - matches = Array( matches ) - matches.each do |match| - return true if match === arg - end - method = caller_locations(1, 1)[0].label - whatstr = if what.is_a?(Integer) - "#{ordinalize(what)} argument" - else - "named '#{what}' argument" - end - raise ArgumentError, "#{whatstr} to #{method} must be one of: #{matches.map { |v| v.inspect }.join(", ")}, you gave: #{arg.inspect}" - end + def_delegators :@editor, :insert, :replace, :delete, :location, :region, :using - # FIXME: make this a mixin in chef-helper (and yeah we could humanize it and spell it out, but ain't got time for that) - # @api private - def ordinalize(int) - s = int.to_s - case - when s.end_with?("1") - "#{int}st" - when s.end_with?("2") - "#{int}nd" - when s.end_with?("3") - "#{int}rd" - else - "#{int}th" - end + def self.from_file(path) + EditableFile.from_file(path) end end end diff --git a/lib/chef/provider/file/editable_file.rb b/lib/chef/provider/file/editable_file.rb index e2474c7363..a4ac6b4d18 100644 --- a/lib/chef/provider/file/editable_file.rb +++ b/lib/chef/provider/file/editable_file.rb @@ -123,6 +123,7 @@ class Chef # FIXME: @param preserve_block [ Boolean ] if `what` is multi-line treat it as a block of lines, not individual lines # FIXME: insert_select support for files? def insert(lines, location:, ignore_leading: false, ignore_trailing: false, ignore_embedded: false, idempotency: true) # , preserve_block: false) + raise "no such location" unless locations.key?(location) # FIXME: better errors lines = lines.read if lines.is_a?(IO) lines = lines.lines if lines.is_a?(String) lines = Array( lines ) @@ -176,6 +177,11 @@ class Chef end end + def use(klass, method) + p = klass.instance_method(method) + instance_eval(&p) + end + private def generate_regexp(string, ignore_leading: false, ignore_trailing: false, ignore_embedded: false) @@ -194,223 +200,6 @@ class Chef Regexp.new(regexp_str) end - # repetetive "append_if_no_such_line" - # - # Examples: - # - # append_lines({ - # /NETWORLKING.*=/ => "NETWORKING=yes" - # /HOSTNAME.*=/ => "HOSTNAME=foo.acme.com" - # }, replace: true, unique: true) - # - # @param lines [ String, Array<String>, Hash{Regexp,String => String} ] lines to append - # @param replace [ true, false ] If set to true, all existing lines will be replaced - # @param unique [ true, false ] If unique is false, all lines are replaced. If unique is true only the - # last match is replaced and the other matches are deleted from the file. - # @return [ Array<String> ] the file_contets array, or nil if there was no modifications - # - def append_lines(lines, replace: false, unique: false) - chkarg 1, lines, [ Array, String, Hash ] - chkarg :replace, replace, [ true, false ] - chkarg :unique, unique, [ true, false ] - - unless lines.is_a?(Hash) - lines = lines.split("\n") unless lines.is_a?(Array) - lines.map(&:chomp!) - lines = lines.each_with_object({}) do |line, hash| - regex = /^#{Regexp.escape(line)}$/ - hash[regex] = line - end - end - modified = false - lines.each do |regex, line| - append_line_unless_match(regex, line, replace: replace, unique: unique) && modified = true - end - modified ? file_contents : nil - end - - # lower level one-line-at-a-time - # @return [ Array<String> ] the file_contets array, or nil if there was no modifications - # - def append_line_unless_match(pattern, line, replace: false, unique: false) - chkarg 1, pattern, [ String, Regexp ] - chkarg 2, line, String - chkarg :replace, replace, [ true, false ] - chkarg :unique, unique, [ true, false ] - - modified = false - regex = pattern.is_a?(String) ? /#{Regexp.escape(pattern)}/ : pattern - if file_contents.grep(regex).empty? - unless file_contents.empty? - file_contents[-1].chomp! - file_contents[-1] << "\n" - end - file_contents.push(line + "\n") - modified = true - else - if replace - replace_lines(regex, line, unique: unique ? :last : false) && modified = true - end - end - modified ? file_contents : nil - end - - # repetitive "prepend_if_no_such_line" - # @return [ Array<String> ] the file_contets array, or nil if there was no modifications - # - def prepend_lines(lines, replace: false, unique: false) - chkarg 1, lines, [ Array, String, Hash ] - chkarg :replace, replace, [ true, false ] - chkarg :unique, unique, [ true, false ] - - unless lines.is_a?(Hash) - lines = lines.split("\n") unless lines.is_a?(Array) - lines.map(&:chomp!) - lines = lines.reverse.each_with_object({}) do |line, hash| - regex = /^#{Regexp.escape(line)}$/ - hash[regex] = line - end - end - modified = false - lines.each do |regex, line| - prepend_line_unless_match(regex, line, replace: replace, unique: unique) && modified = true - end - end - - # @return [ Array<String> ] the file_contets array, or nil if there was no modifications - # - def prepend_line_unless_match(pattern, line, replace: false, unique: false) - chkarg 1, pattern, [ String, Regexp ] - chkarg 2, line, String - chkarg :replace, replace, [ true, false ] - chkarg :unique, unique, [ true, false ] - - modified = false - regex = pattern.is_a?(String) ? /#{Regexp.escape(pattern)}/ : pattern - if file_contents.grep(regex).empty? - file_contents.unshift(line + "\n") - modified = true - else - if replace - replace_lines(regex, line, unique: unique ? :first : false) && modified = true - end - end - modified ? file_contents : nil - end - - # mass delete - def delete_lines_matching(pattern) - regex = pattern.is_a?(String) ? /#{Regexp.escape(pattern)}/ : pattern - file_contents.reject! { |l| l =~ regex } - end - - # delimited delete - def delete_between(start:, finish:, inclusive: false) - start = start.is_a?(String) ? /#{Regexp.escape(start)}/ : start - finish = finish.is_a?(String) ? /#{Regexp.escape(finish)}/ : finish - # find the start - i_start = file_contents.find_index { |l| l =~ start } - return unless i_start - # find the finish - i_finish = nil - i_start.upto(file_contents.size - 1) do |i| - if i >= i_start && file_contents[i] =~ finish - i_finish = i - break - end - end - return unless i_finish - file_contents.slice!(i_start, i_finish) - end - - def replace_between(lines, start:, finish:) - end - - # Search the entire file for matches on each line. When the line matches, replace the line with the given string. Can also be - # used to assert that only one occurance of the match is kept in the file. - # - # @param match [ String, Regexp ] The regular expression or substring to match - # @param line [ String ] The line to replace matching lines with - # @param unique [ false, :first, :last ] If unique is false, all lines are replaced. If unique is set to :first or :last only the - # first or last match is replaced and the other matches are deleted from the file. - # @return [ Array<String> ] the file_contets array, or nil if there was no modifications - # - def replace_lines(match, line, unique: false) - chkarg 1, match, [ String, Regexp ] - chkarg 2, line, String - chkarg :unique, unique, [ false, :first, :last ] - - regex = match.is_a?(String) ? /#{Regexp.escape(match)}/ : match - modified = false - file_contents.reverse! if unique == :last # FIXME: this is probably expensive - found = false - file_contents.map! do |l| - ret = if l != line + "\n" && regex.match?(l) - modified = true - if !(unique && found) - line + "\n" - else - nil - end - else - l - end - found = true if regex.match?(l) - ret - end.compact! - file_contents.reverse! if unique == :last - modified ? file_contents : nil - end - - # mass search-and-replace on substrings - def substitute_lines(match, replace, global: false) - chkarg 1, match, [ String, Regexp ] - chkarg 2, replace, String - chkarg :global, global, [ false, true ] - - regex = match.is_a?(String) ? /#{Regexp.escape(match)}/ : match - modified = false - if global - file_contents.each do |l| - old = l - l.gsub!(regex, replace) - modified = true if l != old - end - else - file_contents.each do |l| - old = l - l.sub!(regex, replace) - modified = true if l != old - end - end - modified ? file_contents : nil - end - - # delimited search-and-replace - def substitute_between(start:, finish:, match:, replace:, global: false, inclusive: false) - start = start.is_a?(String) ? /#{Regexp.escape(start)}/ : start - finish = finish.is_a?(String) ? /#{Regexp.escape(finish)}/ : finish - match = match.is_a?(String) ? /#{Regexp.escape(match)}/ : match - # find the start - i_start = file_contents.find_index { |l| l =~ start } - return unless i_start - # find the finish - i_finish = nil - i_start.upto(file_contents.size - 1) do |i| - if i >= i_start && file_contents[i] =~ finish - i_finish = i - break - end - end - return unless i_finish - # do the substitution on the block - if global - i_start.upto(i_finish) { |i| file_contents[i].gsub!(match, replace) } - else - i_start.upto(i_finish) { |i| file_contents[i].sub!(match, replace) } - end - end - end end end |