POSIX: write once, run anywhere
You've seen scripts that work on Rocky and break on Ubuntu. The fix has a name: POSIX, the shared rulebook that Unix-like systems agree on. Write to the rulebook, and your script runs on Rocky, Ubuntu, a Mac, a tiny Alpine container, a FreeBSD server or a router, unchanged.
You will learn
- What POSIX is, who it's for, and why Linux is “mostly” POSIX
- The difference between a POSIX
shscript and a bash script, with replacements for common bash-isms - GNU extras vs portable options, and why Rocky and Ubuntu agree on tools but not on
/bin/sh - Checking scripts with
shellcheck, and testing with dash
What is POSIX?
POSIX (Portable Operating System Interface, pronounced “POZ-icks”) is a standard published by the IEEE and The Open Group, first in 1988. It describes what a Unix-like system must provide:
- The shell language:
shwithif,for,while,case,$( ),$(( )), functions and redirections. - About 160 standard commands and their options:
ls,cp,grep,sed,awk,find,printf… - The rest of the system: file permissions, signals (
kill -TERM, lesson 6), exit codes, regular expressions and the programming interface that C programs use.
macOS is officially certified as UNIX. Linux distributions follow POSIX very closely but mostly don't pay for certification, so people say Linux is “mostly POSIX.” Ask your system which version it follows:
getconf _POSIX_VERSION # 200809 = POSIX.1-2008 getconf ARG_MAX # the longest command line allowed (bytes) getconf PATH # where the standard tools are guaranteed to be
Why should you care?
- Containers: the popular tiny Alpine image has no bash at all, only a small POSIX
sh(BusyBox). - Macs ship an ancient bash 3.2, and zsh as the default shell.
- Ubuntu's
/bin/shis dash, and so is every cron job and#!/bin/shscript there (lesson 12). - Your future job may have servers you've never heard of. A POSIX script runs there on day one.
The family difference, one last time
ls -l /bin/sh # sh -> bash
When bash runs as sh, it switches to “POSIX mode” but still accepts almost every bash extra. A #!/bin/sh script full of bash-isms works here, so nobody notices until it runs somewhere else.
ls -l /bin/sh # sh -> dash
dash is almost pure POSIX. Bash-isms fail with Syntax error, not found or Bad substitution. That's annoying, but it makes Ubuntu a great place to test that a script is portable.
Both families use the same GNU tools (coreutils, grep, sed…), so the commands agree. The difference is the shell.
Bash-isms and their POSIX replacements
| Bash only | Portable POSIX version |
|---|---|
[[ -f $f && -r $f ]] | [ -f "$f" ] && [ -r "$f" ] |
[ "$a" == "$b" ] | [ "$a" = "$b" ] (one =) |
source file | . file |
echo -e "a\tb" / echo -n | printf 'a\tb\n' (printf behaves the same everywhere) |
for i in {1..5} | for i in 1 2 3 4 5 or i=1; while [ $i -le 5 ]; do …; i=$((i+1)); done |
diff <(cmd1) <(cmd2) | cmd1 > /tmp/a; cmd2 > /tmp/b; diff /tmp/a /tmp/b |
${var//old/new}, ${var^^} | echo "$var" | sed 's/old/new/g', … | tr a-z A-Z |
arrays list=(a b c) | set -- a b c then "$@", or a string with spaces |
cmd &> file | cmd > file 2>&1 |
function name { … } | name() { … } |
read -p "Name: " n | printf 'Name: '; read -r n |
which cmd | command -v cmd |
$RANDOM | awk 'BEGIN{srand(); print int(rand()*100)}' |
Already POSIX, so no changes needed: $( ), $(( )), ${x:-default}, ${x%.txt}, ${x##*/}, case, while, functions, $?, here-documents and 2>&1.
POSIX isn't about avoiding bash. It's about being honest about what you need. If you want arrays and [[ ]], write #!/bin/bash and use them freely. Trouble only starts when a script says #!/bin/sh but needs bash.
GNU extras vs portable options
The GNU tools on Linux add handy options that other systems (macOS, BSD, BusyBox) don't have:
| GNU (Rocky and Ubuntu) | Portable |
|---|---|
sed -i 's/a/b/' file | macOS needs sed -i '' …. Portable: sed 's/a/b/' file > tmp && mv tmp file |
grep -P '\d+' (Perl regex) | grep -E '[0-9]+' |
ls --color, du -h --max-depth=1 | du -h -d 1 works on GNU and BSD |
date -d yesterday | no portable version: do date math in awk or another language |
Setting POSIXLY_CORRECT=1 makes many GNU tools behave more strictly, which is handy for testing.
Check it: shellcheck
ShellCheck reads a script and points at problems, from missing quotes to bash-isms in a #!/bin/sh script. Every warning has a code (like SC3010) you can look up at shellcheck.net.
sudo dnf install epel-release sudo dnf install ShellCheck shellcheck report.sh
It's in EPEL, and the package name has capitals.
sudo apt install shellcheck
shellcheck report.sh
checkbashisms report.sh # bonus tool, from the devscripts packageIn report.sh line 5: if [[ -d /var/log ]]; then ^-- SC3010 (warning): In POSIX sh, [[ ]] is undefined.
Then test for real on a strict shell: dash report.sh on Ubuntu, or sh report.sh in an Alpine container. Many code editors run ShellCheck automatically as you type.
Try it: make a script portable 🌍
A colleague wrote report.sh on a Rocky server and swears it works. The Ubuntu servers disagree. Find out why, then fix it so it runs everywhere. The four sed commands in the missions do the fixes, or you can edit the file yourself.
Quick check
1. A #!/bin/sh script with [[ ]] works on Rocky but fails on Ubuntu. Why?
✓ Rocky hid the bug. Ubuntu revealed it.
2. What's the portable way to print a line containing a tab?
✓ echo flags differ between shells, but printf is the same everywhere.
3. Your script really needs arrays. What's the honest fix?
✓ Portability means your script says what it needs. Bash is installed on Rocky and Ubuntu, just not everywhere.
4. Which of these is already POSIX and needs no change?
✓ $(( )) math is standard. Brace ranges and source are bash extras.