Bash Shebang Explained: #!/bin/bash vs /usr/bin/env bash

Tested on RHEL 10.2 (Coughlan)
Package bash 5.2.26-6.el10
coreutils 9.5-8.el10_2
Applies to Linux distributions with Bash installed
Privilege Normal user; no root required for the examples
Scope Bash shebang syntax and execution, #!/bin/bash vs #!/usr/bin/env bash, Bash vs sh, interpreter options, and common shebang errors. Does not cover Bash language features or strict-mode options — see the linked shell scripting lessons.
Related guides Bash shell script tutorial
Shell scripting course hub
Short on time?Beta Get the gist of this 11-minute read in seconds, then ask follow-up questions

In this article, you'll learn what #!/bin/bash means, when Linux reads the shebang, and how it differs from #!/usr/bin/env bash.

Use #!/bin/bash when Bash is known to exist at /bin/bash and you want a predictable interpreter path. Use #!/usr/bin/env bash when the script must run across environments where Bash may be installed elsewhere; env finds the first bash on PATH. Neither form is universally better — the tradeoff is fixed-path predictability versus PATH-based portability.


What Is a Bash Shebang and What Does #!/bin/bash Mean?

A shebang (also called a hashbang) is the #! at the beginning of a script followed by the interpreter to use. For Bash, the common form is:

text
#!/bin/bash
echo "Hello"

Nothing can appear before #! — not a blank line and, depending on how the file was created, not a UTF-8 BOM. The kernel only recognizes the shebang when those two bytes are the first bytes of the file.

When you execute the script directly (./script.sh), #! has a special meaning to the operating system: Linux uses it to determine which interpreter should start. Linux reads the interpreter path from the shebang and starts that interpreter with the script file. Once Bash reads the file, the shebang line itself is treated like a comment and does not execute as a Bash command—see Bash comments for how ordinary # lines differ from #! on line 1.


When Is the Shebang Actually Used?

The shebang matters only when the operating system executes the file as a program — typically ./script.sh or script.sh when the file is on PATH, has the execute bit set, and is not a binary format the kernel runs directly.

How you invoke the file Shebang used? What runs the script
./script.sh Yes Kernel reads shebang, starts listed interpreter
script.sh (on PATH, executable) Yes Same kernel path
bash script.sh No Bash started explicitly from the command line
sh script.sh No sh started explicitly from the command line
source script.sh or . script.sh No Current shell reads the file

That split explains a common troubleshooting question: why does bash script.sh work when ./script.sh fails? Sourcing a script into the current shell skips the shebang for the same reason — the kernel is not exec'ing the file as a program. Direct execution needs execute permission, a valid interpreter path on line 1, and Unix line endings. A broken shebang or missing execute permission prevents direct execution before Bash starts reading the script.


#!/bin/bash

#!/bin/bash is a common shebang for Bash scripts on Linux servers. It tells the kernel to run /bin/bash and pass your script as an argument.

On RHEL-family systems, /bin/bash and /usr/bin/bash are usually the same file. Either path works when that binary exists:

bash
ls -l /bin/bash /usr/bin/bash
output
-rwxr-xr-x. 1 root root 1410656 Oct 29  2024 /bin/bash
-rwxr-xr-x. 1 root root 1410656 Oct 29  2024 /usr/bin/bash

Save the following as fixed.sh:

text
#!/bin/bash
printf 'Bash version: %s\n' "$BASH_VERSION"

Mark the file executable and run it by path:

bash
chmod +x fixed.sh && ./fixed.sh
output
Bash version: 5.2.26(1)-release

Why use a fixed path?

  • Interpreter selection does not depend on PATH.
  • It works well on systems where /bin/bash is guaranteed to exist.

Weakness: if /bin/bash does not exist on the target system, direct execution fails even when Bash is installed elsewhere.


#!/usr/bin/env bash

#!/usr/bin/env bash asks /usr/bin/env to find bash on PATH and run it. Use which command or command -v bash to see which binary env will pick before you rely on PATH in production. This avoids hard-coding Bash's location, although it still assumes env is available at /usr/bin/env.

Save the same body in envpath.sh with an env shebang:

text
#!/usr/bin/env bash
echo "env bash: $(command -v bash) version $BASH_VERSION"

Make it executable and run it the same way:

bash
chmod +x envpath.sh && ./envpath.sh
output
env bash: /usr/bin/bash version 5.2.26(1)-release

On a typical Linux server, env finds the same Bash as #!/bin/bash. The difference shows up when Bash lives outside /bin.

