In Progress
Unit 1, Lesson 21
In Progress

Special Variables Part 2: I/O

What’s the point of Ruby having cryptic special variables with names like $_, $\, and $;? In part two of this series, you’ll learn how to modify Ruby’s I/O behavior at the command line, and discover the real reason Ruby has these funny little variables in the first place.

Video transcript & code

In the previous episode in this series, we introduced Ruby's Perl-style special variables. Today, we're going to meet another category of these variables. And for the first time, we'll talk about the practical reason for these concisely-named mnemonic variables to exist.

Ruby has a lengthy list of special variables focused on dealing with input and output. One of the most peculiar is the $_, or "last read line" variable.

$_

Here's a script that makes no sense at all. It loops on reading a string from standard input, but doesn't put it anywhere. Then, for no apparent reason, it upppercases the contents of the $_ variable. Before the end of the loop, it prints nothing.

while gets
  $_.upcase!
  print
end

It seems ridiculous, but let's run this program from the command line. When we type in some input and hit enter… we see our input echoed in all caps! We try a few more lines before sending an end of file signal to end the program.

ruby caps.rb
hello
HELLO
banana
BANANA
goodbye
GOODBYE

This script illustrates the $_, or "last read line" variable. The gets and readline methods implicitly store the last line they read into this variable. And the print method, when called without an argument, outputs the contents of this variable.

But why? What on earth could be the justification for this nonobvious behavior?

The fact is, this variable isn't intended for use in scripts at all. If we go to the command line and execute ruby with the -p flag, Ruby will assume the read-and-print loop we wrote earlier. Then all we need to do is add a -e flag, meaning execute, followed by the code to uppercase the last read line.

When we run this command, it behaves exactly like our last script. We can type in lines of text, and see them echoed back to us in all caps. All this works even though the only code we wrote was to modify a funny-looking variable.

$ ruby -p -e '$_.upcase!'
hello
HELLO
apple
APPLE
goodbye
GOODBYE

And now, at last, we start to see the reason for these funny little variables. Because Ruby, like Perl, was created not just to be a general-purpose programming language. It was also created to be an alternative to shell scripting for quickly getting powerful work done at the command line. And when we're working with one-liners at the console, we want to be able to write things concisely.

There are a lot of input and output related special variables, and I'm not going to go over more than a sampling. Some of them are just shorthands for variables you know by other name, like the ARGF stream and the $stdout stream.

$< == ARGF                      # => true
$> == $stdout                   # => true

Others control how Ruby reads and writes structured data. For instance, here's a CSV list of names and email addresses.

$ cat list.csv
Stephan D'Amore,christelle.heaney@example.com
Katherine Cremin Jr.,maryjane.buckridge@example.org
Theron Ortiz,paul@example.org
Neal Yundt,bella.bahringer@example.org
Laura Herzog,christian.dach@example.net
Mr. Adela Rice,carolyn@example.com
Simeon Pollich,rosamond_fadel@example.com
Daphnee Kling,brady@example.org
Ms. Mike Klein,modesto_bradtke@example.org
Devyn Bernhard V,garett.kozey@example.com

If we pipe this into Ruby with the -p option and an empty script, Ruby echoes the file unchanged.

$ ruby -p -e '' < list.csv
Stephan D'Amore,christelle.heaney@example.com
Katherine Cremin Jr.,maryjane.buckridge@example.org
Theron Ortiz,paul@example.org
Neal Yundt,bella.bahringer@example.org
Laura Herzog,christian.dach@example.net
Mr. Adela Rice,carolyn@example.com
Simeon Pollich,rosamond_fadel@example.com
Daphnee Kling,brady@example.org
Ms. Mike Klein,modesto_bradtke@example.org
Devyn Bernhard V,garett.kozey@example.com

If we add the -a option, Ruby also tries to automatically split the current input line into fields. This gives us a new special variable to play with: $F. Let's output just the first field.

$ ruby -p -a -e '$_ = $F[0];' < list.csv
StephanKatherineTheronNealLauraMr.SimeonDaphneeMs.Devyn

Well that's interesting. By default, Ruby splits on whitespace. But we can change this by setting the $; or "input field separator" variable. Let's set it to a comma. Notice that we set this inside a special BEGIN block. This is another little-used Ruby feature, inherited from the AWK programming language, which exists for use in command-line scripts.

Here's the thing: Since we're using the -p option, everything we execute with a -e option is going to be run inside the implicit read/print loop that Ruby sets up for us. But what if we need to do some one-time setup before the loop? Putting code in this special BEGIN block effectively "lifts" the code up "above" the implicit loop.

Let's run the command.

 ruby -p -a -e 'BEGIN { $; = "," }; $_ = $F[0];' < list.csv
Stephan D'AmoreKatherine Cremin Jr.Theron OrtizNeal YundtLaura HerzogMr. Adela RiceSimeon PollichDaphnee KlingMs. Mike KleinDevyn Bernhard V

This is… different, anyway. Right now Ruby is looping and printing out multiple "records", but it doesn't look that way because it's not separating the records with anything.

We can change the output record separator by setting another special variable: $\. We'll set it to a newline character.

And now we've extracted the "names" column from some CSV data, just by using special variables to customizing how Ruby interprets and emits records.

ruby -p -a -e 'BEGIN { $; = ","; $\ = "\n" }; $_ = $F[0]' < list.csv
Stephan D'Amore
Katherine Cremin Jr.
Theron Ortiz
Neal Yundt
Laura Herzog
Mr. Adela Rice
Simeon Pollich
Daphnee Kling
Ms. Mike Klein
Devyn Bernhard V

Of course, Ruby has a perfectly good CSV library for this sort of thing. But I think this is a good demonstration of the kind of one-off text munging that Ruby's perl-style special I/O variables were created for.

We've now encountered special variables related to the Ruby process state, and variables for controlling input and output. In the next episode in this series, we'll encounter a final category of special variable, and talk about some guidelines for using special variables. Until then… happy hacking!

Responses