Sh#
sh is the Bourne shell, Stephen Bourne’s 1977 shell that
defined the syntax most modern shells extend. The name now refers to
whichever shell /bin/sh points at on a system, which is rarely the
original Bourne shell anymore.
In 2026, /bin/sh is typically:
Debian / Ubuntu, Dash.
macOS, Bash 3.2 in POSIX mode.
Alpine, BusyBox Ash.
NixOS, Bash in POSIX mode.
FreeBSD, a Bourne-like Almquist descendant.
Older systems, the actual Bourne shell or ksh.
Whatever it points at, sh is expected to be POSIX-compliant.
What POSIX Is#
POSIX, Portable Operating System Interface, is the IEEE 1003 family of standards (republished as ISO/IEC 9945) that defines a portable Unix-like API and shell. The pieces an operator encounters:
Part |
Defines |
|---|---|
POSIX.1 |
System C API: |
POSIX.1c (Threads) |
|
POSIX.1b (Real-time) |
Timers, async I/O, message queues, shared memory |
POSIX.2 |
Shell language + ~160 utilities ( |
POSIX.1-2017 / 2024 |
Current published revisions (everything merged into one doc) |
Single UNIX Specification |
SUS v4 / v5; strict superset of POSIX, used for certification |
Compliance, in 2026:
Linux, “mostly POSIX” but uncertified. Diverges in places (epoll,
/proc, signalfd, namespaces, eBPF) where the spec doesn’t reach.macOS, formally certified against SUSv4 (Open Group registered).
AIX, HP-UX, Solaris, certified UNIX, also POSIX.
WSL / Cygwin, POSIX-ish layer on top of Windows; not certified.
BSD family, close, with documented exceptions.
The standard is published by the Austin Group (joint IEEE / ISO / Open Group); current text is browseable at https://pubs.opengroup.org/onlinepubs/9699919799/.
POSIX as a Target#
Writing portable scripts means restricting yourself to what POSIX
guarantees, not what your shell happens to allow. The list below is
the most common “looks like Bash, isn’t POSIX” subset that breaks
under Dash or Ash even though it works fine under /bin/bash:
Bashism |
POSIX equivalent |
|---|---|
|
Positional parameters: |
|
|
|
|
|
|
|
|
|
Use a subshell |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The POSIX subset is small but covers more than most scripts need.
#!/bin/sh
set -eu
greet() {
name=$1
printf 'hello, %s\n' "$name"
}
for who in "$@"; do
greet "$who"
done
Why It Matters#
/bin/sh is what runs in places you do not get to choose, the
early boot, distribution package scripts, container entrypoints,
embedded systems, and public install one-liners. Hitting sh
correctly is what keeps a script working across every Unix host the
operator might encounter.
Init scripts, distribution package scripts, container entrypoints often run under
sh. Don’t assume Bash features work.Embedded systems ship the smallest possible shell.
Bootstrapping, in the very early boot, only
shis available.Public scripts that strangers will run on unknown systems should target
sh.
checkbashisms#
Debian ships a tool that flags Bash-only features in sh-claimed
scripts. It catches the easy mistakes, [[ tests, brace
expansion, echo -e, that pass on a Bash-as-sh system and break
on a Dash-as-sh system. Run it as part of any release pipeline for
a portable shell script:
$ sudo apt install devscripts
$ checkbashisms install.sh
Strengths#
The portability story is the entire pitch. Everything else is a side effect of being the lowest common denominator, short, present, and predictable on every Unix host an operator is likely to touch.
Always available.
Portable, one script runs on every Unix.
Tiny, the simplest scripting environment.
Weaknesses#
What you give up to keep the portability guarantee. A POSIX-only
script that needs arrays, regex matching, or mapfile-style I/O
ends up reinventing them painfully, at which point switching to
#!/bin/bash is usually the right call.
Bare, no arrays, no associative arrays, no advanced parameter expansion.
Quoting subtleties, IFS bugs, word-splitting surprises.
Different ``sh`` engines disagree on edge cases.
When to Target sh#
Anywhere a script has to run before you control the environment, or on a machine where you can’t depend on Bash being installed. For everything else, application scripts, internal tooling, anything on a host you administer, target Bash explicitly.
Distribution-shipped install scripts, init scripts.
Container entrypoints (especially Alpine).
Bootstrappers that have to run before any other tool is installed.
Public install one-liners (
curl -fsSL ... | sh).
For application scripting, target Bash explicitly with #!/usr/bin/env bash.