Insert text before the end of a file - ruby

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:
end(?=[^end]*$)
end(?!.*end)
(.*)(end)(.*)
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!

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.

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.

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.

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/)).

Related

How to replace letters with lines? (Ruby)

I'm trying to build a hangman game and I need to "censor" the random word with lines, so that for example the word "ruby" is shown as four lines: _ _ _ _
The problem is that my method replaces the word with two extra lines.
Here is my method:
class Game
attr_reader :the_word
def initialize
#the_word = random_word
#player_input = Array.new
#attempts_left = 10
end
def random_word
#the_word = File.readlines("../5desk.txt").sample
end
def hide_the_word
#hidden_word = #the_word.split("")
#hidden_word.each {|letter| letter.replace("_")}
puts "Can you guess what this word is? #{#hidden_word.join()}"
puts #the_word #delete this
end
end
If the random word is "ruby" I would like the game to replace the word with four lines, but it always shows two extra lines (with every random word).
Any help is much appreciated!
Your word from a file may contain spaces. You can remove them by using strip() method.
#the_word = File.readlines("../5desk.txt").sample.strip()
But, as #ggorien mentioned, a simpler way to achieve this is:
#hidden_word = "_" * #the_word.size
and change the puts to
puts "Can you guess what this word is? #{#hidden_word}"

How to use group capturing for selecting words at the end of the line?

I'm having a hard time using group capturing. I have the following text:
class Rename < ActiveRecord::Migration
def change
rename_table :users, :vendors
rename_table :places, :venues
end
end
In this case, I need to extract vendors and venues.
I'm trying to use something like /rename_table.*([]+)$/, but to no avail.
How do I achieve this?
Something like this might work for you:
/rename_table.+:(\S+)/g
It will store the last word prefixed with : form lines containing rename_table (vendors, venues) in match group $1.
Try it online
▶ text.scan(/rename_table.+:(\w+)\s*$/).flatten
#⇒ [
# [0] "vendors",
# [1] "venues"
# ]
\b(rename_table.*\,.*\:)(.*)/g
match group $1
you can test it here https://regex101.com/r/pN9bO4/1
You don't need regexes:
str = %q{
class Rename < ActiveRecord::Migration
def change
rename_table :users, :vendors
rename_table :places, :venues
end
end
}
str.each_line do |line|
puts line.split[-1] if line.lstrip.start_with? 'rename_table'
end
--output:--
:vendors
:venues
In any case, the group in your regex is ([]+). Brackets are special regex characters and they represent a character class, in which you specify the characters that you want to match, e.g. [xyz]. That character class will match one character that is either an x or a y or a z. In your case, the character class is empty, which produces an error in ruby 2.2:
empty char-class: /rename_table.*([]+)$/
Essentially, ruby is saying, Wtf??! You specified a character class with no characters. Are you really trying to say, I want to match one character that is in the character class consisting of no characters?. I don't think so! Error! Error! Error!

What is <<- in ruby?

From the Ruby_Newbie Guide to Symbols:
Author is trying to show a simplified implementation of the attr_writer method.
#!/usr/bin/env ruby
def make_me_a_setter(thename)
eval <<-SETTERDONE # <----- Here
def #{thename}(myarg)
##{thename} = myarg
end
SETTERDONE
end
class Example
make_me_a_setter :symboll
make_me_a_setter "stringg"
def show_symboll
puts #symboll
end
def show_stringg
puts #stringg
end
end
example = Example.new
example.symboll("ITS A SYMBOL")
example.stringg("ITS A STRING")
example.show_symboll
example.show_stringg
It's a heredoc. From the "Here Documents" documentation:
If you are writing a large block of text you may use a “here document”
or “heredoc”:
expected_result = <<HEREDOC
This would contain specially formatted text.
That might span many lines
HEREDOC
The heredoc starts on the line following <<HEREDOC and ends with the
next line that starts with HEREDOC. The result includes the ending
newline.
It's a multi line string. The code evaluates code embedded inside string. More on multi line strings:
http://blog.jayfields.com/2006/12/ruby-multiline-strings-here-doc-or.html
P.S. Using eval is not recommended, alternatives - yield, instance_eval, class_eval.

Ruby Regex not matching

