How is this command used? Let’s check the comments at the top of the file.

“Usage” comments

# Usage: rbenv version-file-read <file>

We invoke it with a single argument, corresponding to a version file such as .ruby-version or ${RBENV_ROOT}/version:

$ rbenv version-file-read ~/.rbenv/version

2.7.5

$ rbenv version-file-read ./.ruby-version

3.0.0

Fun fact- you can also pass any arbitrary file that doesn’t contain a Ruby version, though this is not considered happy-path usage. You won’t get an error, but you also won’t get a Ruby version:

$ rbenv version-file-read README.md

#

$ rbenv version-file-read Gemfile

source

$ echo $?     # prints the exit code of the last command you ran

0

Let’s move on to the test file.

Tests

After the bats shebang and the loading of test_helper, the first block of code is:

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

This function makes a Ruby project directory and navigates into it. We’ve seen this setup function before in other test files. This is a special bats function, which gets called before each test case. You can also define a teardown hook method, which gets called after each test case, though that isn’t done in this specific test file.

Passing no argument

Next block of code:

@test "fails without arguments" {
  run rbenv-version-file-read
  assert_failure ""
}

Our first test covers the sad-path case where no arguments are provided to the command. In this case, we expect the command to fail with no error output.

We can replicate this in the bash terminal:

bash-3.2$ rbenv version-file-read

bash-3.2$ echo "$?"

1

When the argument is not a real file

Next test:

@test "fails for invalid file" {
  run rbenv-version-file-read "non-existent"
  assert_failure ""
}

This test passes the name of a non-existent file to the version-file-read command, and asserts that the command fails with no printed output.

When the argument is an empty file

Next test:

@test "fails for blank file" {
  echo > my-version
  run rbenv-version-file-read my-version
  assert_failure ""
}

We create an empty version file via the echo > <new_filename> command, and then we pass that filename to the version-file-read command. The file is empty, so the command fails.

Reading a Ruby version

Next test:

@test "reads simple version file" {
  cat > my-version <<<"1.9.3"
  run rbenv-version-file-read my-version
  assert_success "1.9.3"
}

This happy-path test begins by creating a valid version file named “my-version”, containing the string “1.9.3”. We then pass that filename to version-file-read, and assert that the command passes and “1.9.3” is the printed output.

When the version file has leading spaces

Next test:

@test "ignores leading spaces" {
  cat > my-version <<<"  1.9.3"
  run rbenv-version-file-read my-version
  assert_success "1.9.3"
}

This test is similar to the previous test, except the Ruby version string contained in the version file is prefixed with several space characters. The test asserts that the command is successful and that these extra spaces are trimmed off before the version is printed to STDOUT.

When the version file has more than one “word”

Next test:

@test "reads only the first word from file" {
  cat > my-version <<<"1.9.3-p194@tag 1.8.7 hi"
  run rbenv-version-file-read my-version
  assert_success "1.9.3-p194@tag"
}

This test creates a version file with a valid Ruby version (1.9.3-p194@tag), plus a 2nd version (1.8.7) and some random text (hi). We run the command with the name of the version file, and assert that only the first Ruby version is printed to the string. The 2nd valid version number and the random string are ignored.

When the version file has more than one line

Next test:

@test "loads only the first line in file" {
  cat > my-version <<IN
1.8.7 one
1.9.3 two
IN
  run rbenv-version-file-read my-version
  assert_success "1.8.7"
}

This test is similar to the last one, except this Ruby version file contains a multi-line string and random text after each valid Ruby version. Here we assert that the command is successful, and that all text after the Ruby version (including the subsequent lines) are trimmed off.

When the first line of the file is blank

Next test:

@test "ignores leading blank lines" {
  cat > my-version <<IN

1.9.3
IN
  run rbenv-version-file-read my-version
  assert_success "1.9.3"
}

This test again uses a multi-line heredoc, but this time the first line contains only a newline character. The test asserts that the command is successful and that the parser ignores this newline, and returns the correct version number anyway.

When the version file is missing a newline at the end

Next test:

@test "handles the file with no trailing newline" {
  echo -n "1.8.7" > my-version
  run rbenv-version-file-read my-version
  assert_success "1.8.7"
}

With this test, we pass the -n flag when echoing the expected version number to the new version file. This flag tells the shell to not append a trailing newline character to the file, which it usually would do. We then run the version-file-read command and assert that it was successful, and that the expected version number was sent to STDOUT.

When the version number ends with a carriage return

Next test:

@test "ignores carriage returns" {
  cat > my-version <<< $'1.9.3\r'
  run rbenv-version-file-read my-version
  assert_success "1.9.3"
}

This test asserts that the version-file-read command trims off trailing \r characters (aka carriage returns) before outputting the version number to STDOUT.

When the specified filepath includes directory traversal

Next test:

@test "prevents directory traversal" {
  cat > my-version <<<".."
  run rbenv-version-file-read my-version
  assert_failure "rbenv: invalid version in \`my-version'"

  cat > my-version <<<"../foo"
  run rbenv-version-file-read my-version
  assert_failure "rbenv: invalid version in \`my-version'"
}

