Every script you want to run from the command line should have a shebang as the first line.
Note: I talked about this in my MacSysAdmin talk. I wanted to go into more detail here.
You can have scripts without the shebang. In this case the shell that you launch the script from will be used to interpret the script. This can, obviously, lead to complications, on other systems or with other users.
A shebang looks like this:
#!/bin/bash
The eponymous first two characters of the shebang are #!
(hash–bang or shebang). These two characters form a ‘magic number.’ Files are usually identified by certain codes (i.e. magic numbers) in the first few bytes of data. The hex code 23 21
converts to the ascii characters #!
and tells the system that a file is script.
After the shebang comes the command that should interpret the script (the interpreter). This is either a shell, as in #!/bin/sh
or #!/bin/bash
or another interpreter, for example #!/usr/bin/python
or #!/usr/bin/osascript
. Generally, any command that can interpret text files or streams can be used in the shebang.
Since the status or value of the PATH
variable is not guaranteed or certain in many contexts that scripts can be run in, the path after the shebang needs to be absolute.
The env
shebang
There are environments where you cannot predict the absolute path of a given tool. For example the bash
v3.2 shell on macOS is installed by default in /bin/bash
. Users can also download and install bash
version 4.4 onto their computers. The location for the the bash
4 binary is usually at /usr/local/bin/bash
(this might be different depending on the installation method you used). Since /usr/local/bin
is the first item of the default PATH
on macOS the newer bash
4 will be chosen before the built-in bash
3.2 when the user types bash
into their shell.
When you use the absoute path to /bin/bash
in a shebang, you are ensuring that the macOS provided built-in version of bash
will be used. For macOS system administrators, this should be the preferred shebang, since it provides a known, certain environment.
However, there are cases where you want scripts to be run with the user’s preferred tool, rather than a set path. You may also want your script to be able to on multiple different unix (and unix-like) systems where the location of the bash
binary can be unpredictable. In this case you can use the /usr/bin/env
tool as the shebang with bash
(or another interpreter) as the parameter:
#!/usr/bin/env bash
This shebang means: “determine the preferred bash
tool in the user’s environment and use that to interpret the script.”
Note: obviously this also presumes the absolute path to the /usr/bin/env
. However, most unix and unix-like system seem to agree here.
Shebang Arguments
The shebang line can take a single argument. For weird syntactic reasons the entire line after the interpreter path is passed as a single argument to the interpreter tool.
So a shebang line that looks like this:
#!/bin/bash -x
will be executed like this:
/bin/bash "-x" /path/to/script
But if you added another variable to the shebang:
#!/bin/bash -x -e
then it would be executed as
/bin/bash/ "-x -e" /path/to/script
where "-x -e"
is a single argument, making bash
choke.
Some tools (like env
) that are regularly used in shebangs are able to split the single argument. bash
is not one of them. Read a tool’s man
page and documentation and test. In general it is considered bad style to add arguments or options to the shebang.
Note: with bash
you can also set these options in the script with the set
command:
set -x
set -e
bash
versus sh
Many Unix and unix-like systems have sh
as well as bash
and other shells available. sh
goes back to the very early UNIX shells in the seventies. sh
has survived because it serves as the lowest common standard for shell scripting. These standards are defined in the POSIX specification, though POSIX defines more than just shell scripting.
When you have to build scripts that need to run across many different flavors and versions of Unix and Linux, where you cannot rely on bash
being present, then conforming to POSIX and sh
might be necessary.
Note: bash
is also POSIX compliant, but it has more features. When you script against sh
you ensure you don’t use any of those additional features.
However, as macOS administrators, we can rely on a larger ‘common’ set. bash
has been present on macOS since Mac OS X 10.0 (and earlier incarnations in NeXTSTEP). bash
gives you several extra features over plain sh
such as better testing with the double square bracket, ‘here documents’, parameter substitution and arrays.
As a macOS system administrator you should leverage these and always choose /bin/bash
over /bin/sh
.
bash
versus Other Shells
The argument that bash
is better than sh
works for other shells as well. You can make a very good argument that other shells are better than bash
, or at the very least bash
version 3 included in macOS. I often hear this with zsh
, so I will be using zsh
as an example, but the arguments work with other shells as well.
(You can find a very good comparison of different shells here.)
zsh
and other shells have many features that bash
3 (and even 4) lacks. There are two main reasons I would still recommend bash
for scripting:
- most script examples and shared code is written in
bash
so you need to be proficient inbash
anyway (and know the quirks ofbash
) - the
zsh
is not included on the macOS Recovery System or other macOS installation environments, so you cannot write post-installation scripts for these contexts inzsh
When you do reach the limitations of bash
for scripting, other languages such as Python and Swift are even more useful and powerful on macOS than zsh
, so the step up from bash
should not be another shell, but a different scripting language entirely
Interactive shell versus shell scripting
While I argue that that other shells don’t have that much edge on bash
for scripting, they certainly do have an edge on bash
in some of their interactive and customization features.
Two of the more interesting interactive shells for macOS are zsh
and fish
.
The good news here is that you can set one shell for interactive use, and still continue using and writing bash
scripts and get the best of both worlds.
The default interactive shell is set in your user record, you can change this in the ‘Advanced Options’ in the Users & Groups preference pane, or with chsh
command:
$ chsh -s /bin/zsh
Changing shell for armin.
Password for armin:
The shell that interprets a script is, of course, set with the shebang.
Summary
- the first line in a shell script starts with a shebang
#!
and tells the system which tool interprets the script env
should only be used in the shebang when the script needs to run in environments where you cannot predict the location of the actual interpreter. For admins this introduces an extra level of uncertainty- macOS administrators should use
/bin/bash
as the shebang - the ‘step up’ from
bash
scripting should be a more complex language with native macOS API access such as Python or Swift - you can use one shell for interactive Terminal work and another for scripting
Thanks for this post! On your second summary point you left the sentence incomplete: “env should only be used in the shebang when the script needs to”
Thank you! Fixed the line.
Excellent post, definitely one of the clearest, most straightforward and useful posts I’ve seen discussing the shebang!
However, you may want to look at the second bullet point of `bash versus Other Shells` heading as two words get combined where I’m guessing you meant to have a third bullet point or possibly just another sentence.
Thank you, Chris. Glad you like it. I have fixed the layout
On macOS, the shebang interpreter must be an executable binary. On Linux, it can be a shell script.
https://stackoverflow.com/questions/9988125/shebang-pointing-to-script-also-having-shebang-is-effectively-ignored
I ran into this limitation because I am trying to write my own “env” script which will create the environment before invoking the interpreter. Why? Apparently, the system Script menu launches scripts without propagating the environment à la bash/launchd.
https://apple.stackexchange.com/questions/412455/script-menu-how-to-reference-user-path-utilities-path-environment-is-missing