Reading, Writing, and Ruby Magic - Ruby Wizardry: An Introduction to Programming for Kids (2014)

Ruby Wizardry: An Introduction to Programming for Kids (2014)

Chapter 12. Reading, Writing, and Ruby Magic

File Input and Output

Ruben looked around him and sighed. “Why did we run all the way here if the freight elevator goes so slow?” he asked.

“You know,” said the King, rubbing his beardy chin, “I really don’t know. But I imagine it’ll be here any minute!”

No sooner had the King spoken than the freight elevator arrived with a great clang. The doors slid open, revealing a huge metal elevator car.

“All aboard!” said Rusty, and they all climbed in. Rusty punched a round red button labeled LOADING DOCKS, and with another clang, the car began to slowly descend into the heart of the Refactory.

“We’ll be there in a jiffy,” Rusty said.

“A slow jiffy,” Scarlet said. Ruben stifled a laugh.

image with no caption

“Not to worry,” Rusty said. “Every worker in the Refactory is down there, so there’s no chance those villains’ll escape!”

The King paced around the elevator car. “I can’t wait to question those scoundrels,” he said. “All this trouble they’ve caused! I’ll be keen to know what drove them to it.”

“I’ll bet they’re evil ninja wizards!” said Ruben.

“More like evil robot pirates,” said Scarlet.

“Whoever they are, they’ll have a lot to answer for,” said the Queen. “But we’ll know soon enough. We’re close—I can feel it!”

“That we are,” said Rusty. “Next stop: loading docks!”

A moment later, the freight elevator doors groaned open, and the King, the Queen, Scarlet, Ruben, and Rusty stepped onto the immense, bustling floor of the Refactory loading docks.

“Foreman here!” Rusty yelled to the crowd of men and women in hard hats as he led the group up a metal walkway and onto a large platform in the center of the enormous room. “What’ve we got?”

“Sir!” said Marshall, climbing up the walkway, “I rushed down here ahead of you to try to assess the situation. It looks like we’ve got four intruders holed up in one of the loading docks.”

“Which one?” Rusty asked.

Marshall shook his head. “We don’t know! They hid before we could see where they went. All we know is that we had the docks surrounded when they disappeared, so they must still be in here somewhere.”

Rusty nodded and stroked his beard for a moment. “Well,” he said at last, “best get to finding them.” He walked to the edge of the platform and stepped on a large round indentation with his boot. In a hiss of steam, a column rose out of the platform. On the side facing the Foreman shone the unmistakable glow of a Computing Contraption screen.

“Each dock is controlled by a Ruby program,” Rusty said as the King, the Queen, Ruben, and Scarlet gathered around him. “Ruby treats each of them as a file. If we can open each file, we’ll find our missing criminals!”

“A file? You mean, like a regular computer file?” Scarlet asked.

“The very same!” said Rusty. “Ruby can open just about any file you can think of: Ruby programs, text files, pictures, you name it!”

The Queen smiled. “I know all about files!” she said. “I’d be happy to lend a hand opening all these docks to find our culprits.” She cracked her knuckles. “How many files are there?” she asked.

Rusty gestured to the far wall, which was covered in hundreds of heavy metal doors.

image with no caption

“Oh my,” said the Queen. “Well, then! We’d better get started.” She turned to Scarlet and Ruben. “To do this, we’ll need to use Ruby’s file I/O methods,” she explained. “The I/O part stands for ‘input/output.’ Input is what you put into a file, and output is what comes out.”

“Like when you write a text file or save a picture?” Scarlet asked.

“Very much like that,” the Queen said. “Ruby can write input to a file, which is just like typing it with the keyboard and clicking Save. It can also read output from a file, which is just like double-clicking on the file and opening it!”

The Queen turned to Rusty. “Is there a test file I could use to show how it works?” she asked.

Rusty nodded. “Try lunch.txt,” he said. “I think it just has the text ONE KAT-MAN-BLEU BURGER, PLEASE in it.”

“What’s a Kat-Man-Bleu burger?” asked Ruben.

“It’s the Wednesday lunch special in the Refactory cafeteria!” Rusty said. “The food’s not as good here as the food at the Hashery, but it does all right. That file just has the most recent lunch order in it.”