The official Bash Docker image installs Bash at /usr/local/bin/bash, not /bin/bash, and documents #!/usr/bin/env bash for scripts. A script with #!/bin/bash fails on direct exec there; #!/usr/bin/env bash works because /usr/local/bin is on PATH.

Use this form when Bash is installed somewhere other than /bin, as long as it is on PATH.

Weakness: env runs the first bash it finds in the caller's PATH. If another Bash appears earlier in PATH, that version will be selected.


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

Factor #!/bin/bash #!/usr/bin/env bash
Interpreter location Fixed at /bin/bash First bash on PATH
PATH at exec time Ignored for interpreter choice Matters — env searches PATH
Typical RHEL/Ubuntu server Works Works (usually same Bash)
Minimal/container images without /bin/bash Fails on direct exec Works if bash is on PATH
Predictability High Depends on environment
Passing an interpreter option #!/bin/bash -x #!/usr/bin/env -S bash -x where env -S is supported

Prepend a directory with a fake bash ahead of the real one and run the env script again:

Create a fake bash in a local directory:

bash
mkdir -p fakebin && printf '#!/bin/sh\necho FAKE_BASH\n' > fakebin/bash && chmod +x fakebin/bash

Prepend that directory to PATH and run envpath.sh again:

bash
PATH="$(pwd)/fakebin:$PATH" ./envpath.sh
output
FAKE_BASH

Because env searches PATH, it now finds fakebin/bash before the system Bash.

This shows the tradeoff with env: Bash can live in different locations, but the version that runs depends on PATH.


#!/bin/sh vs #!/bin/bash

#!/bin/sh and #!/bin/bash tell the kernel which interpreter path to use when the script is executed directly:

Shebang Meaning
#!/bin/bash Require Bash
#!/bin/sh Use the system's POSIX-style sh
#!/usr/bin/env bash Find Bash through PATH

If your script uses Bash arrays, [[ … ]], process substitution such as <(…), or other Bash-only tests from Bash [ ] vs [[ ]] vs (( )), use #!/bin/bash or #!/usr/bin/env bash instead of #!/bin/sh. Bash also supports source; portable sh uses . instead.

Check what sh is on the host:

bash
readlink -f /bin/sh
output
/usr/bin/bash

On this RHEL host, /bin/sh points at Bash. On Ubuntu and Debian, /bin/sh is often Dash, a smaller POSIX shell. Even if the file starts with #!/bin/bash, running sh script.sh explicitly bypasses that shebang and asks the system's sh to parse the file.

A script that uses Bash arrays illustrates the risk:

text
#!/bin/bash
names=(alice bob)
echo "${names[0]}"

Direct execution uses Bash because of the shebang. On a system where sh is Dash, sh script.sh can fail with a syntax error even though ./script.sh works. For a fuller comparison of Bash and sh, see Bash vs sh.


Why Does bash script.sh Work When ./script.sh Fails?

Explicit interpreter overrides the shebang

Call Bash on the script file directly — no execute bit required:

bash
bash fixed.sh
output
Bash version: 5.2.26(1)-release

You chose Bash explicitly. A broken shebang does not stop this invocation.

Prove that with a missing interpreter path:

Save this as badpath.sh with a deliberately broken interpreter path:

text
#!/nonexistent/bash
echo "body still runs with bash badpath.sh"

Try direct execution first:

bash
chmod +x badpath.sh && ./badpath.sh
output
bash: ./badpath.sh: cannot execute: required file not found

Direct execution fails because the kernel cannot run /nonexistent/bash. Call Bash yourself and the script body runs:

bash
bash badpath.sh
output
body still runs with bash badpath.sh

No shebang on direct execution

If an executable text file has no shebang, Linux may reject direct execution. Some shells then try to interpret the file themselves, so the result can vary depending on which shell launched it. Do not rely on this fallback; executable scripts should include the intended interpreter.

Permission denied is not a shebang error

Without the execute bit, the kernel never reaches shebang parsing:

bash
chmod -x fixed.sh && ./fixed.sh
output
bash: ./fixed.sh: Permission denied

bash fixed.sh still works. Fix with chmod +x — see Linux file permissions for the full mode model.


Passing Options in a Shebang: env -S

Linux supports one optional shebang argument, so #!/bin/bash -x works on Linux. Shebang argument handling differs across Unix-like systems, so avoid assuming complex interpreter arguments are portable.

To start Bash in trace mode with a fixed path:

text
#!/bin/bash -x

When env must locate Bash, use -S so GNU env can split the argument string:

text
#!/usr/bin/env -S bash -x

