Bash vs sh: Differences and Which Shell to Use

Tested on RHEL 10.2 (Coughlan)
Package bash 5.2.26-6.el10
dash 0.5.11.5-4.el10_1
busybox 1:1.37.0-4.el10_2
ShellCheck 0.10.0
Applies to Linux distributions with Bash installed
Privilege Normal user; no root required
Scope Bash vs POSIX sh, common Bash-only syntax, /bin/sh differences across Linux distributions, portability testing with dash and ShellCheck, and choosing the correct shebang. Does not cover Zsh/Fish or full shebang portability — see the linked shebang guide.
Related guides Bash shell script tutorial
Shell scripting course hub
Linux command line basics
Bash if else
Short on time?Beta Get the gist of this 14-minute read in seconds, then ask follow-up questions

This article compares Bash and POSIX sh using the same scripts under different interpreters. It also shows how to check what /bin/sh points to and how common Bash-only syntax behaves under dash.

Many guides treat sh and Bash as interchangeable because both names appear in tutorials. On some Linux servers they even run the same binary. On Debian, Ubuntu, and many minimal container images they do not. That mismatch is why a script works in your terminal and fails in CI or inside an Alpine image.


Bash vs sh in One Minute

Question Pick POSIX sh (#!/bin/sh) Pick Bash (#!/bin/bash)
Target environment Unknown Unix, minimal Docker images, Debian/Ubuntu default sh Modern Linux admin hosts where Bash is guaranteed
Syntax you need [ ], case, ., $(), $(( )), here documents Arrays, [[ ]], =~, {a,b}, <( ), source, (( )) as a statement
Portability goal Script must survive when /bin/sh is dash or BusyBox ash You accept Bash as a dependency
Readability trade-off More verbose tests; no regex in [[ ]] Shorter conditionals and richer data structures

For scripts you control on Linux systems where Bash is guaranteed, Bash is usually the easier default. Choose sh when portability or a minimal runtime is a requirement.

#!/bin/bash explicitly requires Bash, so Bash-specific syntax is valid. #!/bin/sh asks the system's sh implementation to run the script, so the script should stay within the POSIX shell command language rather than relying on Bash extensions. See Bash shebang explained for path and kernel details.


What Does sh Actually Mean?

sh can refer to several related things:

  • POSIX shell command language — the standardized scripting dialect defined by POSIX: if, case, while, . (dot), $(), $(( )), and [ for tests. In discussions such as “write this for sh,” sh commonly means this POSIX-compatible language, not a single vendor program.
  • sh on an actual system — the sh command is provided by a concrete shell implementation. Historically that was the Bourne shell; on modern Linux it is often dash, BusyBox ash, or Bash invoked as sh.
  • /bin/sh on disk — the shell your operating system provides as its default sh. It may be a real executable or a symlink to another shell.

On Red Hat Enterprise Linux 10, /bin/sh is a symlink to Bash:

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

On Debian and Ubuntu, /bin/sh usually points to dash, a small POSIX-focused shell. Alpine Linux and many container images ship BusyBox ash as /bin/sh. All are intended to support POSIX-style shell scripts, but each implementation may provide different non-POSIX extensions.

So when someone says “make it POSIX,” they mean “avoid Bash-only syntax,” not “run it with whatever /bin/sh happens to be on my laptop.” Always test against a separate POSIX-oriented shell implementation such as dash when portability matters.


What Is Bash?

Bash (Bourne Again Shell) is a concrete program: GNU Bash on most Linux distributions. It implements the POSIX shell command language and adds extensions often called bashisms. Bash is also a feature-rich interactive shell (history, job control, command-line editing), but this article focuses on scripting and portability.

Bash is useful for Linux administration and automation when you need features that POSIX sh does not provide:

  • Indexed arrays and associative arrays
  • [[ … ]] with pattern and regex matching (=~)
  • Brace expansion ({a,b})
  • Process substitution (<(command))
  • The source keyword (POSIX uses . only)
  • (( expression )) as a compound command, not only $(( )) arithmetic expansion

Bash is not guaranteed on every Unix. It is standard on RHEL-family servers and common on developer workstations, but slim container images may omit it or place it outside default PATH.


Bash vs sh Feature Comparison

The table shows which common shell features are defined by POSIX and which require Bash. Keep in mind that older shell versions may not yet implement features added in newer POSIX revisions. Arrays and [[ ]] are common indicators that a script is not written for POSIX sh.

Feature POSIX sh language Bash
Command substitution $() Yes Yes
Arithmetic expansion $(( )) Yes Yes
(( )) compound command (assign inside) No Yes
[ ] test builtin Yes Yes
[[ ]] No Yes
Regex =~ inside [[ ]] No Yes
Indexed arrays arr=(a b) No Yes
Brace expansion {a,b} No Yes
Process substitution <(cmd) No Yes
source file No (use . file) Yes
Associative arrays declare -A No Yes
Here-string <<< No Yes
set -o pipefail Yes in POSIX.1-2024 Yes

POSIX.1-2024 includes pipefail, but older shells may not support it yet. For example, the tested dash 0.5.11.5 build rejects set -o pipefail, so check the shell version on systems where you plan to use it.

Two features often mislabeled as Bash-only:

  • Command substitution with $() is POSIX. Both Bash and dash accept $(echo hello).
  • Arithmetic expansion with $(( )) is POSIX. Both shells print the same result for echo $((2 + 3)).

What Bash adds is the (( )) compound command form, where you can assign and test without $(( )) wrapping:

bash
(( n = 4 + 2 )); echo "compound n=$n"
output
compound n=6

Under this dash version, the text is parsed as ordinary shell syntax rather than Bash arithmetic, so dash reports n: not found and no assignment happens:

bash
dash -c '(( n = 4 + 2 )); echo "compound n=$n"'
output
dash: 1: n: not found
compound n=

Arithmetic expansion still works in dash:

bash
dash -c 'echo "arith $((4 + 2))"'
output
arith 6

For portable math, prefer $(( )) in sh scripts. Use Bash (( )) only when you target Bash explicitly.


Check Whether /bin/sh Uses Bash, Dash, or Another Shell

Before assuming what #!/bin/sh or #!/bin/bash will run, check how those paths resolve on your system.

List the shells available on your PATH:

bash
command -v bash sh dash 2>/dev/null
ls -l /bin/sh /bin/bash 2>/dev/null

Sample output on RHEL 10:

output
/usr/bin/bash
/usr/bin/sh
/usr/bin/dash
-rwxr-xr-x. 1 root root 1410656 Oct 29  2024 /bin/bash
lrwxrwxrwx. 1 root root       4 Oct 29  2024 /bin/sh -> bash

/bin/bash is the Bash executable. /bin/sh is a symlink to bash in the same directory, so both paths reach GNU Bash on this system. Because /bin/sh runs Bash on this system, Bash-only syntax may appear to work until the script is moved to a system where /bin/sh uses another shell.

Print version strings where supported:

bash
bash --version | head -1
output
GNU bash, version 5.2.26(1)-release (x86_64-redhat-linux-gnu)

dash does not implement --version; check the package instead (rpm -q dash on RHEL, dpkg -l dash on Debian/Ubuntu).

On Debian or Ubuntu, readlink -f /bin/sh should show /usr/bin/dash or /bin/dash. Treat that path as your portability baseline, not /bin/bash.


Run the Same Script with Bash and sh

Create a small script in the current directory that deliberately uses Bash extensions:

bash
cat > test-bashisms.sh <<'SCRIPT'
#!/bin/bash
arr=(one two)
echo "array: ${arr[0]}"
[[ -n "foo" ]] && echo "double bracket ok"
[[ "abc123" =~ ^[a-z]+[0-9]+$ ]] && echo "regex ok"
cat <(echo "process substitution")
echo {a,b}
source /dev/null 2>/dev/null || true
SCRIPT

Run it with Bash first:

bash
bash test-bashisms.sh
output
array: one
double bracket ok
regex ok
process substitution
a b

All of these constructs succeed under Bash.

On this RHEL system, sh resolves to GNU Bash. When Bash is invoked as sh, it switches to POSIX mode, but that mode does not disable every Bash extension. Arrays and [[ ]] can therefore still appear to work:

bash
sh test-bashisms.sh
output
array: one
double bracket ok
regex ok
process substitution
a b

That result is misleading for portability. Run the file with dash, the shell commonly used as /bin/sh on Debian and Ubuntu, to expose common non-POSIX Bash syntax:

bash
dash test-bashisms.sh
output
test-bashisms.sh: 3: Syntax error: "(" unexpected

dash stops at the array declaration, so the later Bash-specific constructs are never evaluated. Test them separately to see how each one fails:

bash
dash -c 'arr=(one two)' 2>&1
output
dash: 1: Syntax error: "(" unexpected

Double-bracket tests are a separate builtin dash does not provide:

bash
dash -c '[[ -n foo ]]' 2>&1
output
dash: 1: [[: not found

Process substitution uses the same ( token dash rejects:

bash
dash -c 'cat <(echo hi)' 2>&1
output
dash: 1: Syntax error: "(" unexpected

Brace expansion is a Bash feature; dash prints the braces literally:

bash
dash -c 'echo {a,b}'
output
{a,b}

Some Bash-specific syntax fails silently rather than causing an error. With brace expansion, dash simply prints the braces literally, which can let the script continue with the wrong result.

The Bash source keyword also fails because POSIX sh uses the dot command (.) instead:

bash
dash -c 'source /dev/null' 2>&1
output
dash: 1: source: not found

POSIX sh expects . /dev/null instead of source.

BusyBox ash (common in Alpine-based images) rejects arrays the same way:

bash
busybox sh -c 'arr=(one two)' 2>&1
output
sh: syntax error: unexpected "("

BusyBox features can vary depending on how it was built, but treating arrays and Bash [[ ]] as non-portable is safe. When you need Bash syntax, set #!/bin/bash and install Bash in the image rather than assuming /bin/sh is Bash.


Check a Script for Bash-Only Syntax

Before you ship a #!/bin/sh script, check whether it is written in POSIX syntax or only happens to work on your laptop.

Run ShellCheck in POSIX mode against the same file:

bash
shellcheck -s sh test-bashisms.sh

-s sh tells ShellCheck to check the script as POSIX sh. ShellCheck checks the language, but it cannot tell you which POSIX features are available in every shell version you deploy to, so also run the script with the shell you expect to use.

Relevant warnings include:

output
In test-bashisms.sh line 2:
arr=(one two)
    ^-------^ SC3030 (warning): In POSIX sh, arrays are undefined.


In test-bashisms.sh line 3:
echo "array: ${arr[0]}"
             ^-------^ SC3054 (warning): In POSIX sh, array references are undefined.


In test-bashisms.sh line 4:
[[ -n "foo" ]] && echo "double bracket ok"
^------------^ SC3010 (warning): In POSIX sh, [[ ]] is undefined.


In test-bashisms.sh line 5:
[[ "abc123" =~ ^[a-z]+[0-9]+$ ]] && echo "regex ok"
^-- SC3010 (warning): In POSIX sh, [[ ]] is undefined.


In test-bashisms.sh line 6:
cat <(echo "process substitution")
    ^----------------------------^ SC3001 (warning): In POSIX sh, process substitution is undefined.


In test-bashisms.sh line 7:
echo {a,b}
     ^---^ SC3009 (warning): In POSIX sh, brace expansion is undefined.


In test-bashisms.sh line 8:
source /dev/null 2>/dev/null || true
^--------------------------^ SC3046 (warning): In POSIX sh, 'source' in place of '.' is undefined.

ShellCheck also reported style notes on the literal test strings; the portability warnings above are what matter for #!/bin/sh scripts. For a script intended to run as POSIX sh, use both checks: shellcheck -s sh script.sh, followed by a runtime test such as dash script.sh.


Portability: When #!/bin/sh Is Better

Choose POSIX sh when portability matters or Bash may not be installed:

  • Minimal containers — Alpine normally provides BusyBox ash as /bin/sh. Standard Distroless images do not include a shell, so neither #!/bin/sh nor #!/bin/bash works unless you intentionally add one.
  • Installer and bootstrap scripts — portable installers and system scripts often use /bin/sh to minimize shell dependencies across environments.
  • Cross-Unix software — software intended to run or build across BSD, illumos, Linux, and other Unix-like systems often uses POSIX sh for portability.

POSIX sh scripts should use [ ] instead of [[ ]], . instead of source, and case or external tools instead of =~. Test with dash test.sh even when your laptop maps sh to Bash. The Bash shebang guide covers when the kernel reads #!/bin/sh versus when you invoke bash script.sh explicitly.


When Bash Is the Better Choice

Bash is often the more practical choice for Linux administration, automation, and CI/CD scripts when Bash is guaranteed to be available:

  • Arrays and associative arrays for service lists, IP sets, and parsed fields
  • [[ ]] and =~ for readable conditionals without external grep
  • Brace expansion and process substitution for one-liner pipelines

If the script uses any of those features, document Bash as a requirement (#!/bin/bash) and add Bash to container images that need the script. Do not rely on /bin/sh being a symlink to Bash.


Bash POSIX Mode Is Not the Same Question

Running bash --posix or set -o posix enables stricter POSIX behavior in Bash, but it does not turn Bash into dash. On RHEL 10, this still succeeds:

bash
bash --posix -c 'arr=(one two); echo ${arr[0]}'
output
one

POSIX mode changes several areas where Bash's normal behavior differs from POSIX, but it does not remove all Bash extensions.

For portability testing, invoke dash or BusyBox sh directly. For production POSIX scripts, write POSIX syntax and use #!/bin/sh. Use bash --posix when you specifically need to see how Bash behaves in POSIX mode. Do not use it as a substitute for testing the script with another sh implementation such as dash.


Decision Checklist

Use this checklist when starting a new script:

  • Could this run on systems or containers where Bash is not guaranteed? → Use POSIX sh and test with the target shell.
  • Does the script use arrays, double-bracket tests, regex match, brace expansion, or process substitution? → #!/bin/bash and ensure Bash is in the image.
  • Is your local sh Bash but dash in CI? → Run dash script.sh locally before merge.
  • Do you only need command substitution and arithmetic expansion? → Either shell works; still pick one shebang and stay consistent.
  • Is the script only for RHEL-family systems where Bash is already available? → Bash is usually the simpler choice.

If portability is not a requirement and Bash is guaranteed on the target systems, Bash is usually the more practical starting point. Move to POSIX sh when the runtime or portability requirements demand it.


Summary

Bash and sh are not two names for the same program. POSIX sh describes a portable language; /bin/sh is whichever shell your OS selected. On RHEL, that is often Bash, which hides Bash-only syntax until the same file runs under dash on Ubuntu or BusyBox ash in a container.

The cross-shell tests above show common portability problems with arrays, [[ ]], regex matching with =~, process substitution, brace expansion, and source. Command substitution $() and arithmetic expansion $(( )) are defined by POSIX and work in Bash and dash. Bash’s extra (( )) compound command is not a portable substitute for $(( )).

Pick #!/bin/bash when you need Bash features and control the runtime. Pick #!/bin/sh when the script must run across systems where Bash may not be available. Test with readlink -f /bin/sh on each target platform, run shellcheck -s sh script.sh, then validate with dash even if sh currently points at Bash on your workstation.


References


Frequently Asked Questions

Is /bin/sh Bash on Ubuntu?

Usually no. Debian and Ubuntu normally link /bin/sh to dash, which is a small shell designed for POSIX-style scripting. Use readlink -f /bin/sh on the system where the script will run. RHEL-family releases often symlink /bin/sh to Bash instead.

Can every Bash script run with sh?

No. A script using Bash arrays, [[ ]], =~, brace expansion, process substitution, or source is not a portable sh script. Run it with Bash if it was written for Bash, or rewrite those constructs for POSIX sh. When you run bash script.sh, Bash interprets the file directly and the shebang is ignored.

Is Bash a superset of sh?

Informally, Bash is often described as a superset of POSIX sh because it implements the POSIX shell command language and adds many extensions. Bash's default behavior is not identical to POSIX in every case, and Bash also has a POSIX mode that changes some behaviors to more closely follow the POSIX specification. When portability matters, write to the POSIX shell syntax rather than relying on extra features that happen to work in your local /bin/sh.

Is $() Bash-specific?

No. POSIX shells require command substitution with $(). Both Bash and dash accept $(command). Older Bourne shells used backticks; new scripts should prefer $().

Should Docker entrypoint scripts use sh or Bash?

Use #!/bin/sh when the image ships a POSIX shell (for example BusyBox ash on Alpine) and the script stays POSIX. Use #!/bin/bash when you need Bash features and install Bash in the image. Standard Distroless images do not include a shell, so neither #!/bin/sh nor #!/bin/bash works unless you intentionally add one. Use a non-shell entrypoint instead when possible.

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)