Skip to main content

Linux concept: POSIX

06 October 2026

A script works on your laptop. You copy it to the server, it runs from cron at three in the morning, and it fails on a line that has not changed. Nobody edited it, no package was upgraded, and the same file still works when you run it by hand. The usual cause is not a bug in your logic. It is that the two machines disagree about what a Unix system is, and the document that decides who is right is called POSIX.

This article explains what POSIX actually is, where the name comes from, how forty years of Unix arguments produced it, and how to use it: asking your own system which version it implements, reading the standard for free, writing shell scripts that survive a move to another machine, and the feature test macros that decide which functions your C compiler will admit exist. On the way it separates the C library from the kernel, and source portability from binary compatibility. It also covers what POSIX is not, which is where most of the confusion lives.

If you only write shell scripts, you can skip section 6: it is for readers who write or compile C, and the rest of the article does not depend on it.

From "why did my script break on the server" to "which of these two systems is wrong".

The goal: after reading this you can look at any portability problem and say whether the standard has an opinion about it.

1. The Basics

POSIX is not a program, a library, or a piece of Linux. It is a written document: a standard that says what a Unix-like operating system must offer to the programs running on it. It describes system calls, C library functions, a shell language, and a set of command-line utilities, and it describes them precisely enough that software written against the description works on any system that follows it.

You cannot install POSIX. You can only implement it, and Linux, the BSDs, macOS, AIX, Solaris and others each implement it to a different degree. The standard is the shared vocabulary that makes the phrase "a Unix system" mean something concrete instead of something historical.

The right mental model: POSIX is the contract between your code and the system underneath it. Your program keeps its side by only using what the contract lists. The system keeps its side by providing all of it, exactly as described. Portability is what happens when both sides hold.

1.1 What Is Inside the Standard

The current standard is published in four volumes. Knowing which volume answers which kind of question saves a lot of searching:

VolumeShort nameWhat it defines
Base Definitions XBD Terms, concepts, header files, the regular expression syntax, the locale model
System Interfaces XSH The C functions and system calls: open(), fork(), pthread_create() and around a thousand more
Shell and Utilities XCU The shell language and the command-line utilities: sh, awk, sed, grep, find and the rest
Rationale XRAT Why the committee decided what it decided; informative, not binding

The split matters in practice. When you ask "is sed -i portable?" you are asking a question about XCU. When you ask "why does the compiler say strdup does not exist?" you are asking about XBD and XSH. The two halves of the standard are written by the same group but they solve different problems, and this article follows the same division: section 5 is the shell half, section 6 is the C half.

1.2 Ask Your Own System

Your machine will tell you which version of the standard it claims to implement. The getconf utility (short for get configuration) reads the values the system reports at run time:

$ getconf _POSIX_VERSION
200809

That number is a date, not a version count: year 2008, month 09. This system implements POSIX.1-2008, the edition published in September 2008. The same encoding appears everywhere in POSIX, so 199506 means the June 1995 edition and 200112 means December 2001.

A second number tells you how much of the optional X/Open material is present:

$ getconf _XOPEN_VERSION
700

700 means Issue 7 of the Single UNIX Specification, which is the same body of text as POSIX.1-2008 with a few extra volumes. Section 3 explains why one standard has two names and two numbering schemes.

1.3 Standard, Implementation, and Certification

Three ideas get mixed together constantly, and separating them removes most arguments about who is "POSIX compliant":

IdeaWhat it meansExample
The standard The document itself, published by IEEE and The Open Group IEEE Std 1003.1-2024
An implementation Software that provides what the document describes Linux with glibc, coreutils and a shell
Certification Paying to pass a test suite and licensing the UNIX trademark macOS, AIX, HP-UX, Solaris

Linux implements a very large part of POSIX and is certified by nobody, because certification is a commercial licensing decision rather than a technical one. That is why Linux is called "Unix-like" and not "UNIX": the word UNIX is a trademark, and the trademark is what certification buys. Section 7.3 has the strange exception to this rule.

In everyday speech, "POSIX compliant" almost always means "close enough to the standard that normal programs work", which is a useful thing to say and not a claim anybody has verified. When precision matters, name the version: "POSIX.1-2008" says something, "POSIX compliant" does not.

1.4 The Words the Standard Uses

A standard cannot only say "this works". It has to say how much you are allowed to rely on, and POSIX does that with four terms that look like synonyms and are not. They are the difference between a portable script and one that merely happens to run:

TermWhat it means for youExample in this article
Defined Every conforming system does the same thing. Rely on it. [ "$a" = "$b" ] compares two strings
Implementation-defined The system chooses and must document its choice. Rely on it only after reading that documentation, and only on that system. echo with a leading -n or a backslash (section 5.3)
Unspecified The system chooses and need not tell anyone. Two systems may differ and both are correct. local in a shell script (section 5.2)
Undefined The standard imposes no requirement at all. Anything may happen, including working perfectly until it does not. using a C pointer after the memory is freed

The middle two are where portability bugs live. Nothing warns you, both machines are behaving correctly, and code that leans on an unspecified detail looks exactly like code that leans on a guarantee. When the standard uses one of these words about something your script depends on, that dependency is yours to remove, because no system is obliged to keep it working.

Back to top

2. Where the Name Comes From

POSIX stands for Portable Operating System Interface. Read the words in order and they describe the whole project: an interface to an operating system, specified so that software is portable between systems that provide it.

The last letter does not come from the words. There is no X in "Portable Operating System Interface", and the standard ends in one because of a naming habit: Unix systems of the era were called AIX, IRIX, HP-UX, Ultrix, Xenix, and an X at the end read as "this belongs to the Unix family". The name is deliberately pronounceable, and it stuck immediately.

The person who suggested it is recorded in the Linux manual pages. Run man 7 standards and read the entry for the first edition:

$ man 7 standards
POSIX.1-1988
       This was the first POSIX standard, ratified by IEEE as IEEE Std
       1003.1-1988, and subsequently adopted (with minor revisions) as
       an ISO standard in 1990.  The term "POSIX" was coined by Richard
       Stallman.

The IEEE working group needed a name for a document that was, until then, known only by its committee number. The number never went away, which is why you meet the same standard under several names at once:

NameWho uses itMeaning
IEEE 1003.1 IEEE The committee and document number; .1 is the system interface part
POSIX.1-2008 Everyone, informally The 1003.1 document, edition of 2008
ISO/IEC 9945 ISO The same text adopted as an international standard
Base Specifications Issue 7 The Open Group The same text again, in their numbering

A letter after the number means an amendment rather than a new edition. POSIX.1b added real-time facilities in 1993 and POSIX.1c added threads in 1995, and both were later folded into the main document. When you see 1003.1c in a manual page, it is telling you that a function arrived with the threads amendment.

The picture the name describes is a single line drawn through a running system:

    your program        your shell script
    ------------------------------------------   <-- POSIX describes this line
    system calls    C library    sh    utilities
    ------------------------------------------
    the kernel and the hardware underneath

Everything above the line is yours. Everything below it is the implementation's business, and the standard says nothing about how it is done. POSIX never mentions inodes on disk, scheduling algorithms, or how a filesystem is laid out. It only fixes what the layer looks like from above, which is exactly what a program needs to know and nothing more.

Back to top

3. A Short History

POSIX exists because Unix split. AT&T's System V and Berkeley's BSD grew apart through the early 1980s, every vendor shipped its own variant, and software that ran on one machine needed changes to run on the next. The problem was commercial before it was technical: buyers wanted to move software between suppliers, and suppliers wanted to sell to buyers who insisted on that.

The first attempt came from users rather than vendors. The /usr/group association published a standard in 1984 describing what its members expected a Unix system to provide, and the IEEE took that work as the starting point for a formal standard. In 1988 the IEEE ratified IEEE Std 1003.1-1988, and Unix had a written definition for the first time.

That first edition covered the C interface only. Commands and utilities came four years later in POSIX.2, which is where the shell language, awk, sed, and the rest of the standard toolbox were pinned down. Amendments followed for real-time work and for threads.

Meanwhile a second standards effort ran in parallel. The X/Open consortium published its own Portability Guides, and its 1994 revision was nicknamed Spec 1170 after the number of interfaces it defined. X/Open owned the UNIX trademark, so its documents decided who could use the name, and systems conforming to its Single UNIX Specification could be branded UNIX 95, then UNIX 98.

Two standards for one thing is one too many. In 1998 the IEEE, The Open Group and ISO formed the Austin Group, a joint committee named after the city where it first met, with a single principle: write the text once and let all three organisations publish it. The result appeared in 2001 as POSIX.1-2001, the Single UNIX Specification version 3, and ISO/IEC 9945 all at the same time. That merger is why one document has four names.

YearMilestone
1969-1979 Unix at Bell Labs. Version 7 in 1979 is the last release before BSD and System V diverge.
1984 The /usr/group standard: users write down what they expect a Unix system to have.
1988 IEEE Std 1003.1-1988, the first POSIX. System interfaces only.
1990 Adopted by ISO as ISO/IEC 9945-1:1990.
1992 POSIX.2: the shell language and the command-line utilities.
1993, 1995 Amendments 1003.1b (real-time) and 1003.1c (threads).
1994 X/Open Spec 1170, which becomes the Single UNIX Specification and the UNIX 95 brand.
1998 The Austin Group forms to end the split between POSIX and the SUS.
2001 POSIX.1-2001 = SUSv3 = UNIX 03. One document, four names, four volumes.
2008 POSIX.1-2008 = SUSv4. The edition most systems still report today.
2013, 2016 Technical Corrigenda 1 and 2: fixes, no new features.
2017 POSIX.1-2017: the 2008 text with both corrigenda applied. Technically identical.
2024 POSIX.1-2024, Base Specifications Issue 8. The first real revision in sixteen years.

3.1 What the 2024 Revision Changed

IEEE and The Open Group published POSIX.1-2024 on 14 June 2024. After sixteen years of corrigenda, it is a substantial revision, and almost all of it is the standard catching up with what implementations already did.

On the C side it adds around a hundred functions, most of them from the C17 language standard, plus long-established extensions that every system had grown independently: strlcpy() and strlcat() from OpenBSD, memmem(), reallocarray(), getentropy() and qsort_r(). The c99 compiler utility is gone, replaced by c17.

On the shell side it adds utilities that have been on every Linux system for decades: readlink, realpath, timeout, and the gettext family for translated messages. It also standardises two things script writers had been told for years were not portable: set -o pipefail, and sed -E for extended regular expressions.

Two warnings come with that news. First, your documentation is probably older than the standard: the standards(7) manual page shipped with Linux man-pages 6.7 stops at the 2018 edition. Second, and more important for daily work, a standard is a promise about the future, not a description of the machine in front of you. Section 7.4 shows a shell that still refuses pipefail two years after it was standardised.

Back to top

4. Simple Use Cases: POSIX at Your Prompt

POSIX sounds like something only standards committees touch. In fact your system ships several tools whose only job is to answer questions about it, and they are worth knowing before you need them.

4.1 Asking the System What It Provides

getconf without arguments reports one value; with -a (short for all) it dumps everything the system knows. Filtering that dump is the quickest way to see which optional parts of the standard are present:

$ getconf -a | grep -E '_POSIX_(VERSION|THREADS|TIMERS|SPAWN|SHELL)'
_POSIX_THREADS                     200809
_POSIX_TIMERS                      200809
_POSIX_VERSION                     200809
_POSIX_SHELL                       1
_POSIX_SPAWN                       200809

Read those values as answers to yes-or-no questions. A date means "this optional feature is present, at this edition of the standard". A 1 means "present", an empty value means "not supported here". Threads, timers and posix_spawn() are all optional units in the text of the standard, even though every general-purpose system has had them for twenty years.

The same tool reports the system's limits, which are more useful than they look:

$ getconf ARG_MAX          # bytes available for a command line
2097152
$ getconf LINE_MAX         # longest input line a text utility must handle
2048
$ getconf NAME_MAX /       # longest single filename on this filesystem
255
$ getconf PATH_MAX /       # longest full path
4096

ARG_MAX is the number behind the error "Argument list too long", and it is why find ... -exec cmd {} + and xargs exist. NAME_MAX and PATH_MAX take a path argument because the answer depends on the filesystem mounted there, not on the system as a whole.

4.2 The Standard PATH

POSIX defines a PATH value that is guaranteed to find the standard utilities, and getconf will hand it to you:

$ getconf PATH
/bin:/usr/bin

This is the value to use when a script must not depend on whatever PATH it inherited. The shell has a matching tool: command -p (short for path) looks the command up in exactly that standard PATH, ignoring yours:

$ command -p -v ls
/bin/ls

That matters in a cron job, a systemd unit, or anything running under sudo, where the PATH is not the one you tested with. If a script works at your prompt and fails from cron with "command not found", the PATH is the first thing to check, and command -p is the portable way to stop caring.

4.3 Reading the Standard Itself

The standard is not a secret document behind a paywall. The Open Group publishes the full text as HTML for free at pubs.opengroup.org, and it is the authority worth citing in any argument about portability. It is also readable: each utility page has the same layout, with SYNOPSIS, OPTIONS, an EXAMPLES section, and a RATIONALE explaining why the committee chose what it did.

You can also read it as manual pages. The POSIX text ships as its own manual sections, where 1p is utilities and 3p is functions:

$ man 1p ls
No manual entry for ls in section 1p

Those pages are a separate package, because the standard's licence is not free enough for the main archives. On Debian and Ubuntu it sits in multiverse:

$ sudo apt install manpages-posix manpages-posix-dev
$ man 1p ls            # what the standard says about ls
$ man 3p open          # what the standard says about open()

The difference between man 1 ls and man 1p ls is precisely the difference this article is about. The first tells you what your ls does. The second tells you what every ls must do. When they disagree, the extra behaviour is a local extension, and using it is a decision rather than an accident.

Two more pages are worth bookmarking, and both are already installed:

$ man 7 standards        # every standard a STANDARDS section can name
$ man 7 posixoptions     # the optional units and how to test for them

4.4 Checking a Filename Is Portable

POSIX defines a portable filename character set: letters, digits, dot, underscore and hyphen. The pathchk utility checks names against it, with -p for portable:

$ pathchk -p 'My File.txt'
pathchk: non-portable character ' ' in file name 'My File.txt'
$ pathchk -p 'a+b'
pathchk: non-portable character '+' in file name 'a+b'
$ pathchk -p 'report_1.txt'
$ echo $?
0

Silence means the name is safe. This is not about what Linux accepts, which is nearly any byte except / and NUL. It is about what survives a trip through another system, an archive format, or a script that forgot to quote a variable.

-p checks the length against the standard as well, and the standard is stricter than any system you have used this century:

$ pathchk -p 'backup-2026.tar.gz'
pathchk: limit 14 exceeded by length 18 of file name component
         'backup-2026.tar.gz'

Fourteen characters is the floor POSIX guarantees for one filename component, a number inherited from early Unix filesystems. Your filesystem allows 255, which is why nobody hits this. It is a good illustration of what -p means: not "will this work here", but "will this work on the smallest system the standard still permits".

Back to top

5. Moderate Use Cases: Shell Scripts That Travel

Most of the POSIX you meet in practice is in the shell, because that is where the promise is easiest to make and easiest to break. This section is about keeping it.

5.1 What #!/bin/sh Actually Promises

The first line of a script is a statement about which language the rest of the file is written in. #!/bin/bash says "this is Bash". #!/bin/sh says something quite different: "this is the standard shell language, and any POSIX shell may run it".

That is a promise about your code, not about the system. And the system takes you at your word, because /bin/sh is not the same program everywhere:

$ ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Mar 31  2024 /bin/sh -> dash

On Debian and Ubuntu, /bin/sh is dash, a small strict shell that implements the standard and almost nothing else. On Red Hat and Fedora it is Bash started under the name sh, which turns on a partial compatibility mode. On Alpine it is BusyBox ash. Write #!/bin/sh and use a Bash feature, and the script works on one of those and fails on the others.

The shebang is not a suggestion. If a script needs Bash, say #!/bin/bash and stop worrying about portability. If it says #!/bin/sh, it has promised to stay inside the standard, and something will eventually hold it to that promise.

5.2 What the POSIX Shell Does Not Have

Here are the constructs that break most often, checked against dash on Ubuntu 24.04. Each one is fine in Bash and fine in a script that says so; each one is a bug in a file that starts #!/bin/sh:

Bash-onlyWhat dash doesPortable form
[[ "$a" = "$b" ]] [[: not found [ "$a" = "$b" ]
[ "$a" == "$b" ] [: x: unexpected operator [ "$a" = "$b" ], one equals sign
arr=(one two) Syntax error: "(" unexpected positional parameters: set -- one two
${name^^} Bad substitution printf '%s' "$name" | tr a-z A-Z
source file source: not found . file, a dot and a space
function f { ... } Syntax error: "}" unexpected f() { ... }
echo -e "a\tb" prints -e itself as the first word printf 'a\tb\n'
{1..5} prints the text {1..5} a while loop with arithmetic
<<< "text" syntax error printf '%s\n' "text" | or a here-document
$RANDOM expands to nothing awk, or read from /dev/urandom

One entry on that list deserves a note. local works in dash, in Bash, in ksh and in BusyBox, and yet it is not standard: the text of POSIX lists local among the names whose results are unspecified. In practice it is safe on any system you are likely to meet, and this is a good example of the gap between "portable" and "standard". The standard describes the floor, not the ceiling.

The expansions that are standard cover most of what people reach for Bash to do:

$ dash -c 'x=abc; echo ${x:-default} ${#x} ${x%c} ${x#a}'
abc 3 ab bc

Those are the POSIX parameter expansions: a default value, a length, and stripping a suffix or prefix. ${x%.txt} to drop an extension and ${x##*/} to take a basename are standard everywhere, and they save you from calling basename in a loop.

5.3 Why printf Replaced echo

The standard says the result is implementation-defined the moment the first operand starts with -n, or any operand contains a backslash. That is not committee pedantry. Each shell picks a behaviour, documents it, and is entirely correct, and the two answers sit on the same machine:

$ bash -c 'echo "a\tb"'
a\tb
$ dash -c 'echo "a\tb"'
a	b

Same command, same string, two results. Bash prints the backslash and the letter; dash interprets the escape and prints a tab. Neither is wrong, which is exactly the problem: there is no answer you can write a script against. printf has one defined behaviour and is a standard utility in its own right:

$ printf 'a\tb\n'
a	b

The rule that follows is short: use echo for a fixed string with no escapes and no options, and printf for everything else. Note the explicit \n: printf does not add a newline for you, which is a feature the moment you are building output piece by piece.

5.4 command -v, Not which

To test whether a command exists, scripts often call which. It is not in POSIX at all, it varies between distributions, and on some systems it is a csh script. The standard answer is a shell builtin:

if command -v git >/dev/null 2>&1; then
    echo "git is available"
fi

This works in every POSIX shell, starts no process, and reports builtins and functions as well as programs on disk. which can only ever see the third kind.

5.5 Testing That a Script Is Portable

You do not have to guess whether a script stayed inside the standard. Run it under a shell that has nothing else to offer. dash -n parses a file without executing it, which catches every syntax-level bashism in one pass:

$ dash -n deploy.sh
$ echo $?
0

$ dash -n legacy.sh
legacy.sh: 2: Syntax error: "(" unexpected
$ echo $?
2

That is a two-second check you can put in a Makefile or a CI job. It only finds syntax errors, so a bad option to a utility slips through, but the array on line 2 does not.

For the rest, shellcheck reads the shebang and applies the matching rules, so a file starting #!/bin/sh gets warned about constructs that only Bash has. If a file has no shebang, tell it explicitly with a directive on the first line: # shellcheck shell=sh.

5.6 POSIXLY_CORRECT and the GNU Extensions

The GNU utilities go beyond the standard in one habit you use every day without noticing: they let options come after the filenames. The standard says an option after the first operand is an operand, and GNU tools rearrange your command line instead. Setting the environment variable POSIXLY_CORRECT switches that off:

$ ls notes.txt -l
-rw-rw-r-- 1 peter peter 0 Sep  7 10:36 notes.txt

$ POSIXLY_CORRECT=1 ls notes.txt -l
ls: cannot access '-l': No such file or directory
notes.txt

With the variable set, -l stops being an option and becomes a filename, which does not exist. This is what a strict system would have done all along.

The same variable changes defaults where GNU chose a friendlier one than the standard requires. du is the classic case, because POSIX specifies 512-byte blocks and GNU uses 1024:

$ du -s report.log
4	report.log
$ POSIXLY_CORRECT=1 du -s report.log
8	report.log

The same file, the same disk, two numbers that differ by a factor of two. Neither is wrong; they count in different units. Set POSIXLY_CORRECT=1 for a single command when you want to know how a strict system would behave, and do not set it globally: it changes the behaviour of a great many programs at once, and the surprise will arrive weeks later. Note also that POSIXLY_CORRECT is itself a GNU convention. The standard never mentions it.

Back to top

6. Advanced Use Cases: POSIX in C

The other half of the standard is the C interface, and it decides something that surprises people the first time they meet it: which functions your compiler will admit exist.

If you never compile C yourself, skip ahead to section 7. Nothing there builds on this section, and the C items in the later lists are short recaps of what follows here.

6.1 The C Library Is the Layer, Not the Kernel

POSIX describes an interface for programs, and on Linux that interface is provided by the C library, not by the kernel. Your program calls a standard function; the C library decides what to ask the kernel for:

your program
     |
POSIX functions:  open()  fork()  pthread_create()
     |
the C library:  glibc, or musl on a smaller system
     |
the Linux system call interface
     |
the kernel

Those two middle layers are not the same list, and one command shows it. POSIX defines fork(). Linux does have a fork system call, and glibc does not use it: it creates the child with clone() instead, because that one call covers processes and threads together.

$ strace -f -e trace=clone,fork ./forktest
clone(child_stack=NULL,
      flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, ...) = 16716
+++ exited with 0 +++

The program called fork(). The kernel was asked for clone(). Nothing is broken: a POSIX function is a promise about behaviour, not a name for a system call. Some functions are thin wrappers, some are built out of a different call, and parts of pthread live mostly in the library.

Two consequences follow. POSIX says nothing about which system calls exist or how they are numbered, so the kernel is free to change how a function is implemented as long as the function keeps behaving as specified. And conformance is a property of a whole system - kernel, C library, shell and utilities together - which is why "is Linux POSIX?" has no clean answer. Linux is the kernel. The thing that implements POSIX is the distribution.

6.2 Feature Test Macros

Here is a program that uses strdup(), a function that has been in POSIX since 2008 and in every C library for far longer:

#include <stdio.h>
#include <string.h>

int main(void) {
    char *copy = strdup("hello");
    printf("%s\n", copy);
    return 0;
}

Compile it as strict C99 and the compiler says the function does not exist:

$ gcc -std=c99 -Wall -o t t.c
t.c: In function 'main':
t.c:4:18: warning: implicit declaration of function 'strdup';
                   did you mean 'strcmp'?

Nothing is missing from your system. strdup() is in the C library, and the linker will find it. What happened is that -std=c99 asked for the ISO C language and nothing more, and strdup() is not in ISO C: it is POSIX. So the header hid it.

The switch that unhides it is a feature test macro, defined before any header is included, or on the command line:

$ gcc -std=c99 -D_POSIX_C_SOURCE=200809L -Wall -o t t.c
$ ./t
hello

The number is the same date encoding as before: ask for the 2008 edition, get everything the 2008 edition defines. Ask for 199309L and you get the smaller set the 1993 amendment defined. The macros are a way of saying which standard you are writing against, and the headers show you exactly that much:

MacroWhat it exposes
_POSIX_C_SOURCE=200809L POSIX.1-2008 and everything ISO C defines
_XOPEN_SOURCE=700 The above plus the XSI extensions (SUSv4)
_GNU_SOURCE Everything: POSIX, XSI, BSD and GNU-only additions
none of them With GCC's default -std=gnu*, roughly _DEFAULT_SOURCE: POSIX plus common extensions

This is why the same source file compiles on your laptop and fails in a container with a stricter build flag, and why manual pages carry a line in the SYNOPSIS telling you which macro a function needs. man 7 feature_test_macros is the complete reference, and it is one of the most useful pages on the system.

The mapping is visible in the headers themselves:

$ grep -n 'define _POSIX_VERSION' /usr/include/unistd.h
34:# define _POSIX_VERSION	200809L
37:# define _POSIX_VERSION	200112L
40:# define _POSIX_VERSION	199506L
43:# define _POSIX_VERSION	199309L
46:# define _POSIX_VERSION	199009L

Five definitions in one file, inside a chain of #if branches. Whichever macro you asked for picks one of them, and your program then sees the version of the interface that goes with it.

6.3 How POSIX Functions Report Failure

Nearly every function in the standard reports trouble the same way: a return value saying that something went wrong, and the variable errno saying what. The standard defines the error names, which is why ENOENT means the same thing on Linux, on macOS and on AIX:

#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>

int main(void) {
    int fd = open("missing.txt", O_RDONLY);
    if (fd == -1) {
        int saved = errno;          /* copy it on the very next line */
        printf("failed: errno %d, %s\n", saved, strerror(saved));
    }
    return 0;
}
$ ./err
failed: errno 2, No such file or directory

Two rules come with errno, and both are in the standard rather than in folklore. First, only read it after a call has told you it failed. A successful call is allowed to change it, and nothing resets it to zero for you, so a stale value from an earlier call is the usual reason an error message names the wrong problem. Second, read it immediately: any library call in between, printf() included, may overwrite it. That is why the example copies it into saved before doing anything else.

The error worth knowing by name is EINTR. A blocking call interrupted by a signal can return with it instead of finishing, so portable code has to decide whether to retry. man 7 signal is the reference, and it also names a place where Linux goes past the standard: a blocking call here can fail with EINTR after the process is stopped and resumed, which the page says "is not sanctioned by POSIX.1, and doesn't occur on other systems".

6.4 Asking at Compile Time and at Run Time

The same question has two answers, and mixing them up is a real bug. A program can read the compile-time constant from <unistd.h>:

#include <stdio.h>
#include <unistd.h>

int main(void) {
    printf("%ld\n", (long) _POSIX_VERSION);
    return 0;
}
$ gcc -o v v.c && ./v
200809

That is what the headers on the build machine said. To ask the machine the program is actually running on, use the run-time functions: sysconf() for system-wide values, pathconf() and fpathconf() for values that depend on a filesystem, and confstr() for string values such as the standard PATH. getconf from section 4.1 is a thin wrapper around exactly these three.

The distinction matters for limits. The constants whose names start with _POSIX_ are the minimum the standard guarantees, not the size of anything on your machine. The two numbers are not close:

$ grep -n '_POSIX_NAME_MAX\|_POSIX_PATH_MAX' \
      /usr/include/x86_64-linux-gnu/bits/posix1_lim.h
74:#define	_POSIX_NAME_MAX		14
97:#define	_POSIX_PATH_MAX		256

$ getconf NAME_MAX /       # what this filesystem really allows
255
$ getconf PATH_MAX /
4096

A conforming system must allow a filename component of 14 characters and a path of 256. This one allows 255 and 4096. Write for the floor if your code must run anywhere, but never compile either number into a buffer size: ask at run time, on the path you are about to use, because the answer differs between a local ext4 mount and a network share on the same system.

6.5 The Optional Parts

Not everything in POSIX is mandatory. Whole areas are optional units that a conforming system may leave out, and each one has a macro to test for it. Threads, real-time signals, message queues, asynchronous I/O and shared memory are all in this category:

$ man 7 posixoptions      # the full list, with the sysconf name for each

On a general-purpose Linux system nearly all of them are present, which is why this rarely bites. It matters when your target is smaller: an embedded system, a minimal container image with musl instead of glibc, or a real-time variant. The correct way to find out is to ask the system rather than to assume, either with getconf in a script or with sysconf() in code.

6.6 The Standard Compiler Is a Shell Script

POSIX standardises a C compiler as a command-line utility. Until 2024 it was called c99, and your system has it:

$ ls -l /bin/c99
lrwxrwxrwx 1 root root 21 Nov 17  2020 /bin/c99 -> /etc/alternatives/c99
$ ls -l /usr/bin/c99-gcc
-rwxr-xr-x 1 root root 454 Nov 17  2020 /usr/bin/c99-gcc
$ head -10 /usr/bin/c99-gcc
#! /bin/sh

# Call the appropriate C compiler with options to accept ANSI/ISO C
# The following options are the same (as of gcc-3.3):
# 	-std=c99
# 	-std=c9x
# 	-std=iso9899:1999
# 	-std=iso9899:199x

extra_flag=-std=c99

The POSIX C compiler on a Linux system is a 454-byte shell script that adds -std=c99 and calls GCC. This is the pattern for a great deal of the standard: it does not require a particular implementation, only a command with a specified name and specified behaviour. Anything that satisfies the description is a conforming implementation, including a wrapper you could read over a coffee. The 2024 revision retired c99 and standardised c17 in its place.

6.7 The STANDARDS Section in Manual Pages

Linux manual pages for functions carry a STANDARDS section that names the standard an interface comes from, and it is the fastest portability check available:

$ man 2 open      # then look for STANDARDS: POSIX.1-2008
$ man 3 strdup    # POSIX.1-2008; before that, a common extension
$ man 2 epoll_create  # STANDARDS: Linux only

"Linux" in that section means the code you are writing will not compile on a BSD or on macOS. "POSIX.1-2008" means it will work anywhere. Older pages phrase this as CONFORMING TO instead, which is the same information under a different heading.

6.8 Where Linux Goes Past the Standard

That section is most useful when two interfaces do the same job and only one of them is portable. Waiting for activity on many file descriptors at once is the clearest case:

$ man 2 select      # STANDARDS: POSIX.1-2008
$ man 2 poll        # STANDARDS: POSIX.1-2008
$ man 7 epoll       # STANDARDS: Linux
$ man 2 signalfd    # STANDARDS: Linux
$ man 7 inotify     # STANDARDS: Linux

select() and poll() are in the standard and work on any Unix. epoll is Linux only, and it exists because those two scale badly once you are watching thousands of descriptors: they hand the kernel the whole list on every call. The same pattern repeats with inotify for file changes, with eventfd and signalfd, and with the newer io_uring. Each is a Linux answer to a real limit in the portable interface.

That is a trade, not a mistake. A portable library sticks to poll(). A server that will only ever run on Linux uses epoll and says so. What goes wrong is choosing without noticing, which is exactly what the STANDARDS section prevents.

The same split exists outside C. /proc is a Linux invention and appears in no standard, so every script that reads /proc/$$/fd or /proc/meminfo is Linux-only, however ordinary it looks. That is usually fine on a server you control. It is worth knowing before someone runs the script on a Mac.

Back to top

7. Something Most Users Do Not Know

7.1 The Standard Archiver Is Not tar

Ask anyone to name a POSIX utility and tar comes up early. It is not in the standard. The standard archiver is pax, and the rationale explains why in one sentence: "The pax utility was new for the ISO POSIX-2:1993 standard. It represents a peaceful compromise between advocates of the historical tar and cpio utilities." The committee could not choose between two archive formats and two camps, so it specified a third utility that reads and writes both.

The compromise did not win:

$ command -v tar
/bin/tar
$ command -v pax
$ echo $?
1

tar is on the machine and pax is not installed at all. The non-standard tool is everywhere, the standard one has to be installed on purpose, and every script in the world uses tar. This is worth remembering whenever "POSIX" is used as a synonym for "portable": the standard describes what a conforming system must provide, and reality provides a rather different set. GNU tar and BSD tar also differ from each other, which is the practical portability problem the standard failed to solve.

7.2 POSIX ACLs Are Not POSIX

Access control lists on Linux are universally called POSIX ACLs, in documentation, in filesystem options, and in the name of the extended attribute that stores them (system.posix_acl_access). They were never standardised. The manual page says so plainly:

$ man 5 acl
STANDARDS
       The IEEE 1003.1e draft 17 ("POSIX.1e") document describes several
       security extensions to the IEEE 1003.1 standard. While the work on
       1003.1e has been abandoned, many UNIX style systems implement parts
       of POSIX.1e draft 17, or of earlier drafts.

Work on POSIX.1e stopped in the late 1990s and the draft was withdrawn. Implementers had already built against draft 17, so the interface survived its own standard: getfacl, setfacl and the acl_* C functions on Linux implement a document that was never finished. The name stuck because there was nothing else to call it.

The practical consequence is that ACL behaviour varies between systems in ways no standard resolves, and a cp without -p or -a quietly drops them. When permissions "reset themselves" after a copy or a restore, this is usually why.

7.3 Two Certified UNIX Systems Are Linux Distributions

Linux is not certified UNIX, and everybody knows it. What almost nobody knows is that two Linux distributions passed the certification and hold the trademark: Inspur K-UX, based on Red Hat Enterprise Linux, and Huawei's EulerOS, based on CentOS. Both are registered with The Open Group alongside macOS, AIX, HP-UX and Solaris.

They prove the point made in section 1.3. Certification is a commercial process: you run the test suite, you pay the fee, you license the trademark. Nothing technical stops a Linux distribution from being certified UNIX, and the reason mainstream distributions are not is that no one wants to pay for a trademark their users do not ask for. The distinction between "UNIX" and "Unix-like" is a legal one, not an engineering one.

7.4 The Standard Follows Practice, and Your System Lags Behind It

People assume standards lead and implementations follow. POSIX works the other way round: the committee prefers to describe what implementations already do, which is why the 2024 revision finally standardised readlink, realpath and timeout, tools that had been on every Linux system for twenty years.

The delay runs in both directions, and the second one catches people out. set -o pipefail was standardised in 2024. Here it is in the shell that is /bin/sh on this machine, today:

$ dash -c 'set -o pipefail'
dash: 1: set: Illegal option -o pipefail

The same is true of $'...' quoting, which the 2024 text defines in its own section and dash does not understand:

$ bash -c "printf '%s\n' \$'a\tb'"
a	b
$ dash -c "printf '%s\n' \$'a\tb'"
$a\tb

So "it is in POSIX" and "it works everywhere" are two different claims, and for anything added in the last edition the second one will be false for years. Portability is decided by the oldest system you have to support, not by the newest standard.

7.5 bash --posix Is Not a Portability Check

Bash has a POSIX mode, reached with --posix or set -o posix, and it looks like exactly the tool you want for testing a script. It is not. It adjusts a list of behaviours where Bash's default disagrees with the standard, and it leaves every Bash extension in place:

$ bash --posix -c '[[ 1 = 1 ]] && echo yes'
yes
$ bash --posix -c 'a=(x y); echo ${a[1]}'
y
$ bash --posix -c 'echo {1..3}'
1 2 3

Double brackets, arrays and brace expansion all still work in POSIX mode. A script full of bashisms passes cleanly and then fails on the first machine where /bin/sh is dash. Bash started under the name sh enters the same partial mode, which is why a script can pass on Red Hat, where sh is Bash, and fail on Debian, where it is not. To test portability, run the script under a different shell, as in section 5.5.

7.6 Portable Source Is Not a Portable Binary

POSIX standardises source code. Compile that source on Linux and you get a program that runs on Linux and nowhere else, no matter how carefully portable the code was. Two things decide that, and the standard governs neither.

The first is the executable format. Linux uses ELF, macOS uses Mach-O, and a file in one format means nothing to a system expecting the other:

$ file /bin/ls
/bin/ls: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), ...

The second is the numbering underneath every system call. The number for write is not even the same across Linux architectures:

$ grep -w 'define __NR_write' /usr/include/x86_64-linux-gnu/asm/unistd_64.h
#define __NR_write 1
$ grep -w 'define __NR_write' /usr/include/asm-generic/unistd.h
#define __NR_write 64

Number 1 on x86-64, number 64 on the architectures that use the generic table, such as arm64. That numbering, the registers the arguments travel in, how a structure is laid out in memory, how symbols are named: all of it is the ABI, the application binary interface. POSIX is an API, an application programming interface. It is a promise about what your source code means, and each system keeps that promise with its own compiler.

"It runs on any POSIX system" always has an unwritten word in front of it: recompiled. Portability at the source level and compatibility at the binary level are different problems, and POSIX only solves the first.

This is why a container image built for one architecture will not run on another even though both are Linux, and why "works on Unix" has always meant a build step rather than a copy.

7.7 Some Everyday Options Were Never Standard

A few habits are so common that people assume they are universal. The standard's date utility defines exactly one option, -u, and its conversion specifications come from strftime(), which has no %s. Both of these are GNU extensions:

$ date +%s                # seconds since the epoch: GNU, not POSIX
$ date -d '2 days ago'    # date arithmetic: GNU only, BSD uses -v

The same holds for sed -i, which GNU spells -i and BSD spells -i '', for grep -P, for find -print0 and xargs -0, and for mktemp and seq, which are not in the standard at all. None of this means you should stop using them. It means that when a script has to run on more than one kind of system, these are the lines that will need attention, and find -exec cmd {} + is the standard replacement for the -print0 pipeline.

Back to top

8. Best Practices

  • Match the shebang to the language you wrote. #!/bin/bash if you use Bash features, #!/bin/sh only if you stayed inside the standard. Both are correct; claiming the wrong one is the bug.
  • Test sh scripts under a strict shell. dash -n script.sh takes two seconds and catches every syntax-level bashism. Put it in your Makefile or CI job next to shellcheck.
  • Use printf, not echo, whenever escapes or options are involved. echo's behaviour with -n and backslashes is implementation-defined, and two shells on one machine already disagree.
  • Use command -v to test whether a command exists. which is not standard, varies between distributions, and cannot see builtins or functions.
  • Quote every variable, and keep filenames in the portable character set. "$file" costs two characters. pathchk -p tells you which names will cause trouble elsewhere.
  • Ask the system for limits instead of hard-coding them. getconf in scripts, sysconf() and pathconf() in C. The _POSIX_ constants are guaranteed minimums, not your machine's values.
  • Set the feature test macro your code needs, explicitly. -D_POSIX_C_SOURCE=200809L documents which standard you wrote against and stops a stricter build flag from hiding half the C library.
  • Name the version when you say "POSIX". "POSIX.1-2008" is a fact. "POSIX compliant" is a feeling, and the two systems in the argument are probably both right.
  • Do not set POSIXLY_CORRECT globally. Use it per command to see how a strict system would behave, then take it off. It changes many programs at once.
  • Read the documentation. It is all installed, or one package away:
$ man 7 standards            # every standard, with dates and lineage
$ man 7 posixoptions         # the optional units of the standard
$ man 7 feature_test_macros  # which macro exposes which interfaces
$ getconf -a                 # what this system reports about itself
$ man 1p sh                  # the standard itself, after installing manpages-posix

The full text is free to read at pubs.opengroup.org, and it is the only source that settles an argument.

Back to top

9. Common Mistakes

9.1 Myth versus Reality

MythReality
"POSIX is software you can install." POSIX is a document. Systems implement it; nothing on your machine is POSIX itself.
"Linux is POSIX certified." Linux is not certified. It implements most of the standard, and certification is a commercial licence rather than a technical grade.
"#!/bin/sh means Bash." On Debian and Ubuntu it is dash, on Alpine BusyBox ash. It means "the standard shell", whichever program that is.
"If it is a common command, it is POSIX." tar, which, mktemp, seq, ping and ssh are all absent from the standard.
"POSIX ACLs are part of POSIX." They come from IEEE 1003.1e draft 17, which was abandoned. The name survived the standard.
"bash --posix checks my script for portability." It changes a few defaults and keeps every Bash extension. Arrays and [[ ]] still work. Test with dash instead.
"It was standardised, so it works everywhere." set -o pipefail is in POSIX.1-2024 and dash still rejects it. Standards lead implementations by years.
"My code is POSIX, so the binary runs on any Unix." POSIX standardises source code. Executable formats and system call numbers are the ABI, which POSIX does not touch. Portability means recompiling.
"POSIX defines where files go, like /etc and /var." That is the Filesystem Hierarchy Standard, a separate Linux document. POSIX says almost nothing about the directory layout.
"The _POSIX_ limits tell me what my system supports." They are the minimums the standard guarantees. Ask sysconf() or pathconf() for the real values.

9.2 Other Traps to Avoid

  • Writing #!/bin/sh out of habit. Most scripts that start that way are Bash scripts that have not yet met dash. Either fix the code or fix the line.
  • Testing only on the machine you wrote it on. A portability bug is by definition invisible on one system. The check is cheap: dash -n, or a container with a different base image.
  • Assuming the GNU manual page is the standard. man 1 sed documents your sed. man 1p sed documents every sed. The gap between them is exactly your portability risk.
  • Hard-coding PATH_MAX as a buffer size. It is a guaranteed minimum and it varies by filesystem. Allocate from pathconf() instead.
  • Believing a compiler error means a missing library. "Implicit declaration of function" for a POSIX function almost always means a feature test macro, not a missing package.
  • Confusing POSIX with the FHS or the LSB. POSIX defines the interface. The Filesystem Hierarchy Standard defines the directory layout, and the Linux Standard Base defined a binary compatibility profile. Different documents, different scope.
  • Relying on locale-dependent output. Sorting, date formats and message text change with the locale. Set LC_ALL=C when a script parses the output of another command, so it gets the same bytes on every machine.
  • Treating "unspecified" as "will not happen to me". The four words from section 1.4 are a promise scale, and unspecified is where implementations legitimately differ. It is precisely where the three-in-the-morning failure lives.
Back to top

10. Summary

POSIX is the written contract between programs and the systems they run on: a document, published by IEEE and The Open Group, that says what a Unix-like system must provide. It is why a shell script from 1995 still runs, why man 1p and man 1 are different pages, and why the argument about whether a machine is "wrong" usually has a citable answer.

  • POSIX is a standard document, not software. Systems implement it; certification is a separate commercial process, and Linux does not buy it.
  • The name means Portable Operating System Interface. The X is a nod to Unix naming, and Richard Stallman coined the term.
  • It is published in four volumes: definitions, C interfaces, shell and utilities, and rationale. One document also known as IEEE 1003.1, ISO/IEC 9945, and the Single UNIX Specification.
  • It grew out of the split between System V and BSD, was unified with the Single UNIX Specification by the Austin Group in 2001, and was revised as POSIX.1-2024 after sixteen years of stability.
  • getconf answers questions about your own system: _POSIX_VERSION, the standard PATH, and the limits behind errors like "Argument list too long".
  • #!/bin/sh promises standard shell code. On Debian and Ubuntu that shell is dash, and dash -n checks the promise in two seconds.
  • The usual bashisms - [[ ]], arrays, ${x^^}, source, echo -e - all have standard equivalents, and printf replaces echo for anything with an escape in it.
  • Four words carry the whole promise: defined, implementation-defined, unspecified, undefined. The middle two are where portability bugs live.
  • On Linux the C library provides POSIX, not the kernel. A program that calls fork() reaches the kernel as clone(), and conformance belongs to the whole distribution rather than to Linux itself.
  • In C, feature test macros decide which functions the headers expose. -D_POSIX_C_SOURCE=200809L is the difference between "no such function" and a clean build.
  • Standard functions report failure through a return value plus errno. Read it only after a failure, and read it immediately: the next library call may overwrite it.
  • POSIX is an API standard, not an ABI one: portable source, recompiled per system. Executable formats and system call numbers are outside it, and select() is portable where epoll is Linux only.
  • Ask for limits at run time with sysconf() and pathconf(). The _POSIX_ constants are minimums, not measurements.
  • The standard archiver is pax, which is not installed. POSIX ACLs come from an abandoned draft. Two certified UNIX systems are Linux distributions.
  • bash --posix is not a portability check, and something being in the newest standard does not mean the shell in front of you has it yet.

This is the quick reference worth keeping:

getconf _POSIX_VERSION       which edition this system implements
getconf _XOPEN_VERSION       which Single UNIX Specification issue
getconf -a                   every value the system reports
getconf PATH                 the standard PATH for finding utilities
getconf ARG_MAX              the limit behind "Argument list too long"
command -p -v NAME           find a command using the standard PATH
command -v NAME              portable test that a command exists
pathchk -p NAME              is this filename portable
dash -n script.sh            parse a script with a strict POSIX shell
POSIXLY_CORRECT=1 cmd        run one command as a strict system would
printf 'a\tb\n'              portable output; echo escapes are implementation-defined
man 7 standards              every standard, with dates and lineage
man 7 posixoptions           the optional units of the standard
man 7 feature_test_macros    which macro exposes which C interfaces
man 2 poll / man 7 epoll     a portable interface versus a Linux-only one
strace -e trace=clone ./prog which system call a POSIX function becomes
man 1p sh / man 3p open      the standard's own manual pages
gcc -D_POSIX_C_SOURCE=200809L  expose POSIX.1-2008 in the headers

And when a script has run untouched for three years and then fails the week a server is rebuilt, the code is rarely what changed: something underneath it stopped being the system the script quietly assumed, and the standard is where you find out which of the two was making things up.

Back to top
Linux concept: POSIX
Peter Martin
Peter Martin
Joomla Specialist

Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.