Executing The User's Command

We've arrived at the final line of code in the shim:

exec "/Users/richiethomas/.rbenv/libexec/rbenv" exec "$program" "$@"

Confirming The Correct RBENV Version

For what it's worth, this line will look a bit different on your machine than it does on mine, since the username that appears in the filepath in this line ("richiethomas") will of course be different.

But the majority of it should look similar, assuming you're using the same version of RBENV that I am (1.2.0-16-gc4395e5). To make sure this is true, run the following in your Bash terminal:

bash-3.2$ rbenv --version
rbenv 1.2.0-16-gc4395e5
bash-3.2$ 

You should see rbenv 1.2.0-16-gc4395e5 as the output. If not, you may need to re-install RBENV from source (i.e. not from Homebrew or similar), as per the instructions here.

What command are we running?

This line of code starts by running a shell command called exec. We'll get to what it does in a minute, but first let's print out the last 2 arguments that we're passing ("$program" and "$@"), to get the big picture of what we're running.

In-between the export line and the exec line at the bottom of the shim, I add the following two echo statements:

export RBENV_ROOT="/Users/richiethomas/.rbenv"
echo "program: $program"
echo "args: $@"
exec "/Users/richiethomas/.rbenv/bin/rbenv" exec "$program" "$@"

Next, I run the following command:

$ bundle --version
program: bundle
args: --version
Bundler version 2.3.14
$ 

Since the program we're running is bundle and the single argument we're passing is --version, the full command we're running is:

exec /Users/richiethomas/.rbenv/libexec/rbenv exec bundle --version

Before moving on, I make sure to remove the 2 echo statements that I added to my bundle shim.

The exec Command

What does the exec command at the start of the line do? I first try man exec but I get the "General Commands Manual", indicating that this is a builtin command. I then log into a Bash shell and try help exec, where I see:

bash-3.2$ help exec
exec: exec [-cl] [-a name] file [redirection ...]
    Exec FILE, replacing this shell with the specified program.
    If FILE is not specified, the redirections take effect in this
    shell.  If the first argument is `-l', then place a dash in the
    zeroth arg passed to FILE, as login does.  If the `-c' option
    is supplied, FILE is executed with a null environment.  The `-a'
    option means to make set argv[0] of the executed process to NAME.
    If the file cannot be executed and the shell is not interactive,
    then the shell exits, unless the shell option `execfail' is set.
bash-3.2$ 

We're executing a file (i.e. a command), "replacing this shell with the specified program.". What does that mean?

I Google "what is exec in bash", and one of the first links I find is this one, from a site called ComputerHope:

Bash exec builtin command

On Unix-like operating systems, exec is a builtin command of the Bash shell. It lets you execute a command that completely replaces the current process. The current shell process is destroyed, and entirely replaced by the command you specify.

...

Description

exec is a critical function of any Unix-like operating system. Traditionally, the only way to create a new process in Unix is to fork it. The fork system call makes a copy of the forking program. The copy then uses exec to execute the child process in its memory space.

This raises a few new questions:

  • What's a "process"?
  • What's the difference between forking a process and replacing the current process with the new one (i.e. what exec does)?
  • Why use exec over forking, or vice-versa?

It was clear at this point that I needed some background info on processes before I could proceed further. The best resource I found was this page from the University of Cincinatti, which does a great job of explaining processes in beginner-friendly terms, and answers the first two questions above.

Rather than copy/paste the whole page, I'll direct you to open and read it yourself. Feel free to skip the section called "Password Verification" unless you're curious, since it isn't immediately relevant to our discussion here. However the section after that, called "Introduction to the Shell", is worth reading.

To keep the focus of this page on the current line of code, I broke off further discussion of processes into a separate blog post called "What Are Processes?". It contains some additional information and experiments based on the things I learned in the above article. I recommend reading it before moving on if you are still confused about processes, since understanding this line of code is dependent on having that added context.

Armed with this new information, let's try using exec ourselves.

Experiment- trying out the "exec" command

Directly in my terminal, I run:

$ exec ruby -e 'puts 5+5' 
10

[Process completed]

The final output I see in my terminal tab is "[Process completed]", and I can no longer run any commands in this tab. I have to close this tab and open a new one to resume entering commands in the terminal.

So when we read above that the command we execute "completely replaces the current process" and that "(t)he current shell process is destroyed", this is what we mean. Once the command we execute is completed, there is no more shell process to return to, since it was replaced by our command.

Using exec in the RBENV shim

So that's what the shell builtin exec command does. But the line of code we're looking at is:

exec /Users/richiethomas/.rbenv/libexec/rbenv bundle install foo bar baz

This means we're running the builtin exec command, and passing it the /Users/richiethomas/.rbenv/libexec/rbenv filepath. Remember what we read in the help exec output above: exec technically takes a filepath as an argument. It's just that, if we were to run exec rbenv instead of exec /Users/richiethomas/.rbenv/libexec/rbenv, the terminal would look up the filepath for rbenv by iterating through $PATH.

Since we don't know what the user's $PATH contains, we don't know whether the version it would find first is the version of rbenv that lives at /Users/richiethomas/.rbenv/libexec/rbenv. We specifically want to run that version, for reasons explained here, so we declare our desired filepath manually.

The rbenv exec command

We'll dive deeper into what rbenv exec does in a future section, but let's get a quick preview of what it does. A good way to do that is to check whether rbenv exec accepts a --help flag:

$ rbenv exec --help
Usage: rbenv exec  [arg1 arg2...]

Runs an executable by first preparing PATH so that the selected Ruby
version's `bin' directory is at the front.

For example, if the currently selected Ruby version is 1.9.3-p327:
  rbenv exec bundle install

is equivalent to:
  PATH="$RBENV_ROOT/versions/1.9.3-p327/bin:$PATH" bundle install

$ 

Translating the above, rbenv exec ensures that the first directory UNIX finds when it checks $PATH for a directory containing the command we entered is the one containing the version of Ruby you have set as your current version.

Wrapping Up

If you're still confused about processes, exec, and fork, you're probably not alone. Check out the link I mentioned above from University of Cincinatti, as well as my follow-up blog post on processes.

In the meantime, let's move on.