Portability and POSIX shell

What dash and busybox support, POSIX versus bashisms, portable arrays and local, testing against sh, and handling Windows paths and line endings.

Which shell is really there

A script that runs on your laptop may run under dash in a Debian container, busybox ash in an Alpine image, or bash 3.2 on a colleague's Mac. Portability is about knowing which of those you must support before you write the first line.

ls -l /bin/sh                     # dash on Debian/Ubuntu, ash on Alpine
busybox --list | head             # what a minimal container actually has
bash --version | head -n 1        # 3.2 on macOS: no associative arrays

dash -n script.sh                 # syntax check under a strict POSIX shell
checkbashisms script.sh           # report bash-only constructs
shellcheck -s sh script.sh        # lint against the POSIX dialect
ConstructbashPOSIX shPortable replacement
[[ a == b ]]YesNo[ "$a" = "$b" ]
(( i++ ))YesNoi=$((i + 1))
ArraysYesNoOne variable per item, or set --
localYesNot requiredSupported by dash/busybox in practice
echo -eYesNoprintf '%b\n'
function f {}YesNof() { ...; }
${v//a/b}YesNoPipe through sed
sourceYesNo. ./file
&> fileYesNo> file 2>&1
read -dYesNoread -r line per line
  • The shebang decides which dialect you are promising: #!/bin/sh means POSIX only, #!/usr/bin/env bash means bashisms are allowed and expected.
  • printf is the portable way to format output; echo behaviour with backslashes and leading dashes differs between implementations.
  • command -v name is the portable test for "is this tool installed".

Writing portable code

#!/bin/sh
set -eu

# arrays without arrays: positional parameters as a list
set -- alpha beta gamma
for item in "$@"; do
    printf '%s\n' "$item"
done

# a portable temporary file
tmp=$(mktemp "${TMPDIR:-/tmp}/deploy.XXXXXX") || exit 1
trap 'rm -f "$tmp"' EXIT INT TERM

# a portable check for a command
if command -v jq >/dev/null 2>&1; then
    jq -r .version "$file"
else
    sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$file"
fi

# a portable substring via cut when parameter expansion is not enough
printf '%s\n' "$name" | cut -c1-8
  • set -- ... replaces a small array, and $# still gives the count; shift still works.
  • ${TMPDIR:-/tmp} respects the environment while staying inside POSIX expansion rules.
  • If the script genuinely needs arrays and associative lookups, do not contort it into POSIX — declare the requirement in the shebang and check for bash at startup.

Line endings and Windows paths

file script.sh                    # CRLF line endings ...
grep -c $'\r' script.sh          # ... show up as a count

tr -d '\r' < script.sh > script.fixed && mv script.fixed script.sh

# stop Git from converting line endings in the first place
git config --global core.autocrlf input
# .gitattributes
# * text=auto
# *.sh text eol=lf

cygpath -u 'C:\Users\ada\app'   # C:\Users\ada\app -> /c/Users/ada/app
cygpath -w /c/Users/ada/app        # back to a Windows path for native tools
SymptomCauseFix
bad interpreter: ^MCRLF in the shebang lineConvert the file to LF
command not found for a path that existsMSYS path manglingQuote it, or convert with cygpath
A file written with \n looks wrong in NotepadLF-only textUse unix2dos when handing files to Windows tools
Tests pass locally, fail in DockerDifferent /bin/shRun dash -n and shellcheck -s sh in CI
⚠️
A single carriage return is enough to break a shebang, and the resulting error mentions a filename that looks correct. If a script works when you run bash script.sh but not ./script.sh, check the line endings before you debug anything else.

FAQ

Should I write POSIX sh everywhere?
Only when the target demands it — containers with busybox, embedded systems, or a policy of #!/bin/sh. Otherwise bash is clearer and safer, as long as you declare and verify it.
How do I know my script is portable?
Run dash -n for syntax, checkbashisms for bash-only constructs, and shellcheck -s sh to lint. Then run the tests in a container whose /bin/sh is not bash.

Shells, startup files and running scripts Testing and linting shell scripts

Last refreshed 2026-09-18.