I'm writing a short class to extract email addresses from documents. Here is my code so far:
# Class to scrape documents for email addresses
class EmailScraper
EmailRegex = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
def EmailScraper.scrape(doc)
email_addresses = []
File.open(doc) do |file|
while line = file.gets
temp = line.scan(EmailRegex)
temp.each do |email_address|
puts email_address
emails_addresses << email_address
end
end
end
return email_addresses
end
end
if EmailScraper.scrape("email_tests.txt").empty?
puts "Empty array"
else
puts EmailScraper.scrape("email_tests.txt")
end
My "email_tests.txt" file looks like so:
example#live.com
another_example90#hotmail.com
example3#diginet.ie
When I run this script, all I get is the "Empty array" printout. However, when I fire up irb and type in the regex above, strings of email addresses match it, and the String.scan function returns an array of all the email addresses in each string. Why is this working in irb and not in my script?
Several things (some already mentioned and expanded upon below):
\z matches to the end of the string, which with IO#gets will typically include a \n character. \Z (upper case 'z') matches the end of the string unless the string ends with a \n, in which case it matches just before.
the typo of emails_addresses
using \A and \Z is fine while the entire line is or is not an email address. You say you're seeking to extract addresses from documents, however, so I'd consider using \b at each end to extract emails delimited by word boundaries.
you could use File.foreach()... rather than the clumsy-looking File.open...while...gets thing
I'm not convinced by the Regex - there's a substantial body of work already around:
There's a smarter one here: http://www.regular-expressions.info/email.html (clicking on that odd little in-line icon takes you to a piece-by-piece explanation). It's worth reading the discussion, which points out several potential pitfalls.
Even more mind-bogglingly complex ones may be found here.
class EmailScraper
EmailRegex = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\Z/i # changed \z to \Z
def EmailScraper.scrape(doc)
email_addresses = []
File.foreach(doc) do |line| # less code, same effect
temp = line.scan(EmailRegex)
temp.each do |email_address|
email_addresses << email_address
end
end
email_addresses # "return" isn't needed
end
end
result = EmailScraper.scrape("email_tests.txt") # store it so we don't print them twice if successful
if result.empty?
puts "Empty array"
else
puts result
end
Looks like you're putting the results into emails_addresses, but are returning email_addresses. This would mean that you're always returning the empty array you defined for email_addresses, making the "Empty array" response correct.
You have a typo, try with:
class EmailScraper
EmailRegex = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
def EmailScraper.scrape(doc)
email_addresses = []
File.open(doc) do |file|
while line = file.gets
temp = line.scan(EmailRegex)
temp.each do |email_address|
puts email_address
email_addresses << email_address
end
end
end
return email_addresses
end
end
if EmailScraper.scrape("email_tests.txt").empty?
puts "Empty array"
else
puts EmailScraper.scrape("email_tests.txt")
end
You used at the end \z try to use \Z according to http://www.regular-expressions.info/ruby.html it has to be a uppercase Z to match the end of the string.
Otherwise try to use ^ and $ (matching the start and the end of a row) this worked for me here on Regexr
When you read the file, the end of line is making the regex fail. In irb, there probably is no end of line. If that is the case, chomp the lines first.
regex=/\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
line_from_irb = "example#live.com"
line_from_file = line_from_irb +"/n"
p line_from_irb.scan(regex) # => ["example#live.com"]
p line_from_file.scan(regex) # => []

Converting upper-case string into title-case using Ruby

