Module: PuppetX::FileMagic

Defined in:
lib/puppet_x/filemagic.rb

Class Method Summary collapse

Class Method Details

.append(path, regex_start, flags, data, delete) ⇒ Object



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/puppet_x/filemagic.rb', line 285

def self.append(path, regex_start, flags, data, delete)
  # write the new content in one go
  content = readfile(path)
  found_at = get_match_regex(content, regex_start, flags,true)
  data_lines = data2lines(data)

  if found_at > -1
    # Discard from the end of the file all lines including and after content[found_at]
    content = content[0..found_at-1]
  end

  if delete
    data_lines.reverse.each { |line|
      if content[-1].strip == line.strip
        content.pop
      end
    }
  else
    content += data_lines
  end

  writefile(path, content)
end

.data2lines(data) ⇒ Object

split data (string, eg: foonbarbaz..) into an array of lines



13
14
15
16
17
18
19
20
21
22
# File 'lib/puppet_x/filemagic.rb', line 13

def self.data2lines(data)
  if data.nil? || data.empty?
    lines = []
  else
    # support windows by chomping (CR)LF for comparison purposes
    lines = data.split(/\r?\n/)
  end

  lines
end

.determine_line_ending(path) ⇒ Object

read the first line of file to determine it's line endings



6
7
8
9
10
# File 'lib/puppet_x/filemagic.rb', line 6

def self.determine_line_ending(path)
  File.open(path, 'r') do |file|
    return file.readline[/\r?\n$/]
  end
end

.exists?(path, data, regex, flags, check_type, check_for_absent) ⇒ Boolean

sandwich position not implemented yet -1 TOP OF file 0 SANDWICH +1 END OF FILE

Parameters:

  • path

    File to inspect

  • data

    Lines of data to look for. If (CR)LF character found, newlines will be inserted or you can just pass an array

  • regex

    Regular expression to match or false to skip

  • position

    Where to look for a match - -1 top of file, 0 sandwich, +1 bottom of file

  • check_type

    what kind of check are we doing? :present, :absent

Returns:

  • (Boolean)


182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/puppet_x/filemagic.rb', line 182

def self.exists?(path, data, regex, flags, check_type, check_for_absent)
  data_lines = data2lines(data)
  if ! check_for_absent && data_lines.size == 0
    raise "you must supply a valid string of `data` to check for"
  end

  if ! File.exist?(path)
    Puppet.err("File missing at #{path} (file must already exist to do filemagic on it...)")

    # cannot possibly exist if it doesn't exist
    exists = false
  else
    file_lines = readfile(path)
    case check_type
    when :append, :prepend
      # Check for an exact match on `data` at the beginning or end of file
      pos = (check_type == :prepend) ? -1 : 1
      lines_matched, file_lines_less_exact_match = get_match_lines(file_lines, data_lines, pos)
      all_lines_matched = (lines_matched == -1)

      if all_lines_matched
        exists = true
      else
        # if all lines didn't match there might me a regex we need to scan for
        if regex
          partial_match = (get_match_regex(file_lines_less_exact_match, regex, flags, true) > -1)
        end

        # Nothing found by regex.. last check - do we have a partial match on any data?
        if ! partial_match
          partial_match = (get_match_lines(file_lines_less_exact_match, data_lines, pos)[0] > 0)
        end

        exists = check_for_absent && partial_match
      end

    when :gsub
      exists =
          if (get_match_regex(file_lines, regex, flags, true) > -1)
            check_for_absent
          else
            ! check_for_absent
          end
    when :replace, :replace_insert
      # check for a 100% match on `data_lines` in `files_lines`, anywhere in the file
      matched_lines, file_lines_less_exact_match = get_match_lines(file_lines, data_lines, 0)
      all_lines_matched = (matched_lines == -1)

      # Also do a regex search on the file less any exact match, that we me check for any unwanted instances of
      # `match` that still exist in the file ("stragglers")
      match_count = get_match_regex(file_lines_less_exact_match, regex, flags, true)
      if all_lines_matched
        if match_count > -1
          # we have a straggler - force the correct update for ensure present/absent
          exists = check_for_absent
        else
          exists = all_lines_matched
        end
      elsif check_type == :replace_insert
        # `data_lines` were not matched but they are supposed to exist
        exists = false
      else
        # matches were found in check_for_absent mode means we need to remove them, otherwise we exist (aka dont
        # need processing) if there are _no_ matches
        exists = check_for_absent ^ (match_count == -1)
      end
    else
      raise "Unsupported check type #{check_type}"
    end
  end

  exists
