Tags

Understanding Shebangs

At the very top of a shell script, there is often a seemingly insignificant line that determines how the entire script is executed: the shebang. It begins with #!, but the words and paths that follow can make a meaningful difference to portability, compatibility and reliability.

Consider these three examples:

#!/bin/bash
#!/usr/bin/env bash
#!/bin/env bash

All three are intended to launch Bash, but they approach the task differently.

The first specifies Bash’s location directly. On many Linux systems, /bin/bash is perfectly valid, although some systems place it elsewhere or use a merged /usr layout where /bin points to /usr/bin.

The second asks /usr/bin/env to locate Bash through the current PATH. This makes it useful when Bash is installed in different locations, particularly across Linux, macOS and other Unix-like systems.

The third does essentially the same thing, but calls env from /bin/env instead. That path exists on some systems, but not all. On a typical modern Arch Linux installation, /usr/bin/env is the conventional choice, so there is little reason to prefer /bin/env unless you have a specific compatibility requirement.

The many faces of a Bash shebang

Here are some common variations and what they mean:

ShebangWhat it does
#!/bin/bashRuns Bash from /bin/bash.
#!/usr/bin/bashRuns Bash from /usr/bin/bash, where available.
#!/usr/bin/env bashFinds Bash using PATH.
#!/bin/env bashFinds Bash using PATH, if env exists at /bin/env.
#!/usr/local/bin/bashUses a locally installed Bash, common on some BSD and macOS setups.
#!/opt/homebrew/bin/bashUses Bash installed in the standard Apple Silicon Homebrew location.
#!/usr/bin/env -S bash -eUses env -S to pass Bash and the -e option as separate arguments.
#!/bin/bash -eStarts Bash with -e, which exits on many unhandled command failures.
#!/bin/bash -xStarts Bash with execution tracing enabled.

The important distinction is that a shebang is not a shell command. The operating system interprets it when you execute a script directly, so its rules differ from those of a normal command line. Options such as -e and -x also change a script’s behaviour, rather than merely specifying where Bash lives.

What about other shells?

The same principle applies to other interpreters and shells:

#!/bin/sh
#!/usr/bin/env sh

#!/bin/dash
#!/usr/bin/env dash

#!/bin/zsh
#!/usr/bin/env zsh

#!/bin/ksh
#!/usr/bin/env ksh

#!/bin/csh
#!/usr/bin/env csh

#!/bin/tcsh
#!/usr/bin/env tcsh

#!/usr/bin/env fish
#!/usr/bin/env python3
#!/usr/bin/env perl
#!/usr/bin/env ruby

Each line selects a different interpreter. Although Bash and sh share much of their syntax, they are not interchangeable in every situation. On Arch Linux, /bin/sh is normally a link to another POSIX-compatible shell, such as bash or dash on systems configured that way. Scripts written for sh should stick to POSIX shell syntax rather than relying on Bash-specific features such as arrays, [[ ... ]] or process substitution.

Similarly, csh, tcsh, zsh and fish have their own syntax and behaviours. Changing a shebang does not magically convert a script written for one shell into a script compatible with another.

So, which shebang should you use?

For a Bash script, my general recommendation is:

#!/usr/bin/env bash

It is a sensible choice when you want Bash to be located through PATH, rather than assuming a particular installation path. If you specifically want the system’s Bash at a known location, #!/bin/bash is equally reasonable on systems where that path is guaranteed.

One final detail is worth remembering: the shebang does not determine everything about how a script runs. If you execute bash something.sh, you explicitly choose Bash, and the shebang is ignored. If you execute ./something.sh, the shebang normally determines the interpreter, provided the file is executable and the path is valid.

Ultimately, a shebang might be only a single line at the top of a script, but it plays an important role in how that script is executed. Taking a moment to choose the right one can help avoid unnecessary complications, improve portability and make your scripts easier to maintain. It’s a small detail, perhaps easy to overlook, but one that is well worth getting right.