Linux & Bash · Part I · Module 15 · Writing and Running Bash Scripts
The Shebang, and Which Interpreter Runs Your Script
Two characters at the very start of a file decide which program interprets the rest of it. Choosing the wrong one gives you a script that works on your machine and fails on a colleague's, and choosing none at all gives you one whose behaviour depends on how it happened to be launched.
Foundation17 min readLinuxPart IFundamentalsScripting
Part I · Module 15 · Chapter 15.2 · The Shebang Line
Chapter 15.1 put #!/bin/bash at the top of every script and asked you to take it on trust.
1. What It Does
#!/bin/bash
echo "hello"The first two characters, #!, tell the kernel that the rest of the line names the program that should run this file.
The sequence is mechanical:
So ./build.sh with #!/bin/bash at the top becomes, in effect:
/bin/bash ./build.shThe # is a comment to the shell, which is why the line is harmless if the file is read some other way. The ! after it is what the kernel looks for.
2. With No Shebang
$ cat noshebang.sh
echo "interpreter: $(ps -o comm= -p $$)"
$ ./noshebang.sh
interpreter: shIt ran under sh, not bash.
With no marker the kernel refuses the file, and the shell falls back to running it with a plain sh. That is a POSIX requirement, and it means a script without a shebang gets the most basic shell on the system — not the one you wrote it in.
On a machine where sh is a minimal shell, every Bash-specific feature in that script fails. On one where sh is Bash, it works, and you do not discover the problem until somebody else runs it.
3. The Two Common Choices
#!/bin/bash # the Bash shell, specifically
#!/bin/sh # whatever the system's POSIX shell isThey are not interchangeable. #!/bin/sh promises that the script uses only POSIX features, and the shell it gets varies by system:
| System | /bin/sh is usually |
|---|---|
| Debian, Ubuntu | dash — small, fast, strictly POSIX |
| Red Hat, Fedora | bash in POSIX mode |
| macOS | bash 3.2 in sh mode |
| Alpine | busybox ash |
The things that stop working under a strict sh are the ones you have been using throughout:
| Feature | Taught in | POSIX? |
|---|---|---|
| Arrays | Chapter 19.2 | No |
[[ … ]] | Module 16 | No |
{1..5} brace ranges | 9.5 | No |
${v:0:3} substrings | 8.3 | No |
$'…' | 9.3 | No |
<<< here-strings | 12.4 | No |
local in functions | Chapter 18.5 | Widely supported, not POSIX |
$(…), $((…)), ${v:-x} | Modules 8–9 | Yes |
4. The env Form
#!/usr/bin/env bashenv looks up bash on PATH and runs the first one it finds (Chapter 10.2), rather than insisting on /bin/bash.
$ ./envform.sh
env form works; BASH_VERSION=3.2.57(1)-release#!/bin/bash | #!/usr/bin/env bash | |
|---|---|---|
| Finds Bash in an unusual place | No | Yes |
Picks up a newer Bash on PATH | No | Yes |
| Predictable | Yes | Depends on the caller's PATH |
| Works with a scrubbed environment | Yes | Can fail |
The case for env is real. On macOS, /bin/bash is version 3.2 from 2007; a Homebrew Bash 5 installs elsewhere and env finds it. On BSD systems Bash is often at /usr/local/bin/bash. A script hard-coding /bin/bash fails there outright.
The case against is that the script's behaviour now depends on the caller's PATH. In cron, in a build system, or under sudo — all of which reduce or reset PATH (Chapter 10.1) — env may find a different Bash or none.
5. When the Interpreter Is Missing
$ cat bad.sh
#!/bin/nosuchshell
echo hi
$ ./bad.sh
bash: ./bad.sh: bad interpreter: /bin/nosuchshell: no such file or directory
$ echo $?
127The error names the interpreter, not your script — and status 127 is Chapter 11.1's "command not found", because from the kernel's point of view that is exactly what happened.
People read that message as being about bad.sh and go looking for a problem in the script. The file after "bad interpreter:" is where to look.
The carriage-return version
$ ./crlf.sh
bash: ./crlf.sh: bad interpreter: /bin/bash^M: no such file or directory/bin/bash^M. The file was written on Windows, so its first line ends \r\n, and the carriage return is part of the interpreter's name as far as the kernel is concerned. There is no program called /bin/bash followed by a carriage return.
Chapter 4.7 met this and Chapter 13.5 gave the fix:
$ tr -d '\r' < crlf.sh > fixed.sh && chmod +x fixed.sh && ./fixed.sh
hi6. Arguments in the Shebang
#!/bin/bash -euThe shebang may carry arguments, and they are passed to the interpreter. The line above starts Bash with errexit and nounset already on.
Two reasons not to rely on it:
Multiple arguments are not portable. Linux passes everything after the interpreter path as a single argument, so #!/bin/bash -e -u arrives as one string -e -u and fails. macOS is more permissive — all three forms worked on the verification machine — which means the bug appears only when the script moves.
It is invisible. A reader scanning for set -e does not find it, and a script run as bash script.sh loses it entirely, because then the shebang is not consulted at all.
#!/bin/bash
set -euo pipefailPut the options on their own line. It works everywhere, survives bash script.sh, and the next reader can see it. Chapter 10.6 is where those options are explained.
7. Other Interpreters
The mechanism is not about shells:
#!/usr/bin/env python3
#!/usr/bin/perl -w
#!/usr/bin/awk -f
#!/usr/bin/env tclshAny program that can take a filename and read it line by line can be a shebang interpreter. That is why a Python script is run the same way as a shell script, and why ./analyse.py works with no mention of Python on the command line.
It is worth knowing in a mixed repository. A scripts/ directory can hold Bash, Python and Tcl, all invoked identically, each declaring its own interpreter — and nobody has to remember which is which.
8. In Practice
#!/usr/bin/env bash
#
# A script meant to be shared, that needs a modern Bash.
set -euo pipefail#!/bin/bash
#
# A script run by cron, where PATH is minimal and predictability matters.
set -euo pipefail
export PATH="/opt/tool/bin:/usr/local/bin:/usr/bin:/bin"#!/bin/sh
#
# A genuinely POSIX script — no arrays, no [[ ]], no brace ranges.
# Checked against dash, not just against a Mac.
set -euThe comment on that last one is the important part. A #!/bin/sh claim is only worth making if you have tested it under a real POSIX shell, and the cheapest way is:
dash -n script.sh # parse only, no execution — Chapter 10.6's set -ndash is installable on macOS and standard on Debian. One command tells you whether the claim is true.
Practice
mkdir -p /tmp/sb && cd /tmp/sb1 — Find out what runs a script with no shebang.
printf 'echo "interpreter: $(ps -o comm= -p $$)"\n' > a.sh; chmod +x a.sh; ./a.sh
printf '#!/bin/bash\necho "interpreter: $(ps -o comm= -p $$)"\n' > b.sh; chmod +x b.sh; ./b.sh
printf '#!/bin/sh\necho "interpreter: $(ps -o comm= -p $$)"\n' > c.sh; chmod +x c.sh; ./c.sh2 — Find out what your /bin/sh actually is.
ls -l /bin/sh
/bin/sh --version 2>&1 | head -13 — Test whether bashisms fail under it.
printf 'arr=(one two three)\necho "${arr[1]}"\n' > bash1.sh
bash bash1.sh
sh bash1.shIf both work, your /bin/sh is Bash and you cannot test POSIX compliance here.
4 — Break the interpreter.
printf '#!/bin/nosuchshell\necho hi\n' > bad.sh; chmod +x bad.sh
./bad.sh; echo "status: $?"Read which file the message names.
5 — Reproduce the carriage-return failure.
printf '#!/bin/bash\r\necho hi\r\n' > crlf.sh; chmod +x crlf.sh
./crlf.sh
file crlf.sh
tr -d '\r' < crlf.sh > fixed.sh && chmod +x fixed.sh && ./fixed.sh6 — Compare the two bash shebangs.
printf '#!/bin/bash\necho "abs: $BASH_VERSION"\n' > abs.sh; chmod +x abs.sh; ./abs.sh
printf '#!/usr/bin/env bash\necho "env: $BASH_VERSION"\n' > env.sh; chmod +x env.sh; ./env.sh
command -v -a bash 2>/dev/null || which -a bashIf you have more than one Bash, the two may report different versions.
7 — Put options in the shebang, then move them.
printf '#!/bin/bash -eu\necho "flags: $-"\n' > f1.sh; chmod +x f1.sh; ./f1.sh
printf '#!/bin/bash\nset -eu\necho "flags: $-"\n' > f2.sh; chmod +x f2.sh; ./f2.sh
bash f1.sh
bash f2.shThe fourth keeps its options; the third does not.
8 — Confirm the shebang is ignored when sourced.
printf '#!/bin/sh\necho "running under: $(ps -o comm= -p $$)"\n' > s.sh
./s.sh
. ./s.sh9 — Clean up. cd /tmp && rm -rf /tmp/sb
Check Your Understanding
1. What exactly do the first two characters of a script do, and who reads them?
2. A script with no shebang behaves differently on two machines. Why?
3. Why is #!/bin/sh on a script containing arrays a problem that your own testing may never reveal?
4. bad interpreter: /bin/bash^M. What is wrong and what is the fix?
5. When would you prefer #!/usr/bin/env bash to #!/bin/bash?
6. Why put set -euo pipefail on its own line rather than in the shebang?
9. Remember
#!tells the kernel which program should run the file, and the rest of the line is that program's absolute path.- The kernel reads it, so the path cannot be looked up on
PATHand the line must be first. - It is ignored when the file is sourced or handed to an interpreter.
- With no shebang the script runs under
sh— the most basic shell on the system, not the one you wrote it in. #!/bin/shand#!/bin/bashare not interchangeable.shisdashon Debian, Bash on macOS and Red Hat,ashon Alpine.- Arrays,
[[ ]], brace ranges, substrings,<<<and$'…'are all Bash, not POSIX. Chapter 21.6 lists the rest and measures which of them fail where. - You cannot test POSIX compliance where
/bin/shis Bash. Usedash -n script.sh. - Declare
#!/bin/bashunless you have checked. Sayingshdoes not make a script portable. #!/usr/bin/env bashfinds Bash onPATH— better for shared scripts and newer Bash versions, worse undercronandsudo.- A missing interpreter gives "bad interpreter" and status 127, and the message names the interpreter rather than your script.
- A
^Min that message means Windows line endings.tr -d '\r'. - Put
setoptions on their own line, not in the shebang. - Any program that reads a file can be a shebang interpreter — Python, Perl, Tcl,
awk.
The interpreter is settled. The next chapter is what it hands your script when it starts — and what your script cannot hand back.
Where this fits
Part of the Linux & Bash curriculum.
