We’re bypassing rbenv-init because we already covered that earlier, and skipping ahead to rbenv-local. As per usual, let’s look at the test file first.

Tests

After the bats shebang and call to test_loader, the first block of code is:

setup() {
  mkdir -p "${RBENV_TEST_DIR}/myproject"
  cd "${RBENV_TEST_DIR}/myproject"
}

Pretty straightforward, just makes a directory for a fake test project and cds into it. We can assume that we’ll need this fake project directory in order to set its Ruby version.

This setup() function is called by the bats testing framework, inside the bats_test_begin() function of the bats-exec-test file.

Getting the version

The next few tests relate to fetching an existing version. Later tests will relate to setting a new version number.

When no version exists

Our first test for this command:

@test "no version" {
  assert [ ! -e "${PWD}/.ruby-version" ]
  run rbenv-local
  assert_failure "rbenv: no local version configured for this directory"
}

I don’t remember if I’ve seen the -e flag in a [ ... ] call before, so I look it up:

-e file       True if file exists (regardless of type).

So as a sanity check, this test first asserts that a file named .ruby-version does not currently exist in the current directory. It then runs the rbenv local command, and asserts that the command fails with a non-zero exit status and a helpful error message.

When a local version exists in the current dir

Next test:

@test "local version" {
  echo "1.2.3" > .ruby-version
  run rbenv-local
  assert_success "1.2.3"
}

Here we create a .ruby-version file and set its contents equal to 1.2.3. We then run the rbenv local command without any arguments and assert that:

  • the command exited successfully, and
  • its printed output was the contents of the file we just created.

When a local version exists in the parent dir

Next test:

@test "discovers version file in parent directory" {
  echo "1.2.3" > .ruby-version
  mkdir -p "subdir" && cd "subdir"
  run rbenv-local
  assert_success "1.2.3"
}

Here we create that same .ruby-version file in one directory, then we make (and navigate to) a subdirectory. We run the rbenv local command and assert that, even though no .ruby-version file is found in this sub-directory, the command is smart enough to recursively check parent directories until it finds such a file.

Preferring the .ruby-version file over the RBENV_DIR env var

Next test:

@test "ignores RBENV_DIR" {
  echo "1.2.3" > .ruby-version
  mkdir -p "$HOME"
  echo "2.0-home" > "${HOME}/.ruby-version"
  RBENV_DIR="$HOME" run rbenv-local
  assert_success "1.2.3"
}

Here we see that two .ruby-version files are created- the 1st one in the current directory, and the 2nd one in a special $HOME directory. We then set the RBENV_DIR env var equal to the parent directory of that 2nd .ruby-version file and run the rbenv local command. We expect that command to ignore this environment variable, and instead use the current directory’s Ruby version file.

Setting the local Ruby version

The next few tests relate to setting (not getting) the local Ruby version.

Setting a previously-installed version

First setter test:

@test "sets local version" {
  mkdir -p "${RBENV_ROOT}/versions/1.2.3"
  run rbenv-local 1.2.3
  assert_success ""
  assert [ "$(cat .ruby-version)" = "1.2.3" ]
}

Here we create a directory representing the location where RBENV installs its Ruby versions, and create a sub-directory called 1.2.3/. This is our way of mocking out a real RBENV installation of Ruby v1.2.3.

We then run rbenv local with an argument which corresponds to the version of Ruby that we just “installed”. We then assert that the test was successful and that the contents of the (newly-created) .ruby-version file are the same as the argument we provided to rbenv local.

The reason we had to create that fake directory in step 1 is because we don’t actually want RBENV to make a network call, pull down a real Ruby install, and set it up on our machine. That would make the test take forever and could even result in a flaky test if (for example) the network was down.

Updating an existing .ruby-version file to a new version number

Next test:

@test "changes local version" {
  echo "1.0-pre" > .ruby-version
  mkdir -p "${RBENV_ROOT}/versions/1.2.3"
  run rbenv-local
  assert_success "1.0-pre"
  run rbenv-local 1.2.3
  assert_success ""
  assert [ "$(cat .ruby-version)" = "1.2.3" ]
}

Here we create a .ruby-version file with one version of Ruby (1.0-pre), and create a fake “installed” version with a different version number (1.2.3). As a sanity check, we run rbenv local once to confirm that it outputs the first version number.

We then run rbenv local a 2nd time, and this time we pass it the 2nd version number. We then assert that the command exited successfully and that our the contents of our .ruby-version file changed from the 1st Ruby version to the 2nd one.

Unsetting the Ruby version

