Linux Sysadmin · Lesson 14 · 25 min

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 sh script 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:

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:

Same on both
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?

The family difference, one last time

Rocky / RHEL
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.

Ubuntu / Debian
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 onlyPortable POSIX version
[[ -f $f && -r $f ]][ -f "$f" ] && [ -r "$f" ]
[ "$a" == "$b" ][ "$a" = "$b" ] (one =)
source file. file
echo -e "a\tb" / echo -nprintf '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 &> filecmd > file 2>&1
function name { … }name() { … }
read -p "Name: " nprintf 'Name: '; read -r n
which cmdcommand -v cmd
$RANDOMawk '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.

Bash isn't bad

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/' filemacOS 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=1du -h -d 1 works on GNU and BSD
date -d yesterdayno 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.

Rocky / RHEL
sudo dnf install epel-release
sudo dnf install ShellCheck
shellcheck report.sh

It's in EPEL, and the package name has capitals.

Ubuntu / Debian
sudo apt install shellcheck
shellcheck report.sh
checkbashisms report.sh   # bonus tool, from the devscripts package
In 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?

2. What's the portable way to print a line containing a tab?

3. Your script really needs arrays. What's the honest fix?

4. Which of these is already POSIX and needs no change?

Finished the missions and the quiz? Mark it done to track your progress.