Opening a File with Ruby

“Very good!” said the Queen. “Now, if you have a file called lunch.txt that contains only the text ONE KAT-MAN-BLEU BURGER, PLEASE, you can get to it like this!” She began typing:

>> file = File.open('lunch.txt', 'r')

=> #<File:lunch.txt>

>> file.read

=> "ONE KAT-MAN-BLEU BURGER, PLEASE\n"

“That’s exactly the same as if you had double-clicked on lunch.txt, only we can read the file’s text right inside Ruby! The \n at the end of PLEASE is Ruby’s way of representing ‘newline.’ If you open the file, it’ll just be the text ONE KAT-MAN-BLEU BURGER, PLEASE with a blank line under it.”

The Queen thought for a moment. “Let me explain a bit more. File.open tells Ruby to create a file object based on a file called lunch.txt.”

“What about the 'r'?” Ruben asked.

“That’s called a mode,” said the Queen, “and it tells Ruby what mode it should open the file in. 'r' means we’re just reading the file for now, not changing it.”

“Okay,” said Scarlet, “so we’ve got a file object stored in file. What does calling the read method do?”

“Exactly what you’d think!” said the Queen. “It reads the contents of the file and shows them to us.” She paused.

“Though usually, we open files with a block, like this.” She typed some more:

>> File.open('lunch.txt', 'r') { |file| file.read }

=> "ONE KAT-MAN-BLEU BURGER, PLEASE"

“Once again, we’ve got File.open, and we pass in the name of the file we want to open as a string, followed by a second string that tells us what mode to open the file in. In this case, we’ve used 'r' for ‘read.’”

“With you so far,” said the King.

“Instead of saving the file object to a file variable and calling read on it, like we did before,” the Queen continued, “we pass File.open a block. We pass file to the block instead and call file.read inside the block!”

“Is there a difference between opening a file with a block and opening a file without one?” Scarlet asked.

“A very important difference!” said the Queen. “When you open a file with a block, the file is closed as soon as the block is finished. But if you open a file without a block, it won’t automatically close. See?” She typed:

>> file = File.open('lunch.txt', 'r')

=> #<File:lunch.txt>

>> file.closed?

=> false

“How do you close a file if you didn’t open it with a block?” Ruben asked.

“By using the close method, like this!” the Queen said, typing:

>> file = File.open('lunch.txt', 'r')

=> #<File:lunch.txt>

>> file.read

=> "ONE KAT-MAN-BLEU BURGER, PLEASE"

>> file.close

=> nil

“That seems easy enough,” said the King, “but why do we need to close files in the first place?”

“Ruby keeps track of all the files we open, and the computer we’re running Ruby on will only let us open a certain number of files at a time,” the Queen explained. “If we try to open too many without closing them, we could make the computer crash!”

“Sweet kite-flying porcupines!” said the King. “We certainly wouldn’t want that.”

“Also, if you don’t close a file,” the Queen continued, “Ruby won’t know you’re done with it, and unexpected things can happen later if you try to use a file you haven’t properly closed. You might even delete everything in it by accident!”

“Okay, we’ll make sure to close any files we open,” Ruben said. “It sounds like opening a file with a block is the easiest way to do that.”

“What else can we pass into the open method besides 'r'?” asked the King, scratching under his tiny crown. “Can we do things besides just read files?”

Writing and Adding to Files

“Of course, dear,” the Queen said. “You see, Ruby does exactly what you tell it, which means you must be very exact when you tell it to do anything. When you open a file, the first argument you give the open method is the filename, and the second one tells Ruby what it should expect to do with the file. You can do a lot with open—for instance, open 'r' tells Ruby to open a file but only to read from it, starting from the beginning of the file.”

“What are some of the other modes?” Scarlet asked.

“Well, you can use open 'w' to write to a file,” the Queen said. “Using the 'w' mode will tell Ruby to create a new file with the name you give it, or it will completely overwrite any file that already has that name.”

“Overwrite!” said Scarlet. “You mean it will replace everything in the existing file with whatever text you give it?”

“That’s right,” said the Queen.

“What if you want to add to an existing file?” asked Ruben.

