Ruby: How to replace text in a file? - ruby

The following code is a line in an xml file:
<appId>455360226</appId>
How can I replace the number between the 2 tags with another number using ruby?

There is no possibility to modify a file content in one step (at least none I know, when the file size would change).
You have to read the file and store the modified text in another file.
replace="100"
infile = "xmlfile_in"
outfile = "xmlfile_out"
File.open(outfile, 'w') do |out|
out << File.open(infile).read.gsub(/<appId>\d+<\/appId>/, "<appId>#{replace}</appId>")
end
Or you read the file content to memory and afterwords you overwrite the file with the modified content:
replace="100"
filename = "xmlfile_in"
outdata = File.read(filename).gsub(/<appId>\d+<\/appId>/, "<appId>#{replace}</appId>")
File.open(filename, 'w') do |out|
out << outdata
end
(Hope it works, the code is not tested)

You can do it in one line like this:
IO.write(filepath, File.open(filepath) {|f| f.read.gsub(//<appId>\d+<\/appId>/, "<appId>42</appId>"/)})
IO.write truncates the given file by default, so if you read the text first, perform the regex String.gsub and return the resulting string using File.open in block mode, it will replace the file's content in one fell swoop.
I like the way this reads, but it can be written in multiple lines too of course:
IO.write(filepath, File.open(filepath) do |f|
f.read.gsub(//<appId>\d+<\/appId>/, "<appId>42</appId>"/)
end
)

replace="100"
File.open("xmlfile").each do |line|
if line[/<appId>/ ]
line.sub!(/<appId>\d+<\/appId>/, "<appId>#{replace}</appId>")
end
puts line
end

The right way is to use an XML parsing tool, and example of which is XmlSimple.
You did tag your question with regex. If you really must do it with a regex then
s = "Blah blah <appId>455360226</appId> blah"
s.sub(/<appId>\d+<\/appId>/, "<appId>42</appId>")
is an illustration of the kind of thing you can do but shouldn't.

Related

Is there a way to delete a line in a text file if it matches a specific text?

My text file contains an email/password list to set up accounts. Once I've used an email and password combo I would like to erase it form the text file.
My text file looks like this:
email,pass
email,pass
etc..
once I've used the e/p combo I would like to delete it from the file:
File.open("yahoo_accounts.txt") do |email|
email.each do |item|
email, password = item.chomp.split(',')
emails << email
passwords << password
emails.zip(passwords) { |name, pass|
browser = Watir::Browser.new :ff
#using the email and pass
File.open("yahoo_accounts.txt", "w") do |out_file|
File.foreach("yahoo_accounts.txt","r") do |line|
out_file.puts line unless line == '#{name},#{pass}'
end
end
browser.close
end
end
The problem occurs when I try to delete them from the file. I get an "browser.rb:382:in `assert_exists': browser was closed (Watir::Exception::Error)",
but that might just be the browser closing.
If all the e/p's are extracted (meaning there is nothing to delete) in the beginning, how can I loop it to keep going, instead of ending in error after the first zip loop?
If the file is not huge, you can read it into an array, modify the array and then write the array to the file, overwriting the previous contents.
Code
def remove_lines(fname, user_name, password)
IO.write(fname, IO.read(fname).gsub(/^#{user_name},#{password}\n/, ''))
end
Example
Let's create some data:
text =<<_
Bubba,boar
Henrietta,vespa
Luigi,pink
Bubba,boar
Luigi,mauve
_
#=> "Bubba,boar\nHenrietta,vespa\nLuigi,pink\nBubba,boar\nLuigi,mauve\n"
and write it to a file:
FName = "tmp"
IO.write(FName, text)
#=> 61
We can confirm what we've written to file:
IO.read(FName)
#=> "Bubba,boar\nHenrietta,vespa\nLuigi,pink\nBubba,boar\nLuigi,mauve\n"
Now remove the lines for user_name/password #=> Bubba/boar:
remove_lines(FName, "Bubba", "boar")
#=> 39
Let's confirm it worked by examining FName:
IO.read(FName)
#=> "Henrietta,vespa\nLuigi,pink\nLuigi,mauve\n"
Postscript
If something goes wrong while writing the array to file, you may lose both the old file and the new file. For that reason, it is often good practice to write the array to a temporary file, then delete the original file, then rename the temporary file to the name of the original file.
You can't write and read to the same file at the same time like that; the first line you write, will clobber the rest of the file so you won't be able to read it anymore.
You should write the changes to a temporary file as you read the lines, then move the temp file to the original file at the end. Something like:
require 'tempfile'
require 'fileutils'
temp_file = Tempfile.new('foo')
begin
File.open("yahoo_accounts.txt", 'r') do |file|
file.each_line do |line|
temp_file.puts line unless line == '#{name},#{pass}'
end
end
temp_file.close
FileUtils.mv(temp_file.path, "yahoo_accounts.txt")
ensure
temp_file.close
temp_file.unlink
end

Printing lines from a file in ruby

When I start printing lines from a file, I get this error
#<File:0x007ff65ee297b0>
Here is the code
require 'rubygems'
File.open("sample.txt", 'r') do |f|
puts f
end
You are printing the file object. To get the contents line by line, you can use File.foreach
File.foreach('sample.txt', 'r') do |line|
puts line # called for every line
end
To process the whole file at once, you can use the read method on the file object:
File.open('sample.txt', 'r') do |file|
puts file.read # called only once
end
This is not an error. It prints correctly one line which is your File object.
Here your create a file object and you did not ask it to fetch lines or anything else for that matter.
Several good answers already. But here is another way to do it with minimal change to your code:
File.open("sample.txt", 'r').each_line do |f|
puts f
end
Another way :
IO.foreach("sample.txt") {|line| line }
Or
File.foreach('sample.txt') {|line| line }
File::open returns file handle (which apparently is being printed out as #<File:0x007ff65ee297b0>.) If you need the file content line by line you might want to use IO::readlines:
IO.readlines("sample.txt").each do |line|
puts line
end

Add a header to a CSV file with Ruby

I have a CSV file that currently does not have any headers. I would like to add a header for the first column. Is there a way to accomplish this in Ruby?
For instance, lets say my CSV looks like this at first:
1,Testing,Testing,New York
2,Testing,Testing,Boston
I want to add a header to the first column so that my csv looks like this:
my_id
1,Testing,Testing,New York
2,Testing,Testing,Boston
Here is one way to do it.
The r+ mode to file.open opens for read + write.
file = File.open("data.csv", "r+")
buffer = file.read
file.rewind
file.puts "this is my header"
file.print buffer
file.close
One possible solution...
$-i = ".orig"
if ARGV.length == 1 || ARGV.length == 2
header = ARGV.shift
buffer = ARGF.read
puts header
print buffer
else
STDERR.puts "Must supply one or two command-line arguments:"
STDERR.puts "\tdesired header (in quotes if it contains spaces)"
STDERR.puts "\tthe name of the file to prepend the header to"
STDERR.puts "If the second argument is omitted, reads from STDIN"
end
The $-i will make a backup copy of the original input file with the suffix ".orig" appended, and the original file name will now contain the specified header followed by the original contents.

How can I copy the contents of one file to another using Ruby's file methods?

I want to copy the contents of one file to another using Ruby's file methods.
How can I do it using a simple Ruby program using file methods?
There is a very handy method for this - the IO#copy_stream method - see the output of ri copy_stream
Example usage:
File.open('src.txt') do |f|
f.puts 'Some text'
end
IO.copy_stream('src.txt', 'dest.txt')
For those that are interested, here's a variation of the IO#copy_stream, File#open + block answer(s) (written against ruby 2.2.x, 3 years too late).
copy = Tempfile.new
File.open(file, 'rb') do |input_stream|
File.open(copy, 'wb') do |output_stream|
IO.copy_stream(input_stream, output_stream)
end
end
As a precaution I would recommend using buffer unless you can guarantee whole file always fits into memory:
File.open("source", "rb") do |input|
File.open("target", "wb") do |output|
while buff = input.read(4096)
output.write(buff)
end
end
end
Here my implementation
class File
def self.copy(source, target)
File.open(source, 'rb') do |infile|
File.open(target, 'wb') do |outfile2|
while buffer = infile.read(4096)
outfile2 << buffer
end
end
end
end
end
Usage:
File.copy sourcepath, targetpath
Here is a simple way of doing that using ruby file operation methods :
source_file, destination_file = ARGV
script = $0
input = File.open(source_file)
data_to_copy = input.read() # gather the data using read() method
puts "The source file is #{data_to_copy.length} bytes long"
output = File.open(destination_file, 'w')
output.write(data_to_copy) # write up the data using write() method
puts "File has been copied"
output.close()
input.close()
You can also use File.exists? to check if the file exists or not. This would return a boolean true if it does!!
Here's a fast and concise way to do it.
# Open first file, read it, store it, then close it
input = File.open(ARGV[0]) {|f| f.read() }
# Open second file, write to it, then close it
output = File.open(ARGV[1], 'w') {|f| f.write(input) }
An example for running this would be.
$ ruby this_script.rb from_file.txt to_file.txt
This runs this_script.rb and takes in two arguments through the command-line. The first one in our case is from_file.txt (text being copied from) and the second argument second_file.txt (text being copied to).
You can also use File.binread and File.binwrite if you wish to hold onto the file contents for a bit. (Other answers use an instant copy_stream instead.)
If the contents are other than plain text files, such as images, using basic File.read and File.write won't work.
temp_image = Tempfile.new('image.jpg')
actual_img = IO.binread('image.jpg')
IO.binwrite(temp_image, actual_img)
Source: binread,
binwrite.

Read Certain Lines from File

Hi just getting into Ruby, and I am trying to learn some basic file reading commands, and I haven't found any solid sources yet.
I am trying to go through certain lines from that file, til the end of the file.
So in the file where it says FILE_SOURCES I want to read all the sources til end of file, and place them in a file.
I found printing the whole file, and replacing words in the file, but I just want to read certain parts in the file.
Usually you follow a pattern like this if you're trying to extract a section from a file that's delimited somehow:
open(filename) do |f|
state = nil
while (line = f.gets)
case (state)
when nil
# Look for the line beginning with "FILE_SOURCES"
if (line.match(/^FILE_SOURCES/))
state = :sources
end
when :sources
# Stop printing if you hit something starting with "END"
if (line.match(/^END/))
state = nil
else
print line
end
end
end
end
You can change from one state to another depending on what part of the file you're in.
I would do it like this (assuming you can read the entire file into memory):
source_lines = IO.readlines('source_file.txt')
start_line = source_lines.index{ |line| line =~ /SOURCE_LINE/ } + 1
File.open( 'other_file.txt', 'w' ) do |f|
f << source_lines[ start_line..-1 ].join( "\n" )
end
Relevant methods:
IO.readlines to read the lines into an array
Array#index to find the index of the first line matching a regular expression
File.open to create a new file on disk (and automatically close it when done)
Array#[] to get the subset of lines from the index to the end
If you can't read the entire file into memory, then I'd do a simpler variation on #tadman's state-based one:
started = false
File.open( 'other_file.txt', 'w' ) do |output|
IO.foreach( 'source_file.txt' ) do |line|
if started then
output << line
elsif line =~ /FILE_SOURCES/
started = true
end
end
end
Welcome to Ruby!
File.open("file_to_read.txt", "r") {|f|
line = f.gets
until line.include?("FILE_SOURCES")
line = f.gets
end
File.open("file_to_write.txt", "w") {|new_file|
f.each_line {|line|
new_file.puts(line)
}
new_file.close
}
f.close
}
IO functions have no idea what "lines" in a file are. There's no straightforward way to skip to a certain line in a file, you'll have to read it all and ignore the lines you don't need.

Resources