ilovebigmacs
ilovebigmacs

Reputation: 993

Insert text before the end of a file

I am trying to write a script that will insert a text before the last end tag within a Ruby file. For example, I want to insert the following:

def hello
  puts "hello!"
end

within the following file, just before the end of the class:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  helper_method :authenticated?, :current_user

  def current_user?
    session[:current_user]   
  end

end

The result should look like this:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  helper_method :authenticated?, :current_user

  def current_user?
    session[:current_user]   
  end

  def hello
    puts "hello!"
  end

end

I have tried to find a regex that would match the last occurence of end and replace it with the block I want to add but all regexes I have tried match the first end only. Tried these:

To replace the string, I do the following (maybe the EOL characters are screwing up the matching?):

file_to_override = File.read("app/controllers/application_controller.rb")
file_to_override = file_to_override.sub(/end(?=[^end]*$)/, "#{new_string}\nend")

EDIT: I also tried with the solution provided in How to replace the last occurrence of a substring in ruby? but strangely, it replaces all occurences of end.

What am I doing wrong? Thanks!

Upvotes: 2

Views: 358

Answers (4)

Felix
Felix

Reputation: 4716

You can also find and replace the last occurence of "end" (note that this will also match the end in # Hello my friend, but see below) like this

# Our basics: In this text ...
original_content = "# myfile.rb\n"\
                   "module MyApp\n"\
                   "  class MyFile\n"\
                   "    def myfunc\n"\
                   "    end\n"\
                   "  end\n"\
                   "end\n"
# ...we want to inject this:
substitute = "# this will come to a final end!\n"\
             "end\n"

# Now find the last end ...
idx = original_content.rindex("end") # => index of last "end"(69)
# ... and substitute it
original_content[idx..idx+3] = substitute # (3 = "end".length)

This solution is somewhat more old-school (dealing with indexes in strings felt much cooler some years ago) and in this form more "vulnerable" but avoids you to sit down and digest the regexps. Dont get me wrong, regular expressions are a tool of incredible power and the minutes learning them are worth it.

That said, you can use all the regular expressions from the other answers also with rindex (e.g. rindex(/ *end/)).

Upvotes: 0

ilovebigmacs
ilovebigmacs

Reputation: 993

Edit: Answer from Wiktor is exactly what I was looking for. Leaving the following too because it works as well.

Finally, I gave up on replacing using a regex. Instead, I use the position of the last end:

positions = file_to_override.enum_for(:scan, /end/).map { Regexp.last_match.begin(0) }

Then, before writing the file, I add what I need within the string at last position - 1:

new_string = <<EOS
  def hello
    puts "Hello!"
  end
EOS

file_to_override[positions.last - 1] = "\n#{test_string}\n"
File.open("app/controllers/application_controller.rb", 'w') {|file| file.write(file_to_override)}

This works but it doesn't look like idiomatic Ruby to me.

Upvotes: 0

Cary Swoveland
Cary Swoveland

Reputation: 110685

Suppose you read the file into the string text:

text = <<_
class A
  def a
    'hi'
  end

end
_

and wish to insert the string to_enter:

to_enter = <<_
  def hello
    puts "hello!"
  end
_

before the last end. You could write

r = /
    .*             # match any number of any character (greedily)
    \K             # discard everything matched so far
    (?=\n\s*end\b) # match end-of-line, indenting spaces, and "end" followed
                   # by a word break in a positive lookahead
    /mx            # multi-line and extended/free-spacing regex definition modes

puts text.sub(r, to_enter)
(prints)
class A
  def a
    'hi'
  end
  def hello
    puts "hello!"
  end

end

Note that sub is replacing an empty string with to_enter.

Upvotes: 1

Wiktor Stribiżew
Wiktor Stribiżew

Reputation: 626870

The approach explained in the post is working here, too. You just need to re-organize capturing groups and use the /m modifier that forces . to match newline symbols, too.

new_string = <<EOS
  def hello
    puts "Hello!"
  end
EOS

file_to_override = <<EOS
class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  helper_method :authenticated?, :current_user

  def current_user?
    session[:current_user]   
  end

end
EOS

file_to_override=file_to_override.gsub(/(.*)(\nend\b.*)/m, "\\1\n#{new_string}\\2")
puts file_to_override

See IDEONE demo

The /(.*)(\nend\b.*)/m pattern will match and capture into Group 1 all the text up to the last whole word (due to the \n before and \b after) end preceded with a line feed, and will place the line feed, "end" and whatever remains into Group 2. In the replacement, we back-reference the captured substrings with backreferences \1 and \2 and also insert the string we need to insert.

If there are no other words after the last end, you could even use a /(.*)(\nend\s*\z)/m regex.

Upvotes: 2

Related Questions