Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/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:

What happens when you run a script by its own name. The kernel opens the file and reads its first two characters. If they are the hash and exclamation mark, the rest of that line names a program, and the kernel runs that program with the script as its argument. If they are not, the shell falls back to running the file itself. And if the named program does not exist, nothing runs at all.01THE KERNEL OPENS THE FILEand reads only the first two characters02ARE THEY HASH AND EXCLAMATION MARK?that pair is the whole of the marker03IF SO, THE REST OF THE LINE NAMES A PROGRAMan absolute path, not a command to look up04THAT PROGRAM RUNS, GIVEN YOUR FILEwhich is why the interpreter must exist05IF THE PROGRAM IS MISSING, NOTHING RUNSand the error names the interpreter, not your script
Figure 1 — what happens when you run a script by its own name. The kernel opens the file and reads only its first two characters. If they are the hash and exclamation mark, the rest of that line names a program, and the kernel runs that program with your file as its argument. Otherwise the shell falls back to running the file itself. And if the named program is missing, nothing runs — the error names the interpreter, not your script.

So ./build.sh with #!/bin/bash at the top becomes, in effect:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
/bin/bash ./build.sh

The # 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cat noshebang.sh
echo "interpreter: $(ps -o comm= -p $$)"

$ ./noshebang.sh
interpreter: sh

It 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash      # the Bash shell, specifically
#!/bin/sh        # whatever the system's POSIX shell is

They 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, Ubuntudash — small, fast, strictly POSIX
Red Hat, Fedorabash in POSIX mode
macOSbash 3.2 in sh mode
Alpinebusybox ash

The things that stop working under a strict sh are the ones you have been using throughout:

FeatureTaught inPOSIX?
ArraysChapter 19.2No
[[ … ]]Module 16No
{1..5} brace ranges9.5No
${v:0:3} substrings8.3No
$'…'9.3No
<<< here-strings12.4No
local in functionsChapter 18.5Widely supported, not POSIX
$(…), $((…)), ${v:-x}Modules 8–9Yes

4. The env Form

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/usr/bin/env bash

env looks up bash on PATH and runs the first one it finds (Chapter 10.2), rather than insisting on /bin/bash.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./envform.sh
env form works; BASH_VERSION=3.2.57(1)-release
#!/bin/bash#!/usr/bin/env bash
Finds Bash in an unusual placeNoYes
Picks up a newer Bash on PATHNoYes
PredictableYesDepends on the caller's PATH
Works with a scrubbed environmentYesCan 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cat bad.sh
#!/bin/nosuchshell
echo hi

$ ./bad.sh
bash: ./bad.sh: bad interpreter: /bin/nosuchshell: no such file or directory

$ echo $?
127

The 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ tr -d '\r' < crlf.sh > fixed.sh && chmod +x fixed.sh && ./fixed.sh
hi

6. Arguments in the Shebang

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash -eu

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
set -euo pipefail

Put 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/usr/bin/env python3
#!/usr/bin/perl -w
#!/usr/bin/awk -f
#!/usr/bin/env tclsh

Any 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/usr/bin/env bash
#
# A script meant to be shared, that needs a modern Bash.
set -euo pipefail
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/sh
#
# A genuinely POSIX script — no arrays, no [[ ]], no brace ranges.
# Checked against dash, not just against a Mac.
set -eu

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
dash -n script.sh        # parse only, no execution — Chapter 10.6's set -n

dash is installable on macOS and standard on Debian. One command tells you whether the claim is true.

Practice

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mkdir -p /tmp/sb && cd /tmp/sb

1 — Find out what runs a script with no shebang.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.sh

2 — Find out what your /bin/sh actually is.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ls -l /bin/sh
/bin/sh --version 2>&1 | head -1

3 — Test whether bashisms fail under it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'arr=(one two three)\necho "${arr[1]}"\n' > bash1.sh
bash bash1.sh
sh   bash1.sh

If both work, your /bin/sh is Bash and you cannot test POSIX compliance here.

4 — Break the interpreter.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.sh

6 — Compare the two bash shebangs.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 bash

If you have more than one Bash, the two may report different versions.

7 — Put options in the shebang, then move them.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.sh

The fourth keeps its options; the third does not.

8 — Confirm the shebang is ignored when sourced.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf '#!/bin/sh\necho "running under: $(ps -o comm= -p $$)"\n' > s.sh
./s.sh
. ./s.sh

9 — 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 PATH and 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/sh and #!/bin/bash are not interchangeable. sh is dash on Debian, Bash on macOS and Red Hat, ash on 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/sh is Bash. Use dash -n script.sh.
  • Declare #!/bin/bash unless you have checked. Saying sh does not make a script portable.
  • #!/usr/bin/env bash finds Bash on PATH — better for shared scripts and newer Bash versions, worse under cron and sudo.
  • A missing interpreter gives "bad interpreter" and status 127, and the message names the interpreter rather than your script.
  • A ^M in that message means Windows line endings. tr -d '\r'.
  • Put set options 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.