sh vs bash vs dash

On Debian, /bin/sh is dash and not bash

Updated 2026-08-22

A script runs perfectly when you run it. Then cron runs it, or a package's maintainer script calls it, or you type sh script.sh instead of ./script.sh, and it fails on a line that was fine a minute ago.

Almost always, this is the answer:

readlink /bin/sh
dash

On Debian, /bin/sh is dash. It is not bash, and it has not been bash for over a decade.

sh, dash and bash

sh is not a program. It is a specification, the POSIX shell, and /bin/sh is a symlink pointing at whichever installed shell the system has elected to satisfy it. Writing #!/bin/sh is a promise that your script needs nothing beyond that specification.

dash is the Debian Almquist Shell: small, fast, and almost exactly POSIX and no more. Debian made it /bin/sh in Debian 6 for boot speed. Every init script at the time was a shell script, and dash starts and runs measurably faster than bash.

bash is the GNU shell, and on Debian it is the shell you get when you log in. It implements POSIX and then adds a great deal on top. People call that extra "bashisms".

All of them are installed, and the system knows about each:

cat /etc/shells
# /etc/shells: valid login shells
/bin/sh
/usr/bin/sh
/bin/bash
/usr/bin/bash
/bin/rbash
/usr/bin/rbash
/usr/bin/dash

Four differences that break scripts

In each, the first line is bash and the second is /bin/sh.

[[ ]] does not exist. Every tutorial uses [[, because it is better than [:

bash -c '[[ -n $HOME ]] && echo ok'
sh -c '[[ -n $HOME ]] && echo ok'
ok
sh: 1: [[: not found

See conditionals and test for the portable [ forms.

Arrays do not exist, and the failure is a syntax error rather than a missing command, so it takes the whole script down at parse time:

bash -c 'a=(one two); echo ${a[1]}'
sh -c 'a=(one two); echo ${a[1]}'
two
sh: 1: Syntax error: "(" unexpected

echo -e is worse than unsupported. dash's echo interprets backslash escapes already and has no -e flag, so it prints the flag as text and expands the escape anyway:

bash -c 'echo -e "a\tb"'
sh -c 'echo -e "a\tb"'
a	b
-e a	b

Nothing errors. The script keeps going with -e glued to the front of a string, which is how this ends up in a log file three weeks later. printf behaves the same in both and is the fix.

source is spelled .:

sh -c 'source /etc/os-release'
sh: 1: source: not found
sh -c '. /etc/os-release; echo "$ID"'
debian

. works in bash too, so there is no reason to write source in a script at all.

Which shebang to write

Use #!/bin/bash whenever you want a bashism. Bash is installed on every Debian system, and a script that declares what it needs is correct. [[ ]], arrays and local are worth having. While the portability you gain from POSIX can be useful sometimes, the risk of a script misbehaving under dash is likely to be a much bigger problem, so defaulting to bash is usually the right call.

Use #!/bin/sh only when the script stays inside POSIX: something that has to run on a minimal system, in a container without bash, or as a package maintainer script, where Debian Policy requires it. Test it by running it under dash, dash script.sh, rather than by reading it. checkbashisms, from the devscripts package, catches most of the rest.

The one thing not to do is write #!/bin/sh and then use bashisms anyway. It works on systems where /bin/sh happens to be bash, which is why the bug reaches you from someone else's machine rather than your own.

Leave /bin/sh pointing at dash

dpkg-reconfigure dash changes which shell /bin/sh points at. Know that it exists, and leave it alone: the boot path expects dash, and pointing /bin/sh at bash to fix one script slows every script on the system to fix a bug you could have fixed in a line.