Setting the Correct Ruby Version

Next 2 lines of code are:

export RBENV_DIR="${arg%/*}"
break

If our case statement matches, and if our argument corresponds to a filepath with a "/" inside it, we set the RBENV_DIR environment variable. We then break out of the for loop we're in, implying our shim doesn't need to process any more command line arguments. But what does RBENV_DIR do?

Searching for usages of RBENV_DIR

To answer this question, I search for it in the RBENV codebase, which (on my machine) is located at ~/.rbenv/, because I installed RBENV from source, not via Homebrew.

Note that I performed the search below using the ag command, which you can learn how to install here. Your computer will likely ship with the grep command, but ag is much faster for searching codebases.

When I run this search, I see multiple references to it in various files:

$ ag RBENV_DIR
test/test_helper.bash
2:unset RBENV_DIR

test/local.bats
29:@test "ignores RBENV_DIR" {
33:  RBENV_DIR="$HOME" run rbenv-local

test/rbenv.bats
29:@test "default RBENV_DIR" {
30:  run rbenv echo RBENV_DIR
34:@test "inherited RBENV_DIR" {
37:  RBENV_DIR="$dir" run rbenv echo RBENV_DIR
41:@test "invalid RBENV_DIR" {
44:  RBENV_DIR="$dir" run rbenv echo RBENV_DIR

test/version-file.bats
50:@test "RBENV_DIR has precedence over PWD" {
54:  RBENV_DIR="${RBENV_TEST_DIR}/widget" run rbenv-version-file
58:@test "PWD is searched if RBENV_DIR yields no results" {
62:  RBENV_DIR="${RBENV_TEST_DIR}/widget/blank" run rbenv-version-file

libexec/rbenv-version-file
25:  find_local_version_file "$RBENV_DIR" || {
26:    [ "$RBENV_DIR" != "$PWD" ] && find_local_version_file "$PWD"

libexec/rbenv-rehash
71:        export RBENV_DIR="\${arg%/*}"

libexec/rbenv
61:if [ -z "${RBENV_DIR}" ]; then
62:  RBENV_DIR="$PWD"
64:  [[ $RBENV_DIR == /* ]] || RBENV_DIR="$PWD/$RBENV_DIR"
65:  cd "$RBENV_DIR" 2>/dev/null || abort "cannot change working directory to \`$RBENV_DIR'"
66:  RBENV_DIR="$PWD"
69:export RBENV_DIR

README.md
515:`RBENV_DIR` | `$PWD` | Directory to start searching for `.ruby-version` files.
$ 

The reference that catches my eye is the one at the bottom, in the README.md file. This file will likely tell us in plain English what we want to know.

Sure enough, we find that it contains the following table:

name default description
... ... ...
RBENV_DIR $PWD Directory to start searching for .ruby-version files.

Again from reading the README file, we see that the .ruby-version file is one way that RBENV uses to detect which Ruby version you want to use:

"...rbenv scans the current project directory for a file named .ruby-version. If found, that file determines the version of Ruby that should be used within that directory."

So here we're setting the RBENV_DIR variable, in order to tell RBENV which version of Ruby to use.

But what is the export keyword at the start of export RBENV_DIR="${arg%/*}"?

export statements

We've already seen an example of how variables are assigned in Bash, i.e. program="${0##*/}". An assignment statement like export FOO='bar' is similar, in that creates a variable named FOO and sets its value to "bar", but the use of export means it's doing something else as well.

What does export FOO='bar' do that FOO='bar' doesn't do?

It turns out there are two kinds of variables in a Bash script:

  • shell variables
  • environment variables

Adding export in front of an assignment statement is what transforms a shell variable assignment into an environment variable assignment.

The difference between the two is that shell variables are only accessible from within the shell they're created in. Environment variables, on the other hand, are also accessible from within child shells created by the parent shell.

This blog post gives two examples, one demonstrating access of an environment variable from a child shell, and the other of (attempting to) access a shell variable from a child shell. To see this for ourselves, we can do an experiment mimicking these examples in our terminal.

Experiment- environment vs shell variables

We can type the following directly in our terminal:

$ export MYVAR1="Here is my environment variable"
$ MYVAR2="Here is my shell variable"
$ echo $MYVAR1
Here is my environment variable
$ echo $MYVAR2
Here is my shell variable
$ 

So far, so good. Both the shell variable and the environment variable printed successfully.

Now we open up a new shell from within our current terminal tab, and try again:

$ bash

The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.

bash-3.2$ echo $MYVAR1

Here is my environment variable

bash-3.2$ echo $MYVAR2

bash-3.2$ 

We can see here that MYVAR1 is visible from within our new child shell, but MYVAR2 is not. That's because the declaration of MYVAR1 was prefaced with export, while the declaration of MYVAR2 was not.

So our current line of code creates an environment variable called RBENV_DIR, which will be available in child shells. This implies that we'll be creating a child shell soon. What will that child shell do?

We'll need quite a few more chapters to fully explain the answer, but the short answer is that this shim will launch the rbenv command inside a child shell, wherein the environment variable RBENV_DIR (which we just set above) will be used to detect which Ruby version is the right one. Then we execute the original command corresponding to the shim that's being executed (i.e. bundle or whatever).

Setting the RBENV_DIR variable

In the meantime, what do the contents of the RBENV_DIR variable look like? To answer that, we have to know what "${arg%/*}" resolves to, in this line of code. It looks like more parameter expansion, similar to the kind we used here to store the program name in the program shell variable. But the %/* syntax looks new, so let's run an experiment to find out what it does.

Experiment- a simpler version of RBENV's parameter expansion

I replace my foo script with the following:

#!/usr/bin/env bash

myArg="/foo/bar/baz"
bar="${myArg%/*}"
echo $bar

When I run the script, I get:

$ ./foo
/foo/bar
$

So ${arg%/*} takes the argument, and trims off the last / character and everything after it.

This aligns with what we see if we look up the GNU docs:

${parameter##word}

The word is expanded to produce a pattern and matched according to the rules described below (see Pattern Matching). If the pattern matches the beginning of the expanded value of parameter, then the result of the expansion is the expanded value of parameter with the shortest matching pattern (the # case) or the longest matching pattern (the ## case) deleted.

We now know enough to piece together what this line of code is doing. We create a new environment variable named RBENV_DIR which will be available in any child shells. Then, we take the parent directory of the filepath which was passed to the ruby command, and set RBENV_DIR equal to that parent directory.

A quick note- single- and double-quotes

When working with both shell and environment variables, it's a good idea to wrap them in double (not single) quotation marks. That's because sometimes the value stored in a variable can contain spaces.

If no quotation marks are used, the shell may treat the multiple words inside the variable's value as multiple arguments, instead of a single argument. And if single-quotes are used, the shell will treat $FOO as the string "$FOO" instead of as a variable containing a value.

More info at this StackOverflow post.

Summarizing the if-block

Let's summarize what we've learned about the if block inside the shim:

if [ "$program" = "ruby" ]; then
  for arg; do
    case "$arg" in
    -e* | -- ) break ;;
    */* )
      if [ -f "$arg" ]; then
        export RBENV_DIR="${arg%/*}"
        break
      fi
      ;;
    esac
  done
fi

Putting together everything we've learned:

RBENV first checks whether the command you're running is the ruby command. If it's not, we skip the for loop entirely.

If it is the ruby command, RBENV will iterate over each of the arguments you passed to ruby, checking its value. If the arg is -- or if it starts with -e, it will immediately stop checking the remaining args, and proceed to running the code outside the case statement (which we'll review next).

If the argument contains a "/" character, RBENV will check to see if that argument corresponds to a valid filepath. If it does, the shim will store the file's parent directory in an environment variable called RBENV_DIR.

At some future place in the code, RBENV will use this environment variable to decide which Ruby version to use.

Setting RBENV_ROOT

The next line of code is pretty straight-forward, so we'll quickly knock it out before moving to the final line of code in the shim:

export RBENV_ROOT="/Users/richiethomas/.rbenv"

This line of code just sets a 2nd environment variable named RBENV_ROOT.

Referring back to the README.md file we just read, we see that this variable "Defines the directory under which Ruby versions and shims reside." Given we're exporting the variable (i.e. given that this is an environment variable and not a shell variable), we can assume that this variable will be used by a child process, just like RBENV_DIR is.

In my case, the value to which this variable gets set is the .rbenv hidden directory inside my home directory, i.e. /Users/richiethomas/.rbenv.

Wrapping Up

We only have one more line of code to go before we're done with our line-by-line examination of the shim, so let's try to power through to the end of the file. Once we're done, we can start putting together what the shim as a whole does.