Andrew Mercer
on this page

The original Bourne shell (sh) was written by Stephen Bourne at Bell Labs and shipped with Version 7 Unix in 1979. Almost nobody runs that shell today. What survives is its behaviour, standardised by POSIX as the Shell Command Language.

/bin/sh is a standard, not a program

sh is the POSIX shell specification, and /bin/sh is whatever shell the distribution chose to provide POSIX sh behaviour:

System /bin/sh is
Debian, Ubuntu dash
Fedora, RHEL, Arch, openSUSE bash, running in POSIX mode
Alpine, BusyBox-based images BusyBox ash
macOS bash 3.2 in POSIX mode by default
FreeBSD the Almquist-derived FreeBSD sh

A script that starts with #!/bin/sh but uses bash features ([[, arrays, source, function, {1..5}) works on Fedora, where sh is bash, and fails on Debian or Alpine. Either write portable POSIX sh or use #!/usr/bin/env bash and admit the dependency. checkbashisms (Debian's devscripts package) and shellcheck --shell=sh catch bash-only syntax in sh scripts.

Portable equivalents of common bashisms

bash POSIX sh
[[ $a == "$b" ]] [ "$a" = "$b" ] (always quote, single =)
[[ $f == *.txt ]] case $f in *.txt) ... ;; esac
source file . ./file
function f { ...; } f() { ...; }
echo -e "a\tb" printf 'a\tb\n'
{1..5} seq 1 5, or a while loop with a counter
arr=(a b c); "${arr[@]}" set -- a b c; "$@" (the positional parameters are the only array)
${var^^} printf '%s' "$var" \| tr '[:lower:]' '[:upper:]'
${var//a/b} printf '%s' "$var" \| sed 's/a/b/g'
${var:0:3} printf '%s' "$var" \| cut -c1-3
<(cmd) a temporary file from mktemp, or a pipe
$RANDOM awk 'BEGIN { srand(); print int(rand() * 32768) }'
read -p "Name: " n printf 'Name: '; read -r n
&>file >file 2>&1

Still portable and worth using: ${var#prefix}, ${var%suffix}, ${var:-default}, ${#var}, $(( )) arithmetic, $(cmd), case, getopts, and functions.

Checking scripts

shellcheck --shell=sh script.sh          # warns on bash-only syntax
checkbashisms script.sh                  # Debian devscripts
dash -n script.sh                        # parse with a strict POSIX shell without running it

Testing a script under dash is the fastest practical check for portability, because dash implements very little beyond POSIX. See dash and BusyBox ash for the two sh implementations you're most likely to hit, and bash for what bash adds.

Further reading