summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLamont Granquist <lamont@scriptkiddie.org>2018-10-31 10:01:36 -0700
committerLamont Granquist <lamont@scriptkiddie.org>2019-06-26 12:12:32 -0700
commit77ba7c39033513cf976825966eb9787597c9dfed (patch)
tree48f49921f9cb2d5be72a70d84eacd82c9d9bbe0d
parentcb98174f352f145ec8d7cc379b018591b296f66a (diff)
downloadchef-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.rb3
-rw-r--r--lib/chef/provider/file/edit_dsl.rb311
-rw-r--r--lib/chef/provider/file/editable_file.rb223
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