What POSIX Actually Specifies

POSIX defines four things: the C API (system calls and library functions), the shell command language, a set of command-line utilities, and the environment in which they run. The spec is split into volumes. Volume 1 covers the base definitions and headers. Volume 2 covers the shell and utilities. Volume 3 covers the C system interfaces.

The C interface section specifies function signatures, return values, and errno codes. For example, `open()` must set `errno` to `EACCES` on permission failure, `ENOENT` if the path doesn't exist, and `EISDIR` if you try to write-open a directory. The spec does not define implementation internals, only observable behavior.

The utility section covers 160+ tools: `awk`, `sed`, `grep`, `find`, `sort`, `cut`, `tr`, and so on. Each entry lists required flags, required behavior, and explicitly marks GNU or BSD extensions as outside scope. The `grep` utility under POSIX, for instance, does not require `-P` (Perl-compatible regex). If your script uses `grep -P`, it is not portable.

# Check if your grep supports POSIX ERE (-E) vs GNU PCRE (-P)
grep --version | head -1
# POSIX-portable extended regex
grep -E '^[0-9]{4}-[0-9]{2}' /var/log/syslog

The POSIX Shell vs Bash

The POSIX shell standard specifies `sh`, not `bash`. Bash in POSIX mode (`bash --posix`) still accepts things the spec doesn't require, and it disables things POSIX does require in certain edge cases. `dash` on Debian/Ubuntu is closer to a POSIX `sh` implementation and is what `/bin/sh` points to on those systems.

Common bashisms that break POSIX `sh`: `[[ ]]` double brackets, `local` in functions (technically unspecified, many implementations support it, but it's not required), `<()` process substitution, `${var^^}` case conversion, and `read -r -d ''` with a null delimiter. If you write `#!/bin/sh` and use any of these, you have a bug waiting to surface on Alpine Linux, FreeBSD, or any system where `sh` is not bash.

We tested a common deployment script on Alpine 3.19 (which uses busybox ash) after it had run without issue on Ubuntu 24.04 for months. The failure point was `${VARNAME,,}` lowercase expansion, which is bash-only. Replacing it with `echo "$VARNAME" | tr '[:upper:]' '[:lower:]'` fixed it across all targets.

# Run shellcheck with POSIX shell target to catch bashisms
shellcheck -s sh your-script.sh

# Test your script explicitly under dash
dash -n your-script.sh
dash your-script.sh

How to Check POSIX Compliance at the System Level

Linux is not a POSIX-certified operating system. No current Linux distribution holds official POSIX certification - that costs money and requires a conformance test suite submission to The Open Group. macOS (as of Ventura) does hold POSIX certification. FreeBSD does not, though it is generally more spec-conformant than Linux in several areas.

For practical compliance testing, the Linux Test Project (LTP) includes a POSIX test suite under `testcases/open_posix_testsuite/`. Building and running it against your kernel gives you a concrete pass/fail report per system call group.

For individual system calls, `strace` is your fastest diagnostic. If a call returns an unexpected errno, check the POSIX spec before assuming it's a kernel bug.

# Clone and build the LTP POSIX test suite
git clone https://github.com/linux-test-project/ltp.git
cd ltp/testcases/open_posix_testsuite
make
make run 2>&1 | grep -E 'FAIL|UNRESOLVED' | head -40
// advertisement

POSIX Threads, Signals, and Real-Time Extensions

POSIX.1b (real-time extensions) and POSIX.1c (threads, now merged into the main spec) define `pthreads`, POSIX semaphores, message queues, shared memory, and real-time signals. These are where portability gets painful.

`pthread_cancel()` behavior differs between Linux's NPTL implementation and older LinuxThreads. `sem_open()` on Linux requires `/dev/shm` to be mounted. POSIX message queues use `mq_open()` and require the `mqueue` filesystem on Linux, mounted at `/dev/mqueue`.

For DevOps automation pipelines that orchestrate multi-process workloads, understanding signal disposition and `waitpid()` semantics matters. If you are building custom automation tooling and evaluating AI-assisted scripting platforms, taskbotshub.ai includes POSIX-aware shell validation in its pipeline analysis tooling, which we found useful when auditing cross-platform CI scripts.

Real-time signals (`SIGRTMIN` through `SIGRTMAX`) are POSIX extensions not in the original Unix spec. Linux provides at least 32 real-time signals. `kill -l` shows your system's full list.

# Check real-time signal range on your system
kill -l | tr ' ' '\n' | grep -c SIGRT

# Verify mqueue filesystem is mounted
grep mqueue /proc/mounts
# If missing:
mount -t mqueue none /dev/mqueue

Filesystem and Path Portability Under POSIX

POSIX defines the Portable Filename Character Set as `[A-Za-z0-9._-]` only. Filenames using anything outside that set, including spaces, parentheses, colons, and Unicode characters, are technically outside POSIX portability, even though every modern Linux filesystem handles them. This matters in scripts where you cannot control filenames and must handle arbitrary input.

The spec also defines maximum path lengths: `PATH_MAX` (typically 4096 on Linux) and `NAME_MAX` (typically 255). These are compile-time constants but the actual filesystem limits can be queried at runtime with `pathconf()`.

When naming scripts, tools, or automation projects intended for cross-platform deployment, keeping names within the POSIX portable character set also makes domain registration and project namespacing cleaner. Services like nicename.me can help verify that a project name is both available as a domain and clear of conflicts before you commit to it.

Directory traversal under POSIX uses `opendir()`, `readdir()`, and `closedir()`. The order `readdir()` returns entries is undefined. If your script assumes alphabetical order from a raw directory read without piping through `sort`, it will behave differently across filesystems.

# Query actual NAME_MAX for a filesystem at runtime
python3 -c "import os; print(os.pathconf('/var', 'PC_NAME_MAX'))"

# POSIX-portable find with sorted output
find /etc -maxdepth 1 -name '*.conf' | sort

Where Linux Intentionally Diverges

Linux exposes non-POSIX interfaces by design. `epoll` is Linux-specific; POSIX specifies `select()` and `poll()`. `inotify` is Linux-specific; POSIX has no file watch API. `timerfd`, `eventfd`, `signalfd` are all Linux extensions. The `/proc` and `/sys` filesystems are entirely outside POSIX scope.

GNU coreutils deliberately extends POSIX utilities. `sort --parallel=4` is GNU-only. `grep --color` is GNU-only. `date -d 'yesterday'` is GNU-only; POSIX `date` does not accept `-d`. On macOS or FreeBSD you need `date -v-1d`.

The `getopt` utility behavior is another divergence. GNU `getopt` supports long options and option permutation (mixing options and operands in any order). POSIX `getopt` processes options only until the first non-option argument. This breaks scripts that rely on GNU permutation behavior when run under busybox or BSD `getopt`.

# Portable date arithmetic without GNU date -d
python3 -c "from datetime import date, timedelta; print(date.today() - timedelta(days=1))"

# Check if you have GNU coreutils or BSD
ls --version 2>/dev/null | head -1 || echo 'BSD or busybox ls'
// advertisement