mixing file.write and system calls that write to same file - ruby

I have code that invokes two system commands that write to a file and I need to add something else from my code in between these calls:
File.open('test.txt', 'w') {|f|
`echo 1 > #{f.path}`
f.write '2'
`echo 3 >> #{f.path}`
}
as the result the file contains just
2
3
the first line is missing. I am sure there is a simple solution, but I cannot find it.

You are opening the file in "write" mode which is clobbering the first echo. Instead use 'append' mode. Additionally, you're not flushing after you write "2" so it'll be out of order when you read it back. Remember, f.write doesn't append a newline, so you probably need that too.
irb(main):020:0> File.open('asdf', 'a') do |f|
irb(main):021:1* `echo 1 > asdf`
irb(main):022:1> f.write("2\n") and f.flush
irb(main):023:1> `echo 3 >> asdf`
irb(main):024:1> end
=> ""
irb(main):025:0> File.read('asdf')
=> "1\n2\n3\n"
irb(main):026:0> puts File.read('asdf')
1
2
3
The File.open(name, 'a') is the important part. It means append to this file instead of overwriting it. See http://www.ruby-doc.org/core-2.0/IO.html#method-c-new and What are the Ruby File.open modes and options? for descriptions of file open modes.
If it's important to delete any existing file, the first echo will implicitly take care of that (since it's a single >). Or you can do it in ruby explicitly:
File.delete('asdf') if File.exists?('asdf')

to answer my own question:
File.open('test.txt', 'w') {|f|
`echo 11 > #{f.path}`
f.seek(0, IO::SEEK_END)
f.write "2\n"
f.flush
`echo 33 >> #{f.path}`
}

Related

File.exist? always returns false even when file does exist

I have a program that tries to open a file:
Dir.chdir(File.dirname(__FILE__))
puts "Enter file name: ";
relPath = gets;
absPath = Dir.pwd << "/" << relPath;
if File.exist?(absPath) then
puts "File exists";
file = File.open(absPath, "r");
other code...
else
puts "File does not exist";
end
It always prints "File does not exist" even when the current directory exists and the file also exists. The file and script are in the same directory.
I am running it on Mac OS X Yosemite (10.10.3) and Ruby 2.2.0p0.
I can't explain why (albeit I have strong belief that it's for some whitespace characters) but with this little contribution it works ok.
Dir.chdir(File.dirname(__FILE__))
print "Enter file name:";
relPath = gets.chomp; #intuitively used this, and it wroked fine
absPath = File.expand_path(relPath) #used builtin function expand_path instead of string concatenation
puts absPath
puts File.file?(absPath)
if File.exist?(absPath) then
puts "File exists";
puts File.ctime(absPath) #attempting a dummy operation :)
else
puts "File does not exist";
end
runnning code
$ ls -a anal*
analyzer.rb
$ ruby -v
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-linux]
ziya#ziya:~/Desktop/code/ruby$ ruby fileexists.rb
Enter file name:analyzer.rb
/home/ziya/Desktop/code/ruby/analyzer.rb #as a result of puts absPath
true #File.file?(absPath) => true
File exists
2015-06-11 12:48:31 +0500
That code has syntax error ("if" doesnt need "then"), and you dont have to put ";" after each line.
try
Dir.chdir(File.dirname(__FILE__))
puts "Enter file name: "
relPath = gets
absPath = "#{Dir.pwd}/#{relPath.chop}"
if File.exist?(absPath)
puts "File exists"
file = File.open(absPath, "r")
else
puts "File does not exist"
end
remember that gets will add a new line character so you will need to do a chomp, and that way to concatenate string won't work on ruby.
Your code is not idiomatic Ruby. I'd write it something like this untested code:
Dir.chdir(File.dirname(__FILE__))
puts 'Enter file name: '
rel_path = gets.chomp
abs_path = File.absolute_path(rel_path)
if File.exist?(abs_path)
puts 'File exists'
File.foreach(abs_path) do |line|
# process the line
end
else
puts 'File does not exist'
end
While Ruby supports the use of ;, they're for use when we absolutely must provide multiple commands on one line. The ONLY time I can think of needing that is when using Ruby to execute single-line commands at the command-line. In normal scripts I've never needed ; between statements.
then is used with if when we're using a single line if expression, however, we have trailing if which removes the need for then. For instance, these accomplish the same thing but the second is idiomatic, shorter, less verbose and easier to read:
if true then a = 1 end
a = 1 if true
See "What is the difference between "if" statements with "then" at the end?" for more information.
Instead of relPath and absPath we use snake_case for variables, so use rel_path and abs_path. It_is_a_readability AndMaintenanceThing.
File.absolute_path(rel_path) is a good way to take the starting directory and return the absolute path given a relative directory.
File.foreach is a very fast way to read a file, faster than slurping it using something like File.read. It is also scalable whereas File.read is not.