end

.get_match_lines(file_lines, data_lines, pos) ⇒ Object

Return a count of matching lines from the file and an array of lines representing the contents of the file less any matches. the pos parameter can be used to anchor the search to the beginning or end of the file if desired.

Parameters:

  • pos

    -1 match from beginning, 0 match from anywhere, 1 match fom end

Returns:

  • tuple, element 0: -1 if all lines matched in correct order otherwise count of matching lines element 1: the file lines less the exact match



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/puppet_x/filemagic.rb', line 103

def self.get_match_lines(file_lines, data_lines, pos)
  exact_lines_matched = 0
  file_lines_less_exact_match = []

  if pos == 1
    # reverse match order if we are checking the end of the file
    data_lines_ordered = data_lines.reverse if data_lines
    file_lines_ordered = file_lines.reverse if file_lines
  else
    file_lines_ordered = file_lines
    data_lines_ordered = data_lines
  end

  # we need different indices for file_line vs data_line when we are searching with pos=0 since our data_lines are
  # allowed to exist _anywhere_ in the input data
  i = 0
  j = 0
  while file_lines_ordered.size && i < file_lines_ordered.size
    file_line = file_lines_ordered[i]
    data_line = data_lines_ordered[j]

    # check-off that each line in our data is already in the file, in the exact correct position
    line_matched = (file_line == data_line)

    #
    # indicies for next loop
    #
    i += 1
    if (pos != 0)
      j = [i, data_lines_ordered.size].min
    elsif line_matched
      # we have matched data_lines inside file lines _and_ we are matching from anywhere so we have found the
      # start of the match, start incrementing `j` until we run out of lines / matches
      j = [j += 1, data_lines_ordered.size].min
    end

    # evaluate our current match, automatically taking account of position in file_lines due to our loop position
    if line_matched
      exact_lines_matched += 1
    else
      file_lines_less_exact_match.push(file_line)
    end

  end

  if (exact_lines_matched == data_lines_ordered.size) && (data_lines_ordered.size > 0)
    # -1 indicates all lines matched
    lines_matched = -1
  else
    partial_matches = 0
    file_lines.each { |file_line|
      data_lines.each { |data_line|
        if file_line == data_line
          partial_matches += 1
        end
      }
    }
    lines_matched = partial_matches
  end

  if pos == 1
    # If we matched from the end the matched lines will be reversed - fix this in-place
    file_lines_less_exact_match.reverse!
  end

  return lines_matched, file_lines_less_exact_match
end

.get_match_regex(file_lines, regex, flags, first) ⇒ Object

return the index of the first/last match of regex in lines or -1 if no match. Be careful with if statements using this! It's very possible that 0 will be returned to indicate a match in the first element of the array which is of course false

Parameters:

  • file_lines

    data to search

  • regex

    regex to match

  • flags

    flags to use with this regex

  • first

    match the first instance? otherwise match the last



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/puppet_x/filemagic.rb', line 55

def self.get_match_regex(file_lines, regex, flags, first)
  found_at = -1

  if regex
    # parse the string from puppet into a real ruby regex. Flags have to be separate for this
    _regex = Regexp.new(regex, flags)
    i = 0

    if first
      file_lines_ordered = file_lines
    else
      file_lines_ordered = file_lines.reverse
    end

    while found_at == -1 && i < file_lines_ordered.size
      file_line = file_lines_ordered[i]

      # only interested in the the first line of data
      if _regex.match?(file_line)
        found_at = i
      end
      i += 1
    end

    if !first && found_at > -1
      # need to correct the index since we reversed the array we searched
      #
      # `V` points to the element we want in the array of `X`s
      #
      #      V = 5
      # XXXXXXX size == 7
      #
      # want
      #
      #  V=1
      # XXXXXXX
      found_at = file_lines_ordered.size - found_at - 1
    end
  end

  found_at
