foo
foo

Reputation: 195

how to overwrite part of a line in a txt file with regex and .sub in ruby

I have the following layout in a txt file.

[item] label1: comment1 | label2: foo

I have the code below. The goal is to modify part of an existing line in text

def replace_info(item, bar)
return "please create a file first" unless File.exist?('site_info.txt')
    IO.foreach('site_info.txt','a+') do |line|
        if line.include?(item)
            #regex should find the data from the whitespace after the colon all the                     way to the end.
            #this should be equivalent to foo
            foo_string = line.scan(/[^"label2: "]*\z/)
            line.sub(foo_string, bar)
            end
        end
end

Please advise. Perhaps my regrex is off, but .sub is correct, but I cannot overwrite line.

Upvotes: 1

Views: 86

Answers (1)

Amadan
Amadan

Reputation: 198314

Tiny problem: Your regular expression does not do what you think. /[^"label2: "]*\z/ means: any number of characters at the end of line that are not a, b, e, l, ", space, colon or 2 (see Character classes). And scan returns an array, which sub doesn't work with. But that doesn't really matter, because...

Small problem: line.sub(foo_string, bar) doesn't do anything. It returns a changed string, but you don't assign it to anything and it gets thrown away. line.sub!(foo_string, bar) would change line itself, but that leads us to...

Big problem: You cannot just change the read line and expect it to change in the file itself. It's like reading a book, thinking you could write a line better, and expecting it to change the book. The way to change a line in a text file is to read from one file and copy what you read to another. If you change a line between reading and writing, the newly written copy will be different. At the end, you can rename the new file to the old file (which will delete the old file and replace it atomically with the new one).

EDIT: Here's some code. First, I dislike IO.foreach as I like to control the iteration myself (and IMO, IO.foreach is not readable as IO#each_line). In the regular expression, I used lookbehind to find the label without including it into the match, so I can replace just the value; I changed to \Z for a similar reason, to exclude the newline from the match. You should not be returning error messages from functions, that's what exceptions are for. I changed simple include? to #start_with? because your item might be found elsewhere in the line when we wouldn't want to trigger the change.

class FileNotFoundException < RuntimeError; end

def replace_info(item, bar)
  # check if file exists
  raise FileNotFoundException unless File.exist?('site_info.txt')

  # rewrite the file
  File.open('site_info.txt.bak', 'wt') do |w|
    File.open('site_info.txt', 'rt') do |r|
      r.each_line do |line|
        if line.start_with?("[#{item}]")
          line.sub!(/(?<=label2: ).*?\Z/, bar)
        end
        w.write(line)
      end
    end
  end

  # replace the old file
  File.rename('site_info.txt.bak', 'site_info.txt')
end

replace_info("item", "bar")

Upvotes: 3

Related Questions