I'm trying to convert an all-uppercase string in Ruby into a lower case one, but with each word's first character being upper case. Example:
convert "MY STRING HERE" to "My String Here".
I know I can use the .downcase method, but that would make everything lower case ("my string here"). I'm scanning all lines in a file and doing this change, so is there a regular expression I can use through ruby to achieve this?
Thanks!
If you're using Rails (really all you need is ActiveSupport, which is part of Rails), you can use titleize:
"MY STRING HERE".titleize
# => "My String Here"
If you're using plain Ruby but don't mind loading a small amount of ActiveSupport you can require it first:
require 'active_support/core_ext/string/inflections'
# => true
"MY STRING HERE".titleize
# => "My String Here"
N.B. By default titleize doesn't handle acronyms well and will split camelCaseStrings into separate words. This may or may not be desirable:
"Always use SSL on your iPhone".titleize
# => "Always Use Ssl On Your I Phone"
You can (partially) address this by adding "acronyms":
require 'active_support/core_ext/string/inflections' # If not using Rails
ActiveSupport::Inflector.inflections do |inflect|
inflect.acronym 'SSL'
inflect.acronym 'iPhone'
end
"Always use SSL on your iPhone".titleize
# => "Always Use SSL On Your IPhone"
For those who speak the Queen's English (or who struggle to spell titleize), there's no .titleise alias but you can use .titlecase instead.
"HELLO WORLD HOW ARE YOU".gsub(/\w+/) do |word|
word.capitalize
end
#=> "Hello World How Are You"
While trying to come up with my own method (included below for reference), I realized that there's some pretty nasty corner cases. Better just use the method already provided in Facets, the mostest awesomest Ruby library evar:
require 'facets/string/titlecase'
class String
def titleize
split(/(\W)/).map(&:capitalize).join
end
end
require 'test/unit'
class TestStringTitlecaseAndTitleize < Test::Unit::TestCase
def setup
#str = "i just saw \"twilight: new moon\", and man! it's crap."
#res = "I Just Saw \"Twilight: New Moon\", And Man! It's Crap."
end
def test_that_facets_string_titlecase_works
assert_equal #res, #str.titlecase
end
def test_that_my_own_broken_string_titleize_works
assert_equal #res, #str.titleize # FAIL
end
end
If you want something that more closely complies to typical writing style guidelines (i.e. does not capitalize words like "and"), there are a couple of "titleize" gems on GitHub.
From ActiveSupport
"MY STRING HERE".gsub(/\b('?[a-z])/) { $1.capitalize }
If you are using Rails/ActiveSupport, the method is already available for free.
string = "MY STRING HERE"
string.split(" ").map {|word| word.capitalize}.join(" ")
The way this works:
The .split(" ") splits it on spaces, so now we have an array that looks like ["my", "string", "here"]. The map call iterates over each element of the array, assigning it to temporary variable word, which we then call capitalize on. Now we have an array that looks like ["My", "String", "Here"], and finally we turn that array back into a string by joining each element with a space (" ").
"MY STRING HERE".titlecase
Does the job (it's a method in the Rails gem, however)
http://apidock.com/rails/String/titlecase
Unicode-aware titlecase for Ruby 2.4.0+:
class String
def titlecase
split(/([[:alpha:]]+)/).map(&:capitalize).join
end
end
>> "я только что посмотрел \"леди исчезает\", и это чума!".titlecase
=> "Я Только Что Посмотрел \"Леди Исчезает\", И Это Чума!"
(based on https://stackoverflow.com/a/1792102/788700)
To catch any edge case such as:
str = "rUby on rAils"
Don't use:
str.titleize
Output: R Uby On R Ails
Use instead:
str.downcase.titleize
Output: Ruby On Rails
I've try to improve code... ready for critics and suggestions.
class Book
attr_accessor :title
def title=(new_title)
notcap=%w(and the a in of an)
str=''
new_title.gsub(/(\w|\s)\w+/) do |word|
word.strip!
if not notcap.include? word
word.capitalize!
end
str += ' ' + word
end
str.strip!
str = str[0].upcase + str[1..-1]
#title = str
end
end
The ruby core itself has no support to convert a string from upper (word) case to capitalized word case.
So you need either to make your own implementation or use an existing gem.
There is a small ruby gem called lucky_case which allows you to convert a string from any of the 10+ supported cases to another case easily:
require 'lucky_case'
# to get capital word case as string
LuckyCase.capital_word_case('MY STRING HERE') # => 'My String Here'
# or the opposite way
LuckyCase.upper_word_case('Capital Word Case') # => 'MY STRING HERE'
You can even monkey patch the String class if you want to:
require 'lucky_case/string'
'MY STRING HERE'.capital_word_case # => 'My String Here'
'MY STRING HERE'.capital_word_case! # => 'My String Here' and overwriting original
Have a look at the offical repository for more examples and documentation:
https://github.com/magynhard/lucky_case
Capitalizes every word in a sentence using ruby, without regex.. because unfortunately those scare me
class Book
attr_accessor :title
def title=(new_title)
result = []
words = new_title.split(' ')
words.each do |word|
capitalized = word[0].upcase + word[1..word.length].downcase
result.push(capitalized)
end
#title = result.join(' ')
end
end

Resources