Last test:

@test "unsets local version" {
  touch .ruby-version
  run rbenv-local --unset
  assert_success ""
  assert [ ! -e .ruby-version ]
}

Here we create an empty .ruby-version file. The file doesn’t need any contents because, in step 2, we run rbenv local –unset. This should have the effect of deleting that version file. We then assert that the command exited successfully and that the version file no longer exists.

A pretty straightforward series of specs. Now on to the command itself.

Code

The first block of code is:

#!/usr/bin/env bash
#
# Summary: Set or show the local application-specific Ruby version
#
# Usage: rbenv local <version>
#        rbenv local --unset
#
# Sets the local application-specific Ruby version by writing the
# version name to a file named `.ruby-version'.
#
# When you run a Ruby command, rbenv will look for a `.ruby-version'
# file in the current directory and each parent directory. If no such
# file is found in the tree, rbenv will use the global Ruby version
# specified with `rbenv global'. A version specified with the
# `RBENV_VERSION' environment variable takes precedence over local
# and global versions.
#
# <version> should be a string matching a Ruby version known to rbenv.
# The special version string `system' will use your default system Ruby.
# Run `rbenv versions' for a list of available Ruby versions.

set -e
[ -n "$RBENV_DEBUG" ] && set -x

Same deal as always:

  • The Bash shebang
  • The usage + summary comments
  • The call to set -e, which tells the shell to exit immediately upon the first error it encounters
  • When the RBENV_DEBUG env var is present, call set -x to tell the shell to run in verbose mode.

Completions

Next block of code:

# Provide rbenv completions
if [ "$1" = "--complete" ]; then
  echo --unset
  echo system
  exec rbenv-versions --bare
fi

These are the completions that get picked up and printed out when the user runs rbenv local –complete. We echo --unset and system, and then print the output of exec rbenv-versions –bare. In my case, that output is:

2.7.5
3.0.0
3.1.0

That’s because these are the 3 versions of Ruby that I’ve installed via rbenv.

Last block of code:

RBENV_VERSION="$1"

if [ "$RBENV_VERSION" = "--unset" ]; then
  rm -f .ruby-version
elif [ -n "$RBENV_VERSION" ]; then
  rbenv-version-file-write .ruby-version "$RBENV_VERSION"
else
  if version_file="$(rbenv-version-file "$PWD")"; then
    rbenv-version-file-read "$version_file"
  else
    echo "rbenv: no local version configured for this directory" >&2
    exit 1
  fi
fi

Let’s break this up into smaller chunks.

Setting a new Ruby version

RBENV_VERSION="$1"

if [ "$RBENV_VERSION" = "--unset" ]; then
  rm -f .ruby-version

We create a variable named RBENV_VERSION and set it equal to the first argument passed to rbenv local. If that argument was --unset, then we delete the file named .ruby-version if it exists.

elif [ -n "$RBENV_VERSION" ]; then
  rbenv-version-file-write .ruby-version "$RBENV_VERSION"

Otherwise, if the argument was any other value, then we pass that value to the program rbenv-version-file-write. Based on a quick experiment, that program appears to check whether the argument it receives is equal to a version number for an installed version of Ruby. If it’s not, an error is returned:

$ rbenv local foobarbaz

rbenv: version `foobarbaz' not installed

Reading the existing Ruby version

else
  if version_file="$(rbenv-version-file "$PWD")"; then
    rbenv-version-file-read "$version_file"

If we reach the else block, that means there was no first argument, i.e. the user just typed rbenv local. In this case, the expectation is that the user wants to know what version of Ruby has been set by RBENV for the current directory or project.

In this case, we check whether we have a file containing a Ruby version number, using the rbenv-version-file command. I try this command out in a directory without a .ruby-version file, and I see the following:

$ rbenv version-file

/Users/myusername/.rbenv/version

I then make a .ruby-version file in that same directory, and re-run it:

$ echo "2.0.0" > .ruby-version
$ rbenv version-file

/Users/myusername/.rbenv/.ruby-version

If we do have a version file somewhere, we run rbenv version-file-read and pass the name of that version file as an argument. Again trying this command out on my machine, I see:

$ rbenv version-file-read .ruby-version

2.0.0

Last block of code:

  else
    echo "rbenv: no local version configured for this directory" >&2
    exit 1
  fi
fi

If we reach this block, we’re trying to read a Ruby version but RBENV doesn’t have a source from which to read a Ruby version. So it prints out a helpful error message to stderr and exits with a non-zero return status.

That’s it for this command. On to the next one.