Next line of code is:
shopt -s nullglob
Pattern-matching on filepaths
In my Bash shell, I type help shopt
, and get the following:
$ help shopt
shopt: shopt [-pqsu] [-o long-option] optname [optname...]
Toggle the values of variables controlling optional behavior.
The -s flag means to enable (set) each OPTNAME; the -u flag
unsets each OPTNAME...
With no options, or with the -p option, a list of all
settable options is displayed, with an indication of whether or
not each is set.
So we’re enabling some optional behavior here. That behavior is controlled by the nullglob
option. I Google “shopt nullglob”, and find the GNU documentation page for shopt
.
nullglob
If set, Bash allows filename patterns which match no files to expand to a
null string, rather than themselves.
Lastly, StackExchange has an example of what would happen before and after nullglob
is set:
Filename globbing patterns that don’t match any filenames are simply expanded to nothing rather than remaining unexpanded.
$ echo my*file my*file $ shopt -s nullglob $ echo my*file $
This code sets a shell option so that we can change the way we pattern-match when we’re iterating over a list of filepaths. If a pattern doesn’t match any files in the directory we’re searching, the pattern will expand to the empty string. This will prevent us from trying to access a file which doesn’t exist, potentially raising an error and causing our script to terminate.
Experiment- testing the behavior of nullglob
I write a script to try and emulate what we see in the for
loop above:
#!/usr/bin/env bash
for plugin_dir in "$PWD"/foo/*; do
echo "plugin_dir: $plugin_dir"
done
When I create a directory with a few subdirectories and run this test script, I get:
$ mkdir foo
$ mkdir foo/bar
$ mkdir foo/baz
$ mkdir foo/buzz
$ ./script
plugin_dir: /Users/myusername/Workspace/OpenSource/foo/bar
plugin_dir: /Users/myusername/Workspace/OpenSource/foo/baz
plugin_dir: /Users/myusername/Workspace/OpenSource/foo/buzz
So far, that’s what I expected. But what if there are no directories? I delete the three sub-directories and re-run it:
$ rm -r foo/bar
$ rm -r foo/baz
$ rm -r foo/buzz
$ ./script
plugin_dir: /Users/myusername/Workspace/OpenSource/foo/*
OK, this makes sense, given that I didn’t run that shopt
line first. If I add that to my script:
#!/usr/bin/env bash
shopt -s nullglob
for plugin_dir in "$PWD"/foo/*; do
echo "plugin_dir: $plugin_dir"
done
…and re-run it, I get:
$ ./script
No output when shopt -s nullglob
is set. So setting shopt -s nullglob
means that, if there are no filepaths or directory paths which match the given pattern, we make zero iterations in our for
loop.
I’m curious whether there’s any explanation for this block of code in the Github history, so I dig into the git history using my git blame / git checkout
dance again. There’s only one issue and one commit. Here’s the issue with its description:
The purpose of this branch is to provide a way to install self-contained plugin bundles into the $RBENV_ROOT/plugins directory without any additional configuration. These plugin bundles make use of existing conventions for providing rbenv commands and hooking into core commands.
…
Say you have a plugin named foo. It provides an
rbenv foo
command and hooks into therbenv exec
andrbenv which
core commands. Its plugin bundle directory structure would be as follows:foo/ bin/ rbenv-foo etc/ rbenv.d/ exec/ foo.bash which/ foo.bash
When the plugin bundle directory is installed into
~/.rbenv/plugins
, therbenv
command will automatically add~/.rbenv/plugins/foo/bin
to$PATH
and~/.rbenv/plugins/foo/etc/rbenv.d/exec:~/.rbenv/plugins/foo/etc/rbenv.d/which
to$RBENV_HOOK_PATH
.
This explains not only the shopt
line, but the next few lines after that.
Checking for plugins
Next few lines of code are:
bin_path="$(abs_dirname "$0")"
for plugin_bin in "${RBENV_ROOT}/plugins/"*/bin; do
PATH="${plugin_bin}:${PATH}"
done
export PATH="${bin_path}:${PATH}"
It’s a bit harder to read when written this way, since bin_path
is not used inside the subsequent for
loop. Let’s re-arrange it to make things easier:
for plugin_bin in "${RBENV_ROOT}/plugins/"*/bin; do
PATH="${plugin_bin}:${PATH}"
done
bin_path="$(abs_dirname "$0")"
export PATH="${bin_path}:${PATH}"
Adding plugins to PATH
Looking at the code for the for
loop:
for plugin_bin in "${RBENV_ROOT}/plugins/"*/bin; do
PATH="${plugin_bin}:${PATH}"
done
The above Github issue posited a world where we have an RBENV plugin named foo
. It exposes a command named rbenv foo
. When the GH issues says:
...the `rbenv` command will automatically add `~/.rbenv/plugins/foo/bin` to `$PATH`...
…it’s telling us that we’ll be able to call rbenv-foo
, because the rbenv-foo
file is located in ~/.rbenv/plugins/foo/bin
, a folder which is now being added to PATH
.
Since this takes place inside a for
loop which iterates over the contents of "${RBENV_ROOT}/plugins/"*/bin;
, this is true not only for foo
, but for any plugins which are installed within "${RBENV_ROOT}/plugins/"
.
Referring back to this code:
shopt -s nullglob
The StackExchange post from earlier said:
Filename globbing patterns that don’t match any filenames are simply expanded to nothing rather than remaining unexpanded.
I suspect that, with the nullglob
option turned on, the pattern "${RBENV_ROOT}/plugins/"*/bin;
expands to nothing if no plugins are installed. So turning this option on means our for
loop will iterate 0 times if the plugins/
directory is empty.
Adding libexec/
to PATH
Next let’s address these two lines:
bin_path="$(abs_dirname "$0")"
export PATH="${bin_path}:${PATH}"
Here we’re finally calling the abs_dirname
function we defined in the if/else
block earlier.
On my machine, bin_path
resolves to /Users/myusername/.rbenv/libexec
when I echo
it to the screen. By adding this path to our PATH
variable, we’re implying that one or more files inside /libexec
should be executable, since the purpose of the PATH
env var is to:
…specify a set of directories where executable programs are located.
The libexec/
folder contains both the file we’re looking at now (rbenv
) and the other rbenv command files (libexec/rbenv-version
, libexec/rbenv-help
, etc.). So by adding libexec/
to PATH
, we’re indicating that one or more of the commands in libexec/
are meant to be executable.
Let’s move onto the next block of code.