Case 2: When the argument is a filepath

Next block of code:

    */* )

This is clause #2 of our case statement, with */* as the pattern that the case statement will match against.

One difference I notice is that this is not the same as a "catch-all" default case, because the */* ) pattern here doesn't exactly match the * ) pattern in the example code we read earlier.

Instead, it looks like this pattern searches for a forward-slash, surrounded on either side with zero or more characters. The one thing I can think of which would match that pattern is a file path. Let's check that with an experiment.

Experiment: matching the */* pattern

Let's make a Bash script which looks like this:

#!/usr/bin/env bash

for arg; do
  case "$arg" in
    */* )
      echo "match: $arg";
      ;;
    * )
      echo "not a match: $arg";
      ;;
  esac
done

When I run the above with a few different arguments, I get the following results:

$ ./foo 1 2 3 a/b /a b/ / ''
not a match: 1
not a match: 2
not a match: 3
match: a/b
match: /a
match: b/
match: /
not a match: 
$ 

I sent my ./foo script the following arguments:

  • 3 arguments with no forward slashes at all, just 3 random numbers ("1", 2", and "3").
  • 4 arguments containing a forward slash in various positions
  • an empty string, ''

When I run the script:

  • the first group of 3 patterns don't match
  • the second group of 4 patterns do match
  • the final empty string does not match

So yes, it appears to be looking for strings which match the / symbol with zero or more characters of text on either side.

But just because it looks like a valid filepath, doesn't mean it is one. So how do we know it's a file?

We'll answer that question next.

Detecting Filepaths

The next line of code is:

if [ -f "$arg" ]; then
  ...
fi

Running man test and searching for the -f string reveals the following:

$ man test | ag -- -f
  -f file       True if file exists and is a regular file.
$ 

It says that [ -f "$file" ] returns true "if file exists and is a regular file". In other words, our case statement matches if the arg could be a filepath, and we then verify that it actually is a filepath by using [ -f "$arg" ].

To test whether the -f flag behaves the way I think it does, we can update our foo script from earlier to look like the following:

#!/usr/bin/env bash

for arg; do
  case "$arg" in
    */* )
      echo "could be a filepath: $arg";
      if [ -f "$arg" ]; then
        echo "is definitely a filepath";
      else
        echo "turns out, not a filepath";
      fi
      ;;
    * )
      echo "not a match: $arg";
      ;;
  esac
  echo "-----"
done

Then I create an empty file named bar in my current directory:

$ touch bar
$ 

I do this so that I have a file in my directory that will return true for the test [ -f "./bar" ].

Lastly, I run the foo script with a few arguments, and I see the following:

$ ./foo 1 2 3 a/b /a b/ / '' ./bar foo
  not a match: 1
  -----
  not a match: 2
  -----
  not a match: 3
  -----
  could be a filepath: a/b
  turns out, not a filepath
  -----
  could be a filepath: /a
  turns out, not a filepath
  -----
  could be a filepath: b/
  turns out, not a filepath
  -----
  could be a filepath: /
  turns out, not a filepath
  -----
  not a match: 
  -----
  could be a filepath: ./bar
  is definitely a filepath
  -----
  not a match: foo
  -----
$ 

As expected, the arguments which are known to not match a file in my current directory (i.e. "a/b", "/b", "a/", and "/") result in the output "turns out, is not a filepath". My "./bar" argument, which is known to match a file, results in the output "is definitely a filepath".

Only matching certain filepaths

But wait, the last argument "foo" also matches a filepath, i.e. the ./foo script we're running!

The case statement doesn't recognize it as a match, because it doesn't have a "/" in its argument. But it is a file. Since this case statement closely matches the one in RBENV's shim, does that imply the RBENV shim is also skipping some filepaths? And if so, is that on purpose, or should it in fact be treating them as matches?

The short answer is that yes, we are skipping files which don't have a / in them. And that is intentional, because we can assume that files which don't have a / in them are in our current directory.

I'm skipping ahead a bit, but the purpose of this part of the case statement is to set an environment variable called RBENV_DIR. This environment variable controls where RBENV searches for a file called .ruby-version, which tells it which version of Ruby to use. We only want to set that environment variable from within the shim if the Ruby script we're running lives somewhere other than in our current directory.

So for example, let's assume we're running a script which is located inside our current directory, i.e. we're inside ~/foo/ and we want to run ruby ~/foo/buzz.rb. In that case, we assume that the .ruby-version file also lives in the current directory. So we don't need to look for .ruby-version from inside the shim, because the rbenv command (which we'll look at later) will handle that.

On the other hand, if we're currently in ~/foo/, but we're running ruby ~/bar/baz.rb, then we want RBENV to use the .ruby-version file inside ~/bar/, not the .ruby-version file inside ~/foo/.

Wrapping Up

Don't worry if you weren't able to put this together yourself, just based on the code we've read up to this point. I wasn't able to either, and it wasn't until I asked this question in the RBENV Github repo that this all came together for me.

Additionally, in some cases I've thought of questions which I haven't been able to answer in the moment. One important habit that I've had to develop while working on this project is capturing my open questions in a document, and setting them aside until I've read more code and have more knowledge of all the moving parts. Oftentimes, when I come back to those questions after some time, I'm able to answer them myself with the knowledge I've acquired since then.

Let's now move on to a topic that we touched on briefly above- initializing the RBENV_DIR variable.