TODO: add a section on why someone would choose fish
over zsh
or Bash.
Next line of code is:
mkdir -p "${RBENV_ROOT}/"{shims,versions}
Here we make two sub-directories inside “$RBENV_ROOT”- one named “shims” and one named “versions”.
Brace Expansion
The curly-brace syntax around {shims,versions}
deserves special mention. This is called “brace expansion”. It’s useful when the strings that you want to create share similarities, and differ only in one or a few respects.
Here, we’re using it to create the two directories in a single line of code, rather than two separate lines, like so:
mkdir -p "${RBENV_ROOT}/"shims
mkdir -p "${RBENV_ROOT}/"versions
The more strings you want to create, the more useful this is. To illustrate this, let’s try an experiment.
Experiment- brace expansion
I want to create 6 new directories, with the following names:
~/Workspace/OpenSource/foo/bar
~/Workspace/OpenSource/foo/baz
~/Workspace/OpenSource/foo/buzz
~/Workspace/OpenSource/quox/bar
~/Workspace/OpenSource/quox/baz
~/Workspace/OpenSource/quox/buzz
I don’t want to have to write these all out by hand. So instead, I do the following:
mkdir -p ~/Workspace/OpenSource/{foo,quox}/{bar,baz,buzz}
Now, I want to add a .ruby-version
file in each directory, with the string 3.0.0
inside each one:
echo "3.0.0" > ~/Workspace/OpenSource/{foo,quox}/{bar,baz,buzz}/.ruby-version
Next, I want to list the contents of my new directories:
$ ls -la ~/Workspace/OpenSource/{foo,quox}/{bar,baz,buzz}/
/Users/myusername/Workspace/OpenSource/foo/bar/:
total 8
drwxr-xr-x 3 myusername staff 96 May 7 11:23 .
drwxr-xr-x 5 myusername staff 160 May 7 11:22 ..
-rw-r--r-- 1 myusername staff 6 May 7 11:23 .ruby-version
/Users/myusername/Workspace/OpenSource/foo/baz/:
total 8
drwxr-xr-x 3 myusername staff 96 May 7 11:23 .
drwxr-xr-x 5 myusername staff 160 May 7 11:22 ..
-rw-r--r-- 1 myusername staff 6 May 7 11:23 .ruby-version
/Users/myusername/Workspace/OpenSource/foo/buzz/:
total 8
drwxr-xr-x 3 myusername staff 96 May 7 11:23 .
drwxr-xr-x 5 myusername staff 160 May 7 11:22 ..
-rw-r--r-- 1 myusername staff 6 May 7 11:23 .ruby-version
/Users/myusername/Workspace/OpenSource/quox/bar/:
total 8
drwxr-xr-x 3 myusername staff 96 May 7 11:23 .
drwxr-xr-x 5 myusername staff 160 May 7 11:22 ..
-rw-r--r-- 1 myusername staff 6 May 7 11:23 .ruby-version
/Users/myusername/Workspace/OpenSource/quox/baz/:
total 8
drwxr-xr-x 3 myusername staff 96 May 7 11:23 .
drwxr-xr-x 5 myusername staff 160 May 7 11:22 ..
-rw-r--r-- 1 myusername staff 6 May 7 11:23 .ruby-version
/Users/myusername/Workspace/OpenSource/quox/buzz/:
total 8
drwxr-xr-x 3 myusername staff 96 May 7 11:23 .
drwxr-xr-x 5 myusername staff 160 May 7 11:22 ..
-rw-r--r-- 1 myusername staff 6 May 7 11:23 .ruby-version
Now, I want to cat
each of the files, to make sure the previous command succeeded and my new Ruby version file contains the correct info:
$ cat ~/Workspace/OpenSource/{foo,quox}/{bar,baz,buzz}/.ruby-version
3.0.0
3.0.0
3.0.0
3.0.0
3.0.0
3.0.0
Note that the comma-separated values must not include any spaces:
$ touch ~/Workspace/OpenSource/{foo, quox}/{bar, baz, buzz}/new-file
touch: quox}/{bar,: No such file or directory
touch: buzz}/new-file: No such file or directory
You’ll get an error if you try to include spaces in a brace expansion, like we see above. The specific error you get will depend on the command you tried to use brace expansion with.
Setting the PATH
and RBENV_SHELL
env vars
Next line of code:
case "$shell" in
fish )
echo "set -gx PATH '${RBENV_ROOT}/shims' \$PATH"
echo "set -gx RBENV_SHELL $shell"
;;
...
esac
Here we have a simple case statement, which branches based on the value of our “$shell” string, along with our first case branch.
If the value of "$shell"
is fish
, we echo
a few commands to stdout
. These commands are then run inside the command substitution that we add to our shell configuration file. We learned when we set the value of the profile
variable that, if our shell is fish
, this config file lives in ~/.config/fish/config.fish
. If our shell was fish
, the code we added to that file is status --is-interactive; and rbenv init - fish | source
.
Both these commands use the fish
shell’s set
command to set shell variables. The “-g” flag makes the variable global, and the -x
flag makes the variable available to child processes. We’re creating one environment variable (RBENV_SHELL
), and modifying another (PATH
) to pre-pend it with ${RBENV_ROOT}/shims'
so that the shims which RBENV creates will be findable by our terminal. More info here.
Next block of code:
* )
echo 'export PATH="'${RBENV_ROOT}'/shims:${PATH}"'
echo "export RBENV_SHELL=$shell"
...
This code will be executed if our "$shell"
variable contains any value other than fish
.
Just as it was with the fish
script, our goal here is to set the PATH
and RBENV_SHELL
env vars. But since our shell is not fish
, we use regular Bash syntax instead of fish
syntax. For example, instead of set -gx
, we use export
statements.
Importing completion files
Next block of code:
...
completion="${root}/completions/rbenv.${shell}"
if [ -r "$completion" ]; then
echo "source '$completion'"
fi
;;
After the two export
statements, the next block of code in the default case
branch creates a file path where the user’s completion file should live. After the filepath is created, the [ -r "$completion" ]
test checks whether that file actually exists and is readable. If it does exist, we run source
on that file in order to run its contents. If not, nothing will happen.
The ${root}/completions
folder only includes two such files for now- rbenv.bash
and rbenv.zsh
. So the if
logic will only be executed if the user’s shell is either Bash or zsh
. If the shell is something like ksh
, we would reach the [ -r "$completion" ]
check, but that check would be falsy.
Why do we add completions for Bash and zsh
, but not fish
?
I was wondering why we include completion scripts for Bash and zsh
, but not fish
, so I did some digging in the Github history. I found this issue which indicates that fish
supports RBENV completions natively.
Because of that, the rbenv.fish
shell was redundant and was removed as part of this PR. The code to source
the completion file was then moved into the default branch of the case
statement here.
The source
command
We saw that, if the filepath for our completions file actually mapped to a real file, then we call the following code:
echo "source '$completion'"
The code that we echo
is source '$completion'
. This code will then be executed inside the command substitution that we add to our shell’s config file.
The source
command is one of the more commonly-used commands I have in my shell scripting toolbox. The command takes the name of a file, and runs that file. I find this useful when I edit my shell configuration file (i.e. ~/.zshrc
) and I don’t want to have to open a new terminal tab in order to activate the changes I’ve made.
Typing help source
in my Bash shell returns the following:
bash-3.2$ help source
source: source filename [arguments]
Read and execute commands from FILENAME and return. The pathnames
in $PATH are used to find the directory containing FILENAME. If any
ARGUMENTS are supplied, they become the positional parameters when
FILENAME is executed.
Let’s try this out with an experiment.
Experiment- the source
command
I write a script called ./foo
, which includes the following:
#!/usr/bin/env bash
echo "Hello world"
In my terminal, I chmod +x
my new foo
script, then I run the following:
$ source ./foo
Hello world
My script ran, as expected. OK, but how is this different from just running the script directly, like this?
$ ./foo
Hello world
The difference is that ./foo
by itself will run the script in a subshell, meaning that the code will be executed inside a child process, and the effects of running the shell (such as any newly-declared shell functions or environment variable updates) will not be available to the parent process once the child is finished executing.
Let’s see this for ourselves. I update the foo
script to the following:
#!/usr/bin/env bash
export FOO="foo bar baz"
When I run it, I see:
$ ./foo
If I then run echo "$FOO"
, will the env var’s value print to the terminal?
$ echo "$FOO"
$
Nothing was echo
ed, even though my script contained an export
statement. That’s because export
only makes an environment variable to a script and its children, not to any parent script(s). And when we run ./foo
by itself, we create a subshell, which uses a child process.
Sometimes we want to isolate those environment variables from our parent, in which case running ./foo
in a subshell is the right call. But what if that’s not what we want? What if we really do want FOO
to be set in our parent script? Well, we have to use source
instead:
$ source ./foo
Now, when we try to echo "$FOO"
, we see that env var has a value set in our terminal:
$ echo "$FOO"
foo bar baz
That’s the difference between using source
and just calling a script directly.
Rehashing our shims
Next lines of code:
if [ -z "$no_rehash" ]; then
echo 'command rbenv rehash 2>/dev/null'
fi
If $no_rehash
was not set (i.e. if the user did NOT pass --no-rehash
as an argument), then we run rbenv command rehash
and send any errors to /dev/null
. We’ll examine the rbenv rehash
command more fully when we get to the libexec/rbenv-rehash
file. But in brief, this command generates the shim files for any Ruby dependencies we’ve installed.
Why would someone pass the --no-rehash
flag? According to the How RBENV Hooks Into Your Shell
section of the README
file, rehashing gems can add latency to your shell startup. If you want to avoid this latency and think rehashing every time you open a new terminal tab is overkill, pass the --no-rehash
flag.
In the next section, we’ll declare and implement the rbenv
shell function.