A deploy script whose first line is `#!/bin/sh` runs fine on a RHEL host but on Debian fails with `[[: not found` and `Syntax error: "(" unexpected`. What is /bin/sh on each system, and why do those lines fail on one and not the other?
answer
- same shebang, two different programs
- check what /bin/sh points to
- Debian and Ubuntu link sh to dash
- bash as sh keeps its keywords
- [[ ]] and arrays are bash-only
basics
~20 sOn RHEL /bin/sh is a symlink to bash, which still accepts [[ ]] and arrays even when invoked as sh. On Debian and Ubuntu /bin/sh is dash, a small strict POSIX shell that has neither. The script was never portable; RHEL hid the bug.
solid answer
~50 s`#!/bin/sh` does not name a language, it names whatever program that path points to. On RHEL and Fedora `/bin/sh` is a symlink to bash: bash notices it was invoked as `sh` and turns on POSIX mode, but POSIX mode only changes startup files and a handful of behaviours — it does **not** remove bash keywords, so `[[ ]]` and `arr=(a b c)` still work. On Debian and Ubuntu `/bin/sh` is dash, a deliberately small Almquist-shell descendant that implements little more than the POSIX shell command language. `[[` is not a keyword there, so dash looks for a command called `[[` and reports `[[: not found`; `arr=(...)` is not valid POSIX assignment syntax, so it dies at parse time on the parenthesis. The fix is a decision, not a patch: either rewrite those lines in POSIX (`[ -n "$x" ]`, a loop or positional parameters instead of an array) or change the shebang to `#!/bin/bash` and accept bash as a real dependency.
code
bash · 16 lines#!/bin/sh
# These two lines are bash-only and break under dash:
# if [[ -n "$1" ]]; then echo yes; fi
# hosts=(web1 web2)
# POSIX equivalents that run under dash, ash, ksh and bash:
if [ -n "$1" ]; then
echo "got $1"
fi
for host in web1 web2; do
echo "$host"
done
set -- web1 web2
echo "first host: $1, count: $#"go deeper
Know that /bin/sh is not guaranteed to be bash, and that Debian and Ubuntu point it at dash. Be able to run ls -l /bin/sh to find out which shell you are actually getting.
Explain the mechanics: bash invoked as sh enters POSIX mode but keeps its keywords, dash implements little beyond POSIX, and name the POSIX replacements for [[ ]], arrays, source and function.
Show the judgment call — rewrite in POSIX versus declaring #!/bin/bash and installing bash — and say which you would pick for a container entrypoint versus an internal helper, plus how you would verify it before it ships.
Own the policy: decide the organisation's portability target, whether shell is even the right tool for scripts crossing that many platforms, and how CI enforces the choice so the failure cannot reach production images again.
## The shebang names a program, not a language The kernel reads the first line of an executable script, and if it begins with `#!` it runs the named interpreter with the script as an argument. `#!/bin/sh` therefore means "run this with whatever program lives at /bin/sh on this machine". That program is different on almost every family of system: - **Debian, Ubuntu and derivatives** — `/bin/sh` is `dash`, the Debian Almquist Shell. Debian switched in Debian 6 and Ubuntu in 6.10, mainly because dash starts faster and is smaller, which mattered for the hundreds of `#!/bin/sh` scripts run during boot. - **RHEL, CentOS, Fedora, SUSE** — `/bin/sh` is a symlink to `bash`. - **Alpine and other BusyBox systems** — `/bin/sh` is a symlink into the BusyBox multi-call binary, which provides an `ash` applet. - **Historic Unix** — Solaris 10's `/bin/sh` was the pre-POSIX Bourne shell, with the POSIX shell tucked away at `/usr/xpg4/bin/sh`; Solaris 11 made `/bin/sh` ksh93. You can always ask the machine: ```sh ls -l /bin/sh # lrwxrwxrwx ... /bin/sh -> dash readlink -f /bin/sh # /usr/bin/dash ``` ## Why bash-as-sh is not a POSIX shell When bash is invoked under the name `sh` it enters what its manual calls *sh mode*: it changes which startup files it reads (`$ENV` instead of `~/.bashrc`) and turns on POSIX-conforming behaviour for a list of specific details. What it does **not** do is amputate its own grammar. `[[ ... ]]` is still a shell keyword, `arr=(a b c)` is still a valid array assignment, `local`, `source`, `${var/old/new}` and `<(...)` all still work. `set -o posix` and `bash --posix` behave the same way — they are conformance switches, not a portability harness. That is the whole trap. Every RHEL developer who types `#!/bin/sh` out of habit is really writing bash and getting away with it, because the interpreter that runs it is bash. The bill arrives the first time the script lands on a Debian container, a Yocto image, or an Alpine CI runner. ## The two failing lines, specifically `[[ -n "$x" ]]` — in bash and ksh, `[[` is a *keyword* parsed specially by the shell, which is why it can do pattern matching and unquoted variable tests. dash has no such keyword, so `[[` is just a word in command position; dash searches `PATH` for an executable named `[[`, finds none, and prints `[[: not found` with exit status 127. Note this is a **runtime** failure, so it only happens if that branch executes. The portable equivalent is the `test` builtin, spelled `[`: `[ -n "$x" ]`, with the variable quoted because POSIX `test` really does need protection from word splitting. `hosts=(web1 web2)` — POSIX defines assignment as `name=word`. A parenthesis there is a syntax error, and it is a **parse** error, so the script dies before running a single line even if the array is never used. POSIX shells have exactly one array-shaped thing: the positional parameters (`set -- web1 web2`, then `"$@"`). For a fixed list a plain `for host in web1 web2; do ...; done` is usually all you needed. Other members of the same family: `source file` (POSIX spells it `. file`), `function f { }` (POSIX is `f() { }`), `echo -e` (dash's echo does not accept `-e` and prints it literally — use `printf`), `${var/old/new}`, `${var^^}`, `<(cmd)`, and `local` (not in POSIX at all, though dash and BusyBox ash both provide it). ## Choosing a fix There are only two honest options, and picking one is the point of the question: 1. **Mean `#!/bin/sh`.** Rewrite the bashisms in POSIX and verify with a linter and a run under the real target shell. Right for container entrypoints, init and packaging scripts, and anything that must run on a minimal image before bash exists. 2. **Say `#!/bin/bash`.** Declare the dependency and make sure bash is installed (on Alpine, `apk add bash`). Right for ordinary internal tooling, where bash's arrays and `[[ ]]` are worth a few megabytes. What is not an option is leaving `#!/bin/sh` on a script full of bashisms because it happens to work on your laptop. That combination is a latent failure that surfaces on someone else's machine, usually in CI, usually at 2am.
- If bash is running as sh and POSIX mode does not remove [[ ]] or arrays, what does it actually change?Mostly startup and conformance details: invoked as `sh` it reads `$ENV` rather than `~/.bashrc`, and POSIX mode adjusts behaviours such as how `kill`, `time` and word expansions are handled, and makes certain errors in special builtins fatal. None of it disables bash keywords, so it is useless as a portability check.
- Why did the arrays line kill the script even on a branch that never ran, while [[ ]] only failed when reached?They fail at different stages. `arr=(a b c)` is invalid POSIX grammar, so dash rejects it while parsing the enclosing compound command — nothing executes. `[[` parses fine as an ordinary command word, so it only fails at execution time, with a 127 `not found`, when that branch is actually taken.
- How would you find out quickly whether a large script is really POSIX before you promise that it is?Run `shellcheck` on it with `#!/bin/sh` as the shebang so it applies POSIX rules, and run it under the real target interpreter — `dash script.sh` or `busybox sh script.sh` — in a throwaway container. Reading it yourself is unreliable; almost nobody spots every bashism by eye.
#!/bin/sh is like specifying "a car" on a rental booking: on one lot you get the estate you always drive, on another a two-seater. Both are cars; only one fits the luggage you packed.
saying these in an interview costs you the question
- Says /bin/sh is just another name for bash everywhere
- Thinks the error means bash is not installed on Debian
- Claims bash --posix or set -o posix makes bash behave like dash
- Assumes POSIX sh has arrays because every modern shell does
- Concludes the script is portable because bash script.sh works