"Undefined method 'close'" when trying to close a file in Ruby - ruby

I'm working through "Learn Ruby the Hard Way" and receive an undefined method 'close' error when trying to run the example file here: http://ruby.learncodethehardway.org/book/ex17.html
My code, specifically is:
from_file, to_file = ARGV
script = $0
puts "Copying from #{from_file} to #{to_file}."
input = File.open(from_file).read()
puts "The input file is #{input.length} bytes long."
puts "Does the output file exist? #{File.exists? to_file}"
puts "Ready, hit RETURN to contine, CTRL-C to abort."
STDIN.gets
output = File.open(to_file, 'w')
output.write(input)
puts "Alright, all done."
output.close()
input.close()
The error I receive is only for the last line 'input.close()', as 'output.close()' seems to work fine. For reference, I'm using a preexisting input file, and creating a new output file.
Thanks in advance.

Your input is not a file object because of the read() method call:
input = File.open(from_file).read()
Since read returns either nil or "" depending upon the length parameter to read, calling input.close() will raise undefined method close as input in your case is a string and String does not have close() method.
So instead of calling File.open(from_file).read() and calling the close() method, you can just call the File.read():
from_file, to_file = ARGV
script = $0
puts "Copying from #{from_file} to #{to_file}."
input = File.read(from_file)
puts "The input file is #{input.length} bytes long."
puts "Does the output file exist? #{File.exists? to_file}"
puts "Ready, hit RETURN to contine, CTRL-C to abort."
STDIN.gets
output = File.open(to_file, 'w')
output.write(input)
puts "Alright, all done."
output.close()

Related

ex17.rb:5:in `open': no implicit conversion of nil into String (TypeError) from ex17.rb:5:in `<main>'

I am going through "Ruby the Hard Way". Typing in the exercises as instructed. Got to Exercise 17 and as far as i can tell typed in correctly but when i run it get this error:
ex17.rb:5:in open': no implicit conversion of nil into String (TypeError) from ex17.rb:5:in '
Can anyone tell me what is wrong here?
put the 5 in the code below to show line 5. I think the error means it is in line 5.
This is the exercise:
from_file, to_file = ARGV
puts "Copying from #{from_file} to #{to_file}"
5 in_file = open(from_file)
indata = in_file.read
puts "The input file is #{indata.length} bytes long"
puts "Does the output file exist? #{Fileexist?(to_file)}"
puts "Ready , hit RETURN to continue, CTRL-C to abort."
$stdin.gets
out_file = open(to_file, 'w')
out_file.write(indata)
puts "Alright, all done."
out_file.close
in_file.close
I was running this on the Terminal, I ran it as >ruby ex17.rb, without submitting a filename afterwards. So, on the prompt, I should have submitted it as >ruby ex17.rb with a file name.

How would you close this file descriptor?

Let's say you have the following code:
from_file, to_file = ARGV
puts "Copying from #{from_file} to #{to_file}"
#in_file = open(from_file)
#indata = in_file.read
indata = open(from_file).read # Combined in_file and indata.
puts "The input file is #{indata.length} bytes long."
puts "Does the output file exist? #{File.exist?(to_file)}"
puts "Ready, hit RETURN to continue or CTRL-C to abort."
$stdin.gets
out_file = open(to_file, 'w')
out_file.write(indata)
puts "Alright, all done."
out_file.close
#in_file.close
How would you close the file descriptor invoked by indata? You will need to close File open, but indata is really a (File open).read.
P.S. Since it's a script, it will be closed automatically upon exit. Let's assume that we're running a general, consistently running backend service. And we don't know whether garbage collector will kick in, so we will need to explicitly close it. What would you do?
If you are just copying the file...
you could just use FileUtils#cp:
FileUtils.cp("from_file", "to_file")
or even shell-out to the operating system and do it with a system command.
Let's suppose you want to do something to the input file before writing it to the output file.
If from_file is not large,...
you could "gulp it" into a string using IO.read:
str = IO.read(from_file)
manipulate str as desired, to obtain new_str, then then blast it to the output file using IO#write:
IO.write("to_file", new_str)
Note that for the class File:
File < IO #=> true # File inherits IO's methods
which is why you often see this written File.read(...) and File.write(...).
If from_file is large, read a line, write a line...
provided the changes to be made are done for each line separately.
f = File.open("to_file", "w") # or File.new("to_file", "w")
IO.foreach("from_file") do |line|
# < modify line to produce new_line >
f.puts new_line
end
f.close
foreach closes "from_file" when it's finished. If f.close is not present, Ruby will close "to_file" when the method containing the code goes out of scope. Still, it's a good idea to close it in case other work is done before the code goes out of scope.
Passing File.open a block is generally a nice way to go about things, so I’ll offer it up as an alternative even if it doesn’t seem to be quite what you asked.
indata = File.open(from_file) do |f|
f.read
end

