Next block of code:
ksh )
cat <<EOS
function rbenv {
Declaring the rbenv
function in ksh
This is the 2nd branch of our outer case statement (the one which checks which shell the user is using). If the user is using the ksh
shell (aka the Korn shell), we employ a similar strategy of starting to cat
a function definition, but this time we don’t close that definition (that comes later).
Note that the function
keyword prior to the function name is optional in Bash and zsh
, but mandatory in fish
and ksh
. One reason you might not want to use the function
keyword in Bash or zsh
is portability. According to StackOverflow, leaving the keyword off means your script will be more portable with older shells.
Declaring a local variable in ksh
Next block of code:
typeset command
EOS
;;
For now, we just declare a variable named command
, which is scoped locally to the rbenv
function according to the Korn shell docs:
typeset without options has an important meaning: if a typeset statement is used inside a function definition, the variables involved all become local to that function (in addition to any properties they may take on as a result of typeset options).
In other words, here the typeset
keyword is doing what the local
keyword would do in Bash.
Declaring the rbenv
function in other shells
Next lines of code:
* )
cat <<EOS
rbenv() {
local command
EOS
;;
This is the default, catch-all case for our “$shell” variable switch statement. If the user’s shell is not fish
or ksh
, then we cat
our rbenv
function definition, and (again) create a local variable named “command”.
Implementing the function body for all non-fish
shells
Next block of code:
if [ "$shell" != "fish" ]; then
...
fi
The remaining code inside rbenv-init
is only executed if the user’s shell is not fish
. If their shell is fish
, we’ve already finished echo
ing their shell function definition, so there’s nothing more to do.
Next lines of code:
IFS="|"
Here we set the IFS
variable (which stands for “internal field separator”) to the pipe symbol |
. We’ve covered this before, but IFS
is a special shell variable that determines how bash separates a string of characters into multiple strings. For example, let’s say we have a string a|b|c|d|e
. If IFS
is set to the pipe character (as above), and if we pass our single string to a for
loop, then bash will internally split our string into 5 strings (‘a’, ‘b’, ‘c’, ‘d’, and ‘e’) and iterate over each of them.
We’ll see why we needed to reset the value of IFS
shortly.
Printing the remaining function body
Next block of code:
cat <<EOS
...
EOS
The cat << EOS
line of code starts a new heredoc, so that we can finish implementing our rbenv
function.
The function body itself
Next block of code:
command="\${1:-}"
if [ "\$#" -gt 0 ]; then
shift
fi
case "\$command" in
${commands[*]})
eval "\$(rbenv "sh-\$command" "\$@")";;
*)
command rbenv "\$command" "\$@";;
esac
}
This is the rest of the file, and it’s a lot of code. It’s basically everything in the shell function except for the function’s initial declaration, and the declaration of the command
local variable.
We’ll break it up into more digestable pieces, but first, let’s see what it looks like in the shell function itself, without the escape characters:
command="${1:-}"
if [ "$#" -gt 0 ]
then
shift
fi
case "$command" in
(rehash | shell) eval "$(rbenv "sh-$command" "$@")" ;;
(*) command rbenv "$command" "$@" ;;
esac
}
I find the code easier to read without the escape characters.
Storing the user’s command
First line of the above is:
command="${1:-}"
Here we see some parameter expansion. I feel like I may have seen the :-
syntax before, but I don’t remember what it does. Referring to the GNU docs and searching for :-
, I find the following:
The docs go on to show multiple variations of the :-
syntax, however all of them contain something after the hyphen character. None of them end in :-
the way that our code does. We’ll have to do some experiments to see for sure what this does.
Experiment- parameter expansion with :-
I write the following simple script:
#!/usr/bin/env bash
command="${1:-}"
echo "$command"
I chmod
it and run it, both with and without arguments:
$ ./foo
$ ./foo bar baz buzz
bar
When I ran the command without any arguments, nothing printed out. When I ran it with 3 arguments, only the first argument printed out. So it looks like we did indeed capture the first argument.
For more context, I read this StackExchange post, which seems to say the same thing that the GNU docs said:
The variables used in
${1:-8}
and${2:-4}
are the positional parameters$1
and$2
. These hold the values passed to the script (or shell function) on the command line. If they are not set or empty, the variable substitutions you mention will use the default values8
and4
(respectively) instead.
What if we were to provide a default value after the :-
syntax? Would that work as we expect? Let’s modify the experiment accordingly, adding a default value of foo
for the first argument:
#!/usr/bin/env bash
command="${1:-foo}"
echo "$command"
When we call the script, again both with and without arguments, we see:
$ ./foo
foo
$ ./foo bar baz buzz
bar
So the script continues to print the first argument if there is one, but if there isn’t, it defaults the value to foo
. Cool! That means our script populates the command
variable with the first argument, but does not supply a default value of no argument is specified.
Conditionally removing the first argument
Next lines of code:
if [ "$#" -gt 0 ]
then
shift
fi
If we recall, $#
is shorthand for the number of arguments passed to the script. So if the number of arguments is greater than zero, we shift the first one off of the argument stack.
Handling the user’s command
Next lines of code:
case "$command" in
(rehash | shell) eval "$(rbenv "sh-$command" "$@")" ;;
(*) command rbenv "$command" "$@" ;;
esac
This block of code is short and familiar enough that we can analyze it all at once. Functionally, it’s the same as the end of the fish
shell function. We create a case
statement which branches depending on the value of the command
that the user entered.
Recall that the value of the commands
variable was set to:
commands=(`rbenv-commands --sh`)
Therefore, if the user’s command is present in the return value of rbenv-commands --sh
(i.e. if it’s equal to either rehash
or shell
), then we re-run the shell function version of rbenv
, but this time pre-pending sh-
to it.
If the user’s command was something other than shell
or rehash
, we use the command
shell program to skip the rbenv
function and go directly to the rbenv
script inside libexec
. We pass as arguments the name of the command and any other arguments the user included.
Here we see why we needed to override IFS
. The output of rbenv-commands --sh
is rehash | shell
. With IFS
set to its default value, Bash would only execute this branch of the case statement if the user entered the string "rehash | shell"
as their “command”. With IFS
set to |
, this logic works as expected- we execute the case
branch if the user’s command was either rehash
or shell
.
And with that, we’ve reached the end of the rbenv-init
file!