“For that, you can use the 'a' mode,” the Queen said. “That still tells Ruby to create a brand-new file with the name you give it if that file doesn’t already exist, but if that file does exist, Ruby will start writing at the end of the file, so you won’t lose anything that’s already there.”

“Reading, writing, and adding,” said Scarlet. “I think that’s everything we want to do. But what happens if you use a mode that tells Ruby you’re going to do one thing, but then you try to do something else?” she asked.

“I’ll show you!” said the Queen. She typed into the Computing Contraption:

>> file = File.open('lunch.txt', 'w')

=> #<File:lunch.txt>

>> file.read

IOError: not opened for reading

“An error!” said Ruben. “We’ll have to be careful to use the right modes when we open files, then.”

“Precisely,” said the Queen. “Remember: Ruby does exactly what you tell it. If you use the 'w' mode to tell Ruby you’re opening a file only for writing, then try to read from the file instead, Ruby will get confused and produce an error.”

“What if you want to read and write to a file?” asked the King, who was busy inspecting a puff of pink lint he’d found stuck to his beard.

“Then we need to pass a slightly different mode to File.open,” the Queen said. She turned to Rusty. “What’s today’s cafeteria special?” she asked.

“Grilled cheese!” said Rusty. The Queen nodded and typed into the Computing Contraption:

>> file = File.open('lunch.txt', 'w+')

=> #<File:lunch.txt>

>> file.puts('THE MELTIEST OF GRILLED CHEESES')

=> nil

“Wow, what was that?” said Ruben. “I didn’t know you could use puts to write to a file!”

“Yes, you can,” said the Queen. “The only difference between puts and write is that puts adds an extra blank line after whatever you type, which Ruby represents with an \n (remember, that stands for ‘newline’). If you open the file, it’ll just be the text THE MELTIEST OF GRILLED CHEESESwith a blank line under it!”

“Now, we’ll try to read the lunch text back,” said the Queen, “but take a look at what happens the first time we try!”

>> file.read

=> ""

>> file.rewind

=> 0

>> file.read

=> "THE MELTIEST OF GRILLED CHEESES\n"

“Whoa!” said Scarlet. “We got nothing but an empty string the first time we called file.read, but after you called file.rewind, we could read the text in lunch.txt. What does rewind do?”

“Just like you can press REWIND on a remote control and send a movie back to the beginning, Ruby uses the rewind method to send you back to the beginning of a file. If you don’t rewind and then you try to read right after you’ve written to the file, you’ll just get an empty string!” replied the Queen.

“Like trying to press PLAY when you’re already at the end of a movie!” said Ruben.

“Precisely,” said the Queen.

“That all makes sense,” said Scarlet, “but we used the 'w+' mode, which means we overwrote the original lunch.txt file!”

“That we did,” said the Queen. “Let’s put it back! I’ll show you a couple of new tricks while we do.” She began typing:

>> file = File.open('lunch.txt', 'a+')

=> #<File:lunch.txt>

>> file.write('ONE KAT-MAN-BLEU BURGER, PLEASE')

=> 31

>> file.rewind

=> 0

>> file.readlines

