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: fork, exec, sockets, file I/O, signals

POSIX.1c (Threads)

pthread_*, mutexes, condition variables

POSIX.1b (Real-time)

Timers, async I/O, message queues, shared memory

POSIX.2

Shell language + ~160 utilities (awk, sed, find, grep, tar, xargs, …)

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

arr=(a b c)

Positional parameters: set -- a b c; iterate "$@"

[[ "$a" == foo* ]]

[ "$a" = "foo*" ] (no glob match, use case)

[[ "$a" =~ regex ]]

echo "$a" | grep -Eq 'regex'

${var,,} / ${var^^}

echo "$var" | tr '[:upper:]' '[:lower:]'

${var/old/new}

echo "$var" | sed 's/old/new/'

local x

Use a subshell () or accept that POSIX has no local

function name() { ... }

name() { ... }

<(cmd) (process sub)

cmd | other or named pipe (mkfifo)

echo -e '\t'

printf '\t'

read -p 'prompt'

printf 'prompt' && read

$RANDOM

awk 'BEGIN { srand(); print int(rand()*32768) }'

source file (alias)

. file

{1..10}

seq 1 10

&>file

>file 2>&1

\|&

\| then 2>&1

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 sh is 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.