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.