end

.gsub_match(path, regex, flags, data) ⇒ Object



361
362
363
364
365
366
367
# File 'lib/puppet_x/filemagic.rb', line 361

def self.gsub_match(path, regex, flags, data)
  # Read the file and flatten it based on the FILE's encoding (not the os...)
  content = readfile(path).join(determine_line_ending(path))
  _regex = Regexp.new(regex, flags)
  content = content.gsub(_regex, data)
  writefile(path, content)
end

.prepend(path, regex_end, flags, data, delete) ⇒ Object



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/puppet_x/filemagic.rb', line 256

def self.prepend(path, regex_end, flags, data, delete)
  # read the old content into an array and prepend the required lines
  content = readfile(path)
  found_at = get_match_regex(content, regex_end, flags, false)
  data_lines = data2lines(data)

  if found_at > -1
    # Discard from the beginning of the file all lines before and including content[found_at]
    content = content[found_at+1..content.size-1]
  end

  if delete
    # If we deleted based on the match, then target is already gone, otherwise if we are removing
    # based on `data` remove any lines line
    data_lines.each { |line|
      if content[0] == line
        content.shift
      end
    }
  else
    # insert the lines at the start of the file
    content.unshift(data_lines)
  end

  # write the new content in one go
  writefile(path, content)
end

.readfile(path) ⇒ Object

read a text file and get rid of newlines



25
26
27
28
29
30
31
32
33
# File 'lib/puppet_x/filemagic.rb', line 25

def self.readfile(path)
  if File.exists?(path)
    file_lines = File.readlines(path).each {|l| l.chomp!}
  else
    Puppet.err("FileMagic unable to find file at #{path} - make sure it exists")
    file_lines = []
  end
  file_lines
end

.remove_match(path, regex, flags) ⇒ Object



310
311
312
313
314
315
316
# File 'lib/puppet_x/filemagic.rb', line 310

def self.remove_match(path, regex, flags)
  content = readfile(path).reject { |line|
    _regex = Regexp.new(regex, flags)
    _regex.match?(line)
  }
  writefile(path, content)
end

.replace_match(path, regex, flags, data, insert_if_missing, insert_at) ⇒ Object



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/puppet_x/filemagic.rb', line 318

def self.replace_match(path, regex, flags, data, insert_if_missing, insert_at)
  content = []
  inserted = false
  found = false
  _regex = Regexp.new(regex, flags)

  readfile(path).each { |line|

    if _regex.match?(line)
      if ! inserted
        content << data
        inserted = true
      end
    else
      if line == data
        found = true
      end
      content << line
    end
  }

  if insert_if_missing && (! (found || inserted))
    case insert_at
    when 'top'
      insertion_point = 0
    when 'bottom'
      insertion_point = content.size
    else
      line_no = Integer(insert_at)
      if line_no > content.size
        Puppet.warning(
            "#{path}: Cannot insert line #{line_no}, only #{content.size} lines in file")
        insertion_point = content.size
      else
        insertion_point = line_no
      end
    end
    content.insert(insertion_point, data)
  end

  writefile(path, content)
end

.writefile(path, content) ⇒ Object

(over)write the output file and print a message that we did so



36
37
38
39
40
41
42
43
44
45
# File 'lib/puppet_x/filemagic.rb', line 36

def self.writefile(path, content)
  if File.exists?(path)
    File.open(path, "w") do |f|
      f.puts(content)
    end
    Puppet.notice("FileMagic updated: #{path}")
  else
    Puppet.err("FileMagic unable to find file at #{path} - make sure it exists")
  end
end