In Progress
Unit 1, Lesson 1
In Progress

Episode #491 Special Variables Part 1: Process State

Sooner or later, you’ll run across Ruby’s “Perl-style” special variables: $!, $$, $_, $&, $:, and all their kin. They may look like a cat was dancing on the keyboard, but understanding them can sometimes be the key to understanding the function of other people’s code. And in some cases, they can be quite handy…

In this, the first episode of a series, you’ll meet a few of the more prominent members of the special variable family.

Video transcript & code

In today's episode, we're kicking off a miniseries about one of Ruby's quirkier little corners. We're going to be talking about Ruby's "special variables" - the global-looking variables with strange names, that are available in all Ruby programs and which have special significance for the Ruby interpreter.

Today we're going to introduce the concept of special variables, and talk about some of the more commonly used ones.

In Episode #490 we wrote this Ruby crash-logging code.

One of the things that stands out about this code is the heavy use of Perl-style special variables. I'm talking about variables like:

  • $!, which contains the current active exception.
  • $0, which is the executable name of the current program.
  • and $$, which holds the current process ID.
module CrashLogger
  def self.log_crash_info(error=$!)
    program_name = $0
    process_id   = $$
    timestamp    = Time.now.utc.strftime("%Y%m%d-%H%M%S")
    filename     = "crash-#{program_name}-#{process_id}-#{timestamp}.yml"

    error_info = {}
    error_info["error"]         = error
    error_info["stacktrace"]    = error.backtrace
    error_info["environment"]   = ENV.to_h

    File.write(filename, error_info.to_yaml)
    filename
  end
  # ...
end

I called these "Perl-style" variables, because Ruby inherits most of these one-character variable names from the Perl programming language. Perl in turn got several of these special variable names from shell languages, but it expanded on the theme considerably.

Let's take a look at a sampling of these funny little variables.

Specifically, let's examine some variables which reflect and control the current Ruby process' global state.

There's the name of the current executable, contained in $0.

$0                              # => "xmptmp-in4300V4R.rb"

We can demonstrate this a little better by writing a little program to a file called hello.rb, and then executing the script.

code = <<'EOF'
puts "Hello, my name is '#{$0}'"
EOF
File.write("hello.rb", code)
system("ruby hello.rb")

# >> Hello, my name is 'hello.rb'

There is a more readable alias, $PROGRAM_NAME

$0                              # => "xmptmp-in43008dY.rb"
$PROGRAM_NAME                   # => "xmptmp-in43008dY.rb"

Let's use that in our mini-script.

code = <<'EOF'
puts "Hello, my name is '#{$0}'"
puts "That's right, I said my name is '#{$PROGRAM_NAME}'"
EOF
File.write("hello.rb", code)
system("ruby hello.rb")

# >> Hello, my name is 'hello.rb'
# >> That's right, I said my name is 'hello.rb'

By the way, if you're wondering why the shorthand version of this variable is $0, it comes from the fact that traditionally, in C and other programming languages the current process name is found in position zero of the command-line arguments array.

#include <stdio.h>
int main(int argc, char *argv[]) {
    printf("Hello, my name is '%s'\n", argv[0]);
    return 0;
}

Ruby makes the useful design choice of putting only the arguments in the ARGV array, not the program name. This keeps us from having to remember to skip the first element when processing command-line arguments.

code = <<'EOF'
p ARGV
EOF
File.write("args.rb", code)
system("ruby args.rb a b c d")

# >> ["a", "b", "c", "d"]

But it does mean that that we need a separate variable for the program name. $0 is a mnemonic reference to the program name being the traditional "zeroth argument".

Speaking of the arguments array, Ruby also has a special short alias for that as well, although it's not widely used. It's the $* special variable.

code = <<'EOF'
p $*
EOF
File.write("args.rb", code)
system("ruby args.rb a b c d")

# >> ["a", "b", "c", "d"]

Along with program name and program arguments, it's often useful to know the current program's process ID. We get at that using the $$ variable. If you've done any shell scripting, this one may already be familiar to you.

$$                              # => 20564

Another useful process-related special variable is $?. This one gives us quick access to the exit status of the last subprocess executed.

`ruby hello.rb`                 # => "Hello, my name is 'hello.rb'\n"
$?                              # => #<Process::Status: pid 20496 exit 0>

If you're used to using $? in shell scripts, note that the Ruby version returns a Process::Status object instead of a simple integer.

Some other important process global state includes the $" variable, which contains the list of currently loaded Ruby libraries. This is aliased to the more readable $LOADED_FEATURES variable.

$".first(3)                     # => ["enumerator.so", "thread.rb", "rational.so"]
$LOADED_FEATURES.first(3)       # => ["enumerator.so", "thread.rb", "rational.so"]

Related to this one is the $: variable, otherwise known as $LOAD_PATH. Both of these refer to the list of directories Ruby will search when loading libraries.

$:.first(3)
# => ["C:/tools/ruby23/lib/ruby/gems/2.3.0/gems/seeing_is_believing-3.2.0/lib",
#     "C:/tools/ruby23/lib/ruby/gems/2.3.0/gems/seeing_is_believing-3.2.0/lib",
#     "C:/tools/ruby23/lib/ruby/gems/2.3.0/gems/did_you_mean-1.0.0/lib"]
$LOAD_PATH.first(3)
# => ["C:/tools/ruby23/lib/ruby/gems/2.3.0/gems/seeing_is_believing-3.2.0/lib",
#     "C:/tools/ruby23/lib/ruby/gems/2.3.0/gems/seeing_is_believing-3.2.0/lib",
#     "C:/tools/ruby23/lib/ruby/gems/2.3.0/gems/did_you_mean-1.0.0/lib"]

The load path variable is notable because unlike some of the other special variables, it's writable. This is how we add directories for Ruby to search for libraries.

$:.unshift(".")
$LOAD_PATH.first(3)
# => [".",
#     "C:/tools/ruby23/lib/ruby/gems/2.3.0/gems/seeing_is_believing-3.2.0/lib",
#     "C:/tools/ruby23/lib/ruby/gems/2.3.0/gems/seeing_is_believing-3.2.0/lib"]

Moving on from process-global variables, Ruby also provides some special variables for dealing with exceptions. The $! variable gives us a way to access the currently-active exception, if any. Related, there's also a $@ variable for the current exception stack trace. This is really just a shorthand for taking the current exception and asking it for its backtrace.

begin
  fail
rescue
  $!
  # => RuntimeError
  $@
  # => ["xmptmp-in4300FYE.rb:2:in `<main>'"]
  $!.backtrace
  # => ["xmptmp-in4300FYE.rb:2:in `<main>'"]
end

Unlike the process-global special variables we looked at earlier, these variables aren't really global even though they have the $ sigil. Instead, they are thread-local. Some of the other special variables we'll look at in a moment are even more constrained in their scope. They are both thread and method-local.

You can learn more about the differing scopes of these special variables in Episode #407. For right now, all you have to know is that each of these special variables has a sensible level of scoping for its role. Meaning that in any case that you might worry changing a pseudo-global variable might have non-local consequences, Ruby most likely scopes the variable so that such interference is not possible.

Today we've looked at some of Ruby's more well-known special variables. We've seen how some, but not all, have both short form and long form aliases. Love them or hate them, in order to read and understand Ruby code "in the wild" you need to be familiar with these short, mnemonically-named special variables.

In upcoming episodes, we'll talk about some other categories of special variables. We'll discover the practical reason all of these two-letter variable names were created in the first place. We'll establish some guidelines for using special variables. And we'll learn a way to use these variables without making your code unreadable.

But for now… happy hacking!

Responses