=> ["THE MELTIEST OF GRILLED CHEESES\n", "ONE KAT-MAN-BLEU BURGER,

PLEASE"]

image with no caption

“First, we reopen lunch.txt for writing with File.open, using the 'a+' mode,” the Queen explained. “This tells Ruby we want to add our new text to the end of the file instead of replacing all the text that’s already there. Next, we call file.write and pass in the new text we want to add to the end of lunch.txt.”

“Why does Ruby return 31 when we call file.write?” Ruben asked.

“An excellent question!” said the Queen. “Ruby is telling us that it successfully added 31 characters to the end of lunch.txt.”

“I see,” said Ruben. “So the 'a+' mode must mean that we add to the file—so we don’t get rid of what’s already there—and the + part means we can add to and read the file!”

“Correct!” said the Queen. “You’ll also see that since adding the text puts us all the way at the end of the file, we call file.rewind to ‘rewind’ our position to the very beginning. That’s why file.rewind returns 0: we’re at the very start of the file!”

“But what does that readlines method do?” Ruben asked. “Does it just give us back an array of lines of text from the file?”

“Right again,” said the Queen. “Because I used puts to add the first line, ONE KAT-MAN-BLEU BURGER, PLEASE was added on its own line. The readlines method just goes through and creates an array from the file, where each item in the array is a single line of text. So we have an array with two elements here.”

“Astounding!” said the King, peering over his wife’s shoulder.

“Isn’t it?” she asked. “There’s also a readline method, which just gives us back one line at a time. See?” She typed some more:

>> file.rewind

=> 0

>> file.readline

=> "THE MELTIEST OF GRILLED CHEESES\n"

>> file.readline

=> "ONE KAT-MAN-BLEU BURGER, PLEASE"

“We can even use readlines with each to print out all the lines at once!” the Queen said, typing even more quickly:

>> file.rewind

=> 0

>> file.readlines.each { |line| puts line }

THE MELTIEST OF GRILLED CHEESES

ONE KAT-MAN-BLEU BURGER, PLEASE

=> ["THE MELTIEST OF GRILLED CHEESES\n", "ONE KAT-MAN-BLEU BURGER,

PLEASE"]

“That’s amazing!” said Ruben.

Avoiding Errors While Working with Files

“I think I’m starting to understand file input and output now. But what happens if I try to use a file that doesn’t exist?” Ruben asked as he reached over to the Computing Contraption’s keyboard and typed:

>> File.open('imaginary.txt', 'r')

Errno::ENOENT: No such file or directory - imaginary.txt

“An error!” Scarlet said. “That makes sense. Is there any way to find out if a file exists before we try to use it?”

“Good question!” said the Queen. “If we’re not sure whether a file exists, we can use Ruby’s built-in File.exist? method to check.” She typed:

>> File.exist? 'lunch.txt'

=> true

>> File.exist? 'imaginary.txt'

=> false

“Wonderful, wonderful!” said the King, clapping his hands together. “With all these magnificent Ruby tools, I have no doubt we can capture these crooks quite quickly.”

“You’re right!” said the Queen. She turned to Rusty. “Is there anything in the Ruby program that represents all the loading docks?” she asked.

Rusty nodded. “There’s an array, loading_docks, which is an array of files. Each file represents a loading dock door, so if you open and read all the files, all the doors should open!”

The Queen thought for a moment, her fingers hovering above the keyboard. Then she typed into the Computing Contraption:

loading_docks.each do |dock|

current_dock = File.open(dock, 'r')

puts current_dock.read

current_dock.close

end

One by one, the doors to each loading dock rolled open, hung ajar for a moment, then slid shut. Descriptions of each dock’s contents began to fill the Computing Contraption’s screen.

“Ruby code . . . Ruby code . . . shipment of Key-a-ma-Jiggers . . . there!” shouted Rusty, pointing to a door in the center of the far wall.

Four shadowy figures leapt from the loading dock near the lower-left corner of the wall just as the doors began to slide shut again.

image with no caption

“Freeze!” shouted the King. “We’ve got you surrounded!”

The four figures moved with surprising speed, knocking over several Refactory workers as they tried to make their way to the nearest exit.

“Stop them!” Rusty yelled as the five of them ran down the metal walkway to the loading dock floor.

Several Refactory workers struggled with the intruders, but they were too fast and too slippery. In just a few seconds, they’d made it all the way to the exit!

“Make way, make way!” cried the Queen, and the five of them reached the Refactory exit just as the shadowy villains escaped through the door. Without breaking stride, the King, the Queen, Ruby, Scarlet, and Rusty barreled through the doorway and into the narrow corridor leading back the way they’d come in.

“Are they headed for the freight elevator?” Ruben panted as they ran.

“Much worse!” Rusty said. “They’re headed straight for the WEBrick road!”

The King and Queen gasped together. “The WEBrick road!” said the Queen. “That leads straight out of the kingdom! If they get out through the kingdom gates, we’ll never catch them!”

“Then we’ll just have to be sure that doesn’t happen,” Rusty said. He turned and called over his shoulder: “Everyone, after them!” And with that, every single person in the Refactory ran toward the small bright exit sign, with the King, the Queen, Scarlet, Ruben, and Rusty leading the pack.

All Loading Docks, Report for Duty!

We’ve nearly caught our crooks red-handed! Oh man, the suspense is killing me. Who are they? Will the King, the Queen, Ruben, Scarlet, and Rusty catch them in time? What’s on the Refactory cafeteria lunch menu for tomorrow? Questions worth pondering until the end of time, for sure—or at least, until the end of this chapter. In the meantime, let’s get in just a bit more practice reading from and writing to a file.

Let’s start out by making a new file called loading_docks.rb and typing the following code. This is a simple little program that will create a text file for each of our loading docks, write some text into it, and then read it back to us.

loading_docks.rb

def create_loading_docks(➌docks=3)

➊ loading_docks = []

➋ (1..docks).each do |number|

➍ file_name = "dock_#{number}.txt"

loading_docks << file_name

➎ file = File.open(file_name, 'w+')

file.write("Loading dock no. #{number}, reporting for duty!")

file.close

end

loading_docks

end

➏ def open_loading_docks(docks)

➐ docks.each do |dock|

file = File.open(dock, 'r')

puts file.read

file.close

end

end

➑ all_docks = create_loading_docks(5)

➒ open_loading_docks(all_docks)

While there are a few bits of code that are making appearances from earlier chapters, there’s nothing brand-new here for you to worry about. Let’s walk through the code line by line.

First, we set up an empty array called loading_docks ➊, which we’ll use to store the names of all the loading dock files we’ll create (so we can read them later). Next, we use the (1..docks) range to create as many loading docks as the create_loading_docks method requires ➋ (it defaults to 3 if no number is passed in ➌).

For each number in the range, we call a block that creates a file with that number (such as dock_1.txt) and adds that filename to the loading_docks array ➍. We then open the file, write a string of text into it, and close it ➎.

Finally, in the open_loading_docks method ➏, we simply take our array of loading dock names (it looks something like ["dock_1.txt", "dock_2.txt"...], and so on), and for each filename, we open the file for reading, read its contents, and close it ➐. So when we run this script with all_docks = create_loading_docks(5) ➑ and open_loading_docks(all_docks) ➒ at the bottom, we end up creating dock_1.txt through dock_5.txt, each of which has its individual number and the "reporting for duty!" string in it.

Pretty great, right?

As always, you can run the finished script by typing ruby loading_docks.rb at the command line. When you run it, you’ll see this:

Loading dock no. 1, reporting for duty!

Loading dock no. 2, reporting for duty!

Loading dock no. 3, reporting for duty!

Loading dock no. 4, reporting for duty!

Loading dock no. 5, reporting for duty!

If you look in the directory where you ran loading_docks.rb, you’ll also see a .txt file for each dock, containing the very text our script printed out!

But I’m sure your head is already spinning with ways to improve this humble little script. For instance, we could change the number of files we create from 5 to 1, 3, 10, or any other number we choose! Just be careful—creating too many files will not only fill up your folder, but it could even crash your computer. (That’s why we defaulted to 3 and only did 5 in the example.)

You probably noticed that we wrote to the files with the 'w+' mode, meaning that if we run the script again, it will overwrite the files with the new content. What if we want to add to the file instead, though? (Hint: The 'a+' mode might be involved.)

For that matter, what if we want to write something fancier than just a plain old text file? What if we want to write a file that writes another Ruby file? This is not only possible, but it’s a big part of what professional programmers do every day. Try to write a file with a small bit of Ruby in it—something as simple as puts 'Written by Ruby!'. (Make sure you write the file with .rb at the end instead of .txt so Ruby can run it.)

Finally, how might you work in some of the file methods we saw, like exist?, rewind, or puts? Are there other file methods in the Ruby documentation at http://ruby-doc.org/core-1.9.3/File.html that might be cool to use? Remember to ask your local adult before going online!

You Know This!

You can read! You can write! Well, okay, you already knew how to do those things, but now you know how to do them with Ruby. I don’t doubt that you’re a full-fledged Ruby sorcerer by now, but just to make sure there’s nothing unclear about this new Ruby wizardry we’ve covered, let’s take a second to review it.

You saw that Ruby can create, read, write, and understand files, which are exactly like the computer files you already know about: text documents, pictures, Ruby scripts, and more. Ruby can open a file that already exists with the open method:

>> file = File.open('alien_greeting.txt', 'r')

=> #<File:alien_greeting.txt>

It can read a file with the read method:

>> file.read

=> "GREETINGS HUMAN!"

And when we’re finished using a file, we should close it using the close method:

>> file.close

=> nil

It turns out we can accidentally crash our computer by keeping too many files open at once, so it’s always a good idea to close any file we’ve opened. Luckily, if we open a file with a block, Ruby automatically closes the file for us:

>> File.open('alien_greeting.txt', 'r') { |file| file.read }

=> "GREETINGS HUMAN!"

Ruby is pretty picky about being told what to do, so we have to use different modes to tell Ruby which input and output mode it should use. When we use 'r', we tell Ruby that we expect it only to read files, and when we use 'w', we tell it we expect it only to write files. To tell Ruby it should both read and write a file, we can give it the 'w+' mode:

>> new_file = File.new('brand_new.txt', 'w+')

=> #<File:brand_new.txt>

>> new_file.write("I'm a brand-new file!")

=> 21

>> new_file.close

=> nil

>> File.open('brand_new.txt', 'r') { |file| file.read }

=> "I'm a brand-new file!"

You found out that 'w+' will overwrite a file—that is, it will replace everything in the existing file with whatever string we tell Ruby to put in there. If we just want to add to a file instead of replacing it completely, we can use the 'a' mode ('a+' if we want to add to the file and read from it):

>> file = File.open('breakfast.txt', 'a+')

=> #<File:breakfast.txt>

>> file.write('Chunky ')

=> 7

>> file.write('bacon!')

=> 6

>> file.rewind

=> 0

>> file.read

=> "Chunky bacon!"

Speaking of our friend rewind, you saw we could use it to back up to the start of the file and read the whole file:

>> file = File.open('dinner.txt', 'a+')

=> #<File:dinner.txt>

>> file.write('A festive ham!')

=> 14

>> file.read

=> ""

>> file.rewind

=> 0

>> file.read

=> "A festive ham!"

In that first file.read, the string is empty because we’re at the end of the file. After we rewind, though, we go back to the start, and when we file.read again, our text is there.

You discovered that if we want to add a blank line after a line of text, we can use a file’s puts method instead of write. When we read the file back, Ruby shows us the blank line as a backslash and the letter n (\n):

>> file.puts('A sprig of fresh parsley!')

=> nil

>> file.rewind

=> 0

>> file.read

=> "A festive ham!A sprig of fresh parsley!\n"

In fact, you saw that we could use the readline and readlines methods to read out lines of a file one by one. readline reads one line from the file at a time, and calling it a bunch of times reads each line, one after another:

>> file = File.new('dessert.txt', 'a+')

=> #<File:dessert.txt>

>> file.puts('A gooseberry pie')

=> nil

>> file.puts('A small sack of muffins')

=> nil

>> file.rewind

=> 0

>> file.readline

=> "A gooseberry pie\n"

>> file.readline

=> "A small sack of muffins\n"

If we want to read the lines of our file all at once, we can use file.readlines with a call to the each method and a block:

>> file.rewind

=> 0

>> file.readlines.each { |line| puts line }

A gooseberry pie

A small sack of muffins

=> ["A gooseberry pie\n", "A small sack of muffins\n"]

Finally, you saw that we could check whether a file exists by using the exist? method:

>> File.exist? 'breakfast.txt'

=> true

>> File.exist? 'fancy_snack.txt'

=> false

Files and file input/output probably don’t seem like a big deal to you now (especially since you know a lot about how they work), but they’re a major part of how computers get work done. Don’t hesitate to mess around with creating and changing your files on your computer, and—with permission—hunt around the Internet for more information on files, how they work, and any interesting bits of Ruby code you can run to improve your understanding. But enough out of me: our heroes are hot on the tails of the tricksters who have been mucking things up in the kingdom all day, and we’re about to find out who they are, what they want, and whether the King, the Queen, Ruben, Scarlet, and the crew of the Refactory can stop them once and for all!