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| Construct | bash | POSIX sh | Portable replacement |
|---|---|---|---|
[[ a == b ]] | Yes | No | [ "$a" = "$b" ] |
(( i++ )) | Yes | No | i=$((i + 1)) |
| Arrays | Yes | No | One variable per item, or set -- |
local | Yes | Not required | Supported by dash/busybox in practice |
echo -e | Yes | No | printf '%b\n' |
function f {} | Yes | No | f() { ...; } |
${v//a/b} | Yes | No | Pipe through sed |
source | Yes | No | . ./file |
&> file | Yes | No | > file 2>&1 |
read -d | Yes | No | read -r line per line |
- The shebang decides which dialect you are promising:
#!/bin/shmeans POSIX only,#!/usr/bin/env bashmeans bashisms are allowed and expected. printfis the portable way to format output;echobehaviour with backslashes and leading dashes differs between implementations.command -v nameis 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-8set -- ...replaces a small array, and$#still gives the count;shiftstill 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| Symptom | Cause | Fix |
|---|---|---|
bad interpreter: ^M | CRLF in the shebang line | Convert the file to LF |
command not found for a path that exists | MSYS path mangling | Quote it, or convert with cygpath |
A file written with \n looks wrong in Notepad | LF-only text | Use unix2dos when handing files to Windows tools |
| Tests pass locally, fail in Docker | Different /bin/sh | Run 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.Related
Shells, startup files and running scripts Testing and linting shell scripts
Last refreshed 2026-09-18.