On Linux, the kernel passes bash -x after /usr/bin/env as one argument. Without -S, env therefore looks for a command literally named bash -x and typically fails. GNU env -S splits it into bash and -x — documented in Coreutils.

Save a traced script as trace-env.sh:

text
#!/usr/bin/env -S bash -x
echo "traced via env -S"

Run it and watch stderr for the + prefix Bash prints before each command:

bash
chmod +x trace-env.sh && ./trace-env.sh 2>&1 | head -3
output
+ echo 'traced via env -S'
traced via env -S

Use this form only when you actually need to pass options through env; older or non-GNU implementations may not support -S.


Common Bash Shebang Errors

bad interpreter / wrong path

When ./script.sh fails immediately, the kernel could not start the interpreter listed on line 1. Depending on the shell and system, you may see errors such as:

  • bad interpreter: No such file or directory
  • cannot execute: required file not found

Both usually mean the interpreter path is wrong or corrupted (see CRLF below). bash script.sh can still work because it never uses the shebang.

^M / CRLF line endings

Scripts edited on Windows often get \r\n line endings. The shebang line may be read as #!/bin/bash\r, so the kernel looks for a nonexistent interpreter.

Create a script with CRLF line endings and confirm the file format:

bash
printf '#!/bin/bash\r\necho crlf-test\r\n' > crlf.sh && chmod +x crlf.sh && file crlf.sh
output
crlf.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators

Check the shebang itself with cat -A:

bash
head -1 crlf.sh | cat -A && ./crlf.sh
output
#!/bin/bash^M$
bash: ./crlf.sh: cannot execute: required file not found

The ^M at the end of the shebang line is a carriage return. Strip CRLF and run again:

bash
sed -i 's/\r$//' crlf.sh && ./crlf.sh
output
crlf-test

For shell scripts in a shared repository, add a .gitattributes rule so Git always checks out Unix line endings:

text
*.sh text eol=lf

Configure editors and Git line-ending settings as a secondary safeguard on teams that mix Windows and Linux.

Wrong interpreter (sh vs bash)

A script with #!/bin/sh that uses Bash arrays, [[ … ]], process substitution, or other Bash-specific syntax may fail on systems where /bin/sh is Dash. The error is a shell syntax message, not a shebang parse failure — but the root cause is the shebang picking the wrong interpreter. See the #!/bin/sh vs #!/bin/bash section above.


Which Bash Shebang Should You Use?

Context Recommendation
Scripts on controlled Linux servers #!/bin/bash — predictable when /bin/bash is guaranteed to exist
Scripts shared across systems where Bash may be installed in different locations #!/usr/bin/env bash — useful when Bash may not be under /bin; make sure the intended Bash is on PATH
Container / minimal image Check where Bash is installed; use #!/usr/bin/env bash when Bash is on PATH but not at /bin/bash

Within one project, use the same shebang style unless a script has a specific reason to differ.


Summary

The Bash shebang is the #! line at the top of a script. When you run ./script.sh, the Linux kernel reads that line and starts the listed interpreter with your file as an argument. This differs from bash script.sh, where you explicitly start Bash and the shebang does not select the interpreter.

On servers you control, #!/bin/bash is predictable: the kernel always runs /bin/bash and never consults PATH. #!/usr/bin/env bash helps when Bash lives outside /bin, which is common in container images, but env follows whatever PATH the caller exported. Neither form is always better. Use a fixed path when you control where Bash is installed; use env when Bash may be installed elsewhere and you can rely on PATH.

When ./script.sh fails immediately, check execute permission, Unix line endings (file or cat -A on line 1), and the interpreter path before you debug script logic. bad interpreter and ^M errors point at the shebang layer. bash script.sh can still run the body because it never uses that line.


References


Frequently Asked Questions

Is #!/usr/bin/bash the same as #!/bin/bash?

On many mainstream Linux distributions, /bin/bash and /usr/bin/bash resolve to the same Bash executable, but this is not guaranteed. The path in a fixed shebang must exist and be executable on the system where the script runs. When both resolve to the same executable, either path works.

Does the shebang work without execute permission?

No. Direct execution (./script.sh) requires the execute bit (chmod +x). Without it you get Permission denied before the shebang is used. You can still run bash script.sh without execute permission.

Why does bash script.sh work when ./script.sh fails?

When you run bash script.sh, you start Bash explicitly, so the kernel never reads the shebang. ./script.sh needs execute permission, a valid interpreter path on line 1, and Unix line endings. Fix those three before debugging script logic.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)