What's the difference between gets and readline?

As far as I know, both of them can read from the console.
I know gets can read from a file too, but I am interested in console-input.
Example:
a = readline.chomp
puts a
a = gets.chomp
puts a
This gives me the same output. So what's the difference for console-input?
From ruby-doc.org about Kernel#readline:
Equivalent to Kernel::gets, except readline raises EOFError at end of file.
gets returns nil at end of input.
You can see the difference easily:
echo -n "" | ruby -e "gets" # no error
echo -n "" | ruby -e "readline" # -e:1:in `readline': end of file reached (EOFError)

Multiple files as command line arguments?

I wrote a script to read IP addresses from a file and print the amount in the file. I wasn't fully satisfied so I attempted to modify it to allow reading multiple files, and I would specify the files via cmd arguments. The problem that I'm having is that it seems to read multiple files as one argument.
def host_count(*files)
begin
files.each do
files = files.join(' ')
read = IO.read(files)
reg = read.scan(/(?:\d{1,3}\.){3}\d{1,3}/).size
puts "There are " << reg.to_s << " IP addresses in #{files}."
end
rescue Errno::ENOENT
puts "File #{files} does not exist!"
rescue TypeError
puts "Usage: #{$0} [file]"
puts "Example: #{$0} /home/user/ipfile.txt"
end
end
host_count(ARGV)
Running this script with multiple files gives me this error:
File file1 file2 does not exist!
They aren't separated by commas or anything, so it's not reading my arguments as: ["file1","file2"], which was my original problem. What am I not understanding?
You wrote
files.each do
files = files.join(' ')
Why would you do that?
you're changing the array..
the "files" array is already an array, you don't have to join it with strings.
edit1:
to get the specific file for each run, you should write:
files.each do |file|
puts file # will print "file 1", and in the next iteration will print "file 2".
end

How to replace " for ' in all csv files in one directory?

I need to replace " for ' in all csv files in one directory. I wonder if there is either
nice one line piece of code to do that or
if I use the fastest way to solve what I do
My code is
files = Dir["*.csv"]
files.each do |file_name|
File.open(file_name, "w") {|file|
file.puts File.read(file_name).gsub('"', "'")
}
end
update
ruby 1.9.3p194 (2012-04-20) [i386-mingw32]
I prefer ruby only solution
You can edit the files in place like so:
ruby -pi~ -e "gsub(/\"/, \"'\")" *.csv
Windows or a *nix system? If you're on something with sed, let it do what it's designed to do:
sed "s/\"/'/" < file_to_fix > updated_file
sed is used for similar tasks constantly and is blindingly fast. I wouldn't embed it in a Ruby script at all, but instead would use it at the command-line.
The problem with the accepted answer is that it is asking you to call an external script (yes, it is ruby but external non the less) instead of using ruby.
How about:
def inplace_edit(file, bak, &block)
old_argv = Array.new(ARGV)
old_stdout = $stdout
ARGV.replace [file]
ARGF.inplace_mode = bak
ARGF.lines do |line|
yield line
end
ARGV.replace old_argv
$stdout = old_stdout
end
my_files_array.each do |file|
inplace_edit file, '.bak' do |line|
STDOUT.puts "[Debug] #{line}"
line = line.gsub(/search1/,"replace1")
line = line.gsub(/search2/,"replace2")
print line
end
end
If you don't want to create a backup of every file then change '.bak' to ''.

File reading problem

f = File.open("/test/serverlist.txt", "r")
list = f.readlines
list.each do|servers|
File.open('/test/results.txt','w') do |b|
servers.each do |p|
r = `ping -n 1 #{p}`
b.puts r
end
end
It reads the serverlist file, and returns a string. The serverlist file contains the following IP addresses:
192.168.150.254
192.168.120.2
Are you looking to read each line from the file and then do something with like this.
fout = File.open('/test/results.txt','w')
File.open("/test/serverlist.txt", "r").each_line do |server|
server.chomp!
r = `ping -n 1 #{server}`
fout.puts r
end
I don't think you will need to iterate over the server line itself, and with a few style mods added and ping(1) arguments changed, I would suggest...
open 'serverlist.txt', 'r' do |f|
open '/tmp/results.txt', 'w' do |b|
f.readlines.each do |server|
b.puts `ping -c 1 -t 1 #{server}`
end
end
end
Just use b.write in place of b.puts
if you're using linux you could just go for
File.open("serverlist.txt").each { |addy| `echo "#{`ping -c 1 #{addy}`}" >> result.txt` }
and be done with it
well .. maybe add
`echo "# server-availability" > result.txt`
before the above line so the file gets reset every time you call this

Resources