Why can't I use `filename.open' instead of `open(filename)'?

In this piece of code which lets you read a file in the terminal, why do you need to use open(filename) rather than filename.open?
filename = ARGV.first
txt = open(filename)
puts "Here's your file #{filename}:"
print txt.read
print "Type the filename again: "
file_again = $stdin.gets.chomp
txt_again = open(file_again)
print txt_again.read
You cant use filename.open, because filename is a String and method open is not defined in String
Use File#open
File.open(filename)
File.open("file")
opens a local file and returns a file object. Here File#open is a method of File class.
open("file")
is actually Kernel#open and looks at the string to decide what to do with it.
Trivializing things:
File.open("file") is telling Ruby specifically to open a file.
In case of open("file") Ruby examines the string "file" to determine what type it is (here a file) and open corresponding type else throws appropriate error.
Ruby has a class for dealing with paths in an object-oriented way: Pathname
require 'pathname'
loop do
print 'Enter filename: '
pn = Pathname(gets.chomp)
if pn.file?
puts "Here's your file '#{pn}':", pn.read
elsif pn.exist?
puts 'That is not a file.'
else
puts 'File does not exist.'
end
end

Undefined method 'close' for main:Object (NoMethodError) in Ruby

filename = ARGV.first
txt = open filename
puts "Here's your file #{filename}:"
print txt.read
puts "Type the filename again: "
file_again = $stdin.gets.chomp
txt_again = open file_again
print txt_again.read
close(txt)
close(txt_again)
The program runs fine till the end, but crashes with the titled error message right after printing the contents of the second file.
I checked the txt, txt_again using (.class) and confirmed that both are File objects. Why isn't close working?
You need to call close on the file object:
txt.close

ruby popen3 -- how to repeatedly write to stdin & read stdout without re-opening process?

I am using Open3's popen3 method to start a process that functions in a console-like / REPL fashion to repeatedly accept input and return output.
I am able to open the process, send input, and receive the output just fine, with code like this:
Open3.popen3("console_REPL_process") do |stdin, stdout, stderr, wait_thr|
stdin.puts "a string of input"
stdin.close_write
stdout.each_line { |line| puts line } #successfully prints all the output
end
I want to do that many times in a row, without re-opening the process, as it takes a long time to start up.
I know I have to close stdin in order for stdout to return.. but what I don't know is, how do I 'reopen' stdin so I can write more input?
Ideally I want to do something like this:
Open3.popen3("console_REPL_process") do |stdin, stdout, stderr, wait_thr|
stdin.puts "a string of input"
stdin.close_write
stdout.each_line { |line| puts line }
stdin.reopen_somehow()
stdin.puts "another string of input"
stdin.close_write
stdout.each_line { |line| puts line }
# etc..
end
solution
Thanks to pmoo's answer, I was able to devise a solution using PTY and expect, expecting the prompt string that the process returns whenever it is ready for more input, like so:
PTY.spawn("console_REPL_process") do |output, input|
output.expect("prompt >") do |result|
input.puts "string of input"
end
output.expect("prompt >") do |result|
puts result
input.puts "another string of input"
end
output.expect("prompt >") do |result|
puts result
input.puts "a third string of input"
end
# and so forth
end
You can have some success using expect library, and have the child process to explicitly mark the end of each output, like:
require 'expect'
require 'open3'
Open3.popen3("/bin/bash") do
| input, output, error, wait_thr |
input.sync = true
output.sync = true
input.puts "ls /tmp"
input.puts "echo '----'"
puts output.expect("----", 5)
input.puts "cal apr 2014"
input.puts "echo '----'"
puts output.expect("----", 5)
end
As a bonus, expect has a timeout option.

Resources