Here we assert that strings which would normally cause directory traversal to happen (i.e. .. and ../foo) will trigger a failure in the version-file-read command.

This test is related to this issue in the Github history, which describes a security vulnerability reported by another contributor. It looks like an earlier version of RBENV included the possibility of using a version of Ruby other than that intended by the user, if a malicious person was somehow able to modify the victim’s RBENV version file.

When a version file includes path segments

Next and last spec:

@test "disallows path segments in version string" {
  cat > my-version <<<"foo/bar"
  run rbenv-version-file-read my-version
  assert_failure "rbenv: invalid version in \`my-version'"
}

This test was introduced in the same PR that introduced the previous test. It asserts that version-file-read takes steps to prevent strings which resemble directories (such as foo/bar) from being included in the version file that it reads. We create a version file with a string resembling a directory path as its contents, then pass that file to the command and assert that it fails with a helpful error message.

Onto the file itself.

Code

The usual first block of code:

set -e
[ -n "$RBENV_DEBUG" ] && set -x
  • Set “exit-on-error” mode
  • Set “verbose” mode when the user passes the RBENV_DEBUG variable

Storing the target file name

Next block of code:

VERSION_FILE="$1"

We store the first argument in a variable named VERSION_FILE.

Testing if we have a non-empty file

Next block of code:

if [ -s "$VERSION_FILE" ]; then
  ...
fi

First we test if the value stored in VERSION_FILE actually represents an existing file, and that file has some sort of content (this is what the -s flag does). If so, we execute the code inside the if block.

Reading the version number from the file

# Read the first word from the specified version file. Avoid reading it whole.
IFS="${IFS}"$'\r'

We modify the value of the internal field separator to be its original value, plus the carriage return. The "${IFS}"$'\r' syntax can be thought of as "${IFS}" plus $'\r'. The 2nd half of this syntax is the Bash way of expanding escape sequences, as illustrated by this StackOverflow answer. For example:

$ echo $'Name\tAge\nBob\t24\nMary\t36'

Name    Age
Bob     24
Mary    36

Next line of code:

read -n 1024 -d "" -r version _ <"$VERSION_FILE" || :

Let’s break this down into its pieces:

Reading the first 1024 characters

read -n 1024

Here we read up to the first 1024 characters of some input (source TBD).

According to help read:

If -n is supplied with a non-zero NCHARS argument, read returns after NCHARS characters have been read.

Setting the delimiter for the read operation

Next:

-d ""

According to this link:

With -d '' it sets the delimiter to '\0' and makes read read the whole input in one instance, and not just a single line. IFS=$'\n' sets newline (\n) as the separator for each value. __ is optional and gathers any extra input besides the first 3 lines.

We read the whole contents of the input source, not just the first line, and that the input which is read is then delimited according to the value of IFS that we set on the previous line.

Disabling backslash escaping

-r

Again according to help read:

If the -r option is given, this signifies `raw’ input, and backslash escaping is disabled.

So we ensure that the backslash character \ is treated literally and not as an escape character.

Storing the version number in a variable

version _

We store the first “word” we read (i.e. the version number) in the variable version, and any remaining words we read in a throwaway variable named _. The use of the underscore character is a convention (at least in Ruby) to indicate that the variable will not be used subsequently.

Specifying our input source

<"$VERSION_FILE"

We use the < character to redirect the contents of $VERSION_FILE to the input of the read command.

Gracefully handling any errors

|| :

Lastly, this StackOverflow article mentions that the intention of || : is to prevent the read command from erroring out, or returning a non-successful exit code.

So to summarize the entire line of code:

  • We take the first non-whitespace line from the input file.
  • We trim any whitespace from that line.
  • We trim anything after the IFS value (which now includes the carriage return character in addition to whatever its previous value was).
  • We store the remaining value as the version number, for use in subsequent lines of code.

Preventing directory traversal

Next block of code:

  if [ "$version" = ".." ] || [[ $version == */* ]]; then
    echo "rbenv: invalid version in \`$VERSION_FILE'" >&2

We check whether there is an attempt to traverse directories by using either .. or a string which resembles a path, i.e. a string including a forward-slash. If either of these conditions returns true, we echo an error message to STDERR.

Note that there is no exit inside this conditional branch, the way there is in the subsequent elif branch. We simply exit the if condition and continue to the next line of code, which happens to be exit 1 (see below).

Printing the version number

Next block of code:

  elif [ -n "$version" ]; then
    echo "$version"
    exit
  fi
fi

If there’s a non-empty string stored in $version, we simply echo that string and exit with a successful exit code.

Exiting if no version file was found

Last line of code in this file:

exit 1

We’d reach this line if one of the following things were true:

  • there was no value set for $version.
  • there was a value but it was set to something resembling directory traversal.

In either case, we exit with a non-success return code.

On to the next file.