Setting the Name of the User's Program

Moving on to the next line of code:

program="${0##*/}"

We declare a variable named program, so it looks like we're storing the name of the program we're running. Let's see if that's correct.

Experiment- what string are we storing?

I open up the shim file (~/.rbenv/shims/bundle), and edit it to add the echo statement just beneath the declaration of the program variable. If your shim doesn't live here, run which bundle in your terminal to get its filepath.

After editing the shim file, it now looks like this:

#!/usr/bin/env bash
set -e
[ -n "$RBENV_DEBUG" ] && set -x

program="${0##*/}"

echo "program name: $program"    #  <= I added this line

if [ "$program" = "ruby" ]; then
  for arg; do
    case "$arg" in
    -e* | -- ) break ;;
    */* )
      if [ -f "$arg" ]; then
        export RBENV_DIR="${arg%/*}"
        break
      fi
      ;;
    esac
  done
fi

export RBENV_ROOT="/Users/richiethomas/.rbenv"
exec "/opt/homebrew/bin/rbenv" exec "$program" "$@"

This new echo statement prints the value of the program variable to the screen. The $ in program name: $program tells Bash that we want to print the value of the program variable, as opposed to the string "program".

Then I run bundle version in my terminal, and I see the following:

$ bundle version
program name: bundle
Bundler version 2.3.14 (2022-05-18 commit 467ad58a7c)
$ 

We see program name: bundle before our expected output of the Bundler version number. Just to be safe, I do the same experiment with the shim for the ruby command (i.e. ~/.rbenv/shims/ruby). When I run ruby --version, I see the following:

$ ruby --version
program name: ruby
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin23]
$ 

Same thing- it printed the name of the first command I entered into the terminal, followed by my Ruby version.

Before moving on, I delete my echo commands from both the bundle and ruby shims.

Parameter Expansion

What is this weird syntax which evaluates to the name of the program? After Googling the exact string ${0##*/}, I find this StackOverflow link, which says:

...this has to deal with parameter expansion.

$0 is the name and path that this script was executed with. For example, if you call /usr/bin/example.sh, then $0 will be /usr/bin/example.sh. If your current working directory is /usr, and you call the same script with ./bin/example.sh, then $0 will be ./bin/example.sh.

As for the #, this means to expand $0 after removing the previously specified prefix. In this case the previously specified prefix is the */ glob. A single # is non-greedy, so after it matches the first */ glob, it will stop. So it will remove the first / and everything before it. Two #'s mean to greedily remove all */. So it will find remove all / and everything that comes before them.

The answer says we're dealing with something called "parameter expansion", and works as follows:

  • $0 will evaluate to the path of the file that we're executing.
  • We can modify it by using # and */ inside the curly braces.

Let's test how $0 is affected by this parameter expansion syntax.

Experiment- reproducing the effects of parameter expansion

I create a directory named foo/bar/, containing a file named baz, and chmod the file so it will execute. The -p flag after mkdir just creates the bar/ directory as well as any in-between directories (i.e. foo/) which don't already exist:

$ mkdir -p foo/bar
$ touch foo/bar/baz
$ chmod +x foo/bar/baz
$ 

I open up baz and I type the following:

#!/usr/bin/env bash

echo "$0"

Then I run it, and I see the following:

$ ./foo/bar/baz
./foo/bar/baz
$ 

The output was ./foo/bar/baz, meaning we've verified that we can reproduce the $0 behavior described in the StackOverflow post.

On a hunch, I try wrapping $0 in curly braces, to see if its output will change

#!/usr/bin/env bash

echo "${0}"

When I execute this updated version of ./foo/bar/baz, it displays the same output as before:

$ ./foo/bar/baz
./foo/bar/baz
$ 

So $0 and ${0} seem to be functionally equivalent.

Now to test the 2nd part of the answer, about removing prefixes. I'll first try the same syntax as in the StackOverflow answer (i.e. ##*/):

#!/usr/bin/env bash

echo "${0##*/}"

When I run it, I see:

$ ./foo/bar/baz   
baz
$ 

So without the ##*/ syntax, we get ./foo/bar/baz as our output. With this new syntax, we get just baz as the output. Therefore, adding ##*/ inside the curly braces had the effect of removing the leading "./foo/bar/" from ./foo/bar/baz.

Out of curiosity, what happens when I remove one of the two # symbols?

#!/usr/bin/env bash

echo "${0#*/}"

Running the above returns:

$ ./foo/bar/baz
foo/bar/baz
$ 

Now we see foo/bar/baz. The foo/bar/ prefix is no longer missing, but the leading ./ before foo/ has been removed.

This is expected. The StackOverflow answer mentions that including only one # will cause the shell to remove the first case of its search pattern, plus everything before it. On the other hand, two # symbols tells the shell to stop after matching the last case of its search pattern (again, plus everything before it).

In our case, one # will cause ./ to be removed, while two ## will cause ./foo/bar/ to be removed.

There is much, much more to learn about parameter expansion. We will encounter it again in future parts of the codebase, using new and different patterns besides ##/*. This link contains the GNU docs for parameter expansion, including a much more complete list of the syntax and its capabilities.