Shebangs

If you recall, our shim file looks like this:

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

program="${0##*/}"
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" "$@"

Which means the first line of code is:

#!/usr/bin/env bash

I run a Google search for the string #!/usr/bin/env bash, and I learn that this line of code is called a "shebang".

In UNIX, a shebang is a special line of code at the top of a script file which tells UNIX which program to use when executing the rest of the file's code. In this case, since the shebang ends in Bash, we're telling UNIX to use the bash shell to evaluate the code.

If you're going to include a shebang, it must be on the first line of the file in order for it to work correctly.

Different types of shebangs

Note that you might sometimes see #!/usr/bin/bash as a shebang, instead of #!/usr/bin/env bash. The difference between these two is that /usr/bin/env does a bit of extra work. Specifically, it:

  • checks your terminal environment for variables,
  • sets them, and then
  • runs your command.

If we type env into our terminals, we can see a list of the environment variables that #!/usr/bin/env will set. The following is a partial list, based on running env on my machine:

$ env
TERM_PROGRAM=Apple_Terminal
SHELL=/bin/zsh
TERM=xterm-256color
TMPDIR=/var/folders/n9/35wcp_ps2l919c07czwh504c0000gn/T/
TERM_PROGRAM_VERSION=452
TERM_SESSION_ID=563FA0D2-24E2-4FDE-9DE4-D7E6D9E58123
USER=richiethomas
...

Using the #!/usr/bin/env bash shebang instead of #!/usr/bin/bash has both its pros and cons.

Pros of #!/usr/bin/env bash

On the one hand, using this shebang means that your script doesn't depend on Bash residing in a specific folder on the user's machine. This is because #!/usr/bin/env bash tells macOS "Hey, check the directories listed in your $PATH environment variable for the 'bash' executable, and use the first one you find to run code which follows."

We'll talk about environment variables in a future chapter. But if you're unfamiliar with the $PATH environment variable, I've got a write-up about it here.

If we use the /usr/bin/bash shebang, then whoever runs our script must have Bash installed in their /usr/bin/ directory. Since people run RBENV on all kinds of machines with all sorts of software installed, this is not a safe assumption. For example, my bash executable is installed at /bin/bash, with no /usr/ prefix. You may face a similar situation on your machine.

If we load $PATH into our environment via the /usr/bin/env bash shebang, then UNIX will search through all the directories in $PATH until it finds Bash. More directories in our $PATH means more chances to find a working bash executable.

Cons of using #!/usr/bin/env bash

Since using #!/usr/bin/env bash means that macOS will find the first version of Bash that it finds, this means that your users could potentially be using different versions of Bash. This could cause your program to behave in unexpected ways, depending on how different those versions are.

The links here and here contain additional info on the differences between the two types of shebangs, including some cases where you might not want to use /usr/bin/env bash.

Why do we need any shebang at all?

Hypothetically, we could leave the shebang out from this file. But somehow we have to tell UNIX which program to use when running the file. If we don't do so in the file itself (i.e. by using a shebang), we'd have to do so when we type the command into the terminal. So instead of typing bundle install in the command line, we'd have to type the following every time:

$ /usr/bin/env bundle install

Or:

$ /bin/bash bundle install

Using a shebang not only saves us a few keystrokes, but it's also one less thing that we humans can mess up when manually typing our command into the terminal.

Non-bash shebangs

As I mentioned before, the string "bash" at the end of the shebang tells UNIX to use the Bash shell when interpreting the code which follows. But Bash is not the only interpreter we can tell UNIX to use for a script that we write.

The only reason the code uses the bash shebang here is because the subsequent code is written in Bash. If they had written it in Ruby, they could have used a Ruby shebang (i.e. #!/usr/bin/env ruby) instead. In fact, let's try doing exactly that, as an experiment.

Experiment- writing our own shebang

We start by writing a regular Ruby script with a ".rb" file extension. We'll name the file hello.rb, and the file will include the following code:

# hello.rb

puts "Hello, world!"

When we run ruby hello.rb from the command line, we get:

$ ruby hello.rb 
Hello, world!
$

What happens if we don't use the ruby command, instead just running the file as if it were an executable?

$ ./hello.rb
zsh: permission denied: ./hello.rb
$

OK, well this is just because we haven't yet updated the file permissions to make the file executable. That's a step we'll need to do whenever we make a brand-new file.

We do that with the chmod command, passing +x to tell UNIX to set the file's execution permission. Let's do that, and then try to re-run the file:

$ chmod +x hello.rb 
~/Desktop/Workspace/impostorsguides.github.io (main)  $ ./hello.rb 
./hello.rb: line 1: puts: command not found
$

Now we have a new error, which is telling us that UNIX doesn't recognize the command puts. That's because puts is a Ruby command, and we haven't yet told UNIX that we want to use Ruby.

Lastly, let's add a Ruby-specific shebang to the top of the file:

#!/usr/bin/env ruby

puts "Hello, world!"

Now, when we re-run the file, we get:

$ ./hello.rb 
Hello, world!
$

Success! We've told bash which interpreter we want to use, meaning that we no longer need to use the ruby command at the terminal prompt.

A Github gist of the above hello.rb file can be found here.

File extensions vs. shebangs

Our file includes a .rb file extension at the end, but the terminal doesn't use that extension when deciding how to interpret the file. I came across this StackOverflow post while looking for documentation on this question. One of the answers states:

On OS X (and U*x generally), the name of the file doesn't matter at all. What matters is that it needs to have executable permission (chmod +x file) and a correct shebang line.

This is one big difference between Windows and UNIX. The former takes the approach of using the file extension to determine which program to use when executing a given file. This means Windows application developers have to tell the OS which file extensions their application can open. The benefit of the Windows approach is that no shebang is required in the file itself.

UNIX, on the other hand, doesn't use such a registry, at least not when directly running scripts from the terminal. This means that the author of a file (rather than the author of an application) gets to decide how to open their file. Different philosophies, different trade-offs.

Moving On

The next line of code is:

set -e

This is a tiny line of code which packs a lot of punch. In the next section, we'll look at how the set command works.