Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Bash & the POSIX Shell  Module 14 – Portability: POSIX sh, versions and platforms

What POSIX.1-2024 changed for shell scripts

Read what POSIX.1-2024 added to the shell language and utilities, see what bash 5.3.20 and dash 0.5.13.5 run, and probe for Issue 8 features safely.

  • Intermediate
  • 15 minutes
  • Examples run with Bash 5.3.20, dash 0.5.13.5, shellcheck 0.11.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • List the POSIX.1-2024 (Issue 8) changes that affect everyday scripts
  • Test which ones bash 5.3.20 and dash 0.5.13.5 support today
  • Use the new features without breaking older systems

Before you start

A script written for POSIX.1-2024 may lean on features that a POSIX.1-2017 shell never had to provide: of the seven probed below, dash 0.5.13.5 ran four and the bash 5.3.20 printf builtin refused one. POSIX.1-2024, also called Issue 8: The eighth edition of The Open Group Base Specifications, published jointly as IEEE Std 1003.1-2024 (POSIX.1-2024). It replaces Issue 7 and adds dollar-single-quotes, pipefail, trap -p, a Job Control section (2.11) and new behaviour to read, test and printf., changed the shell language in four ways that scripts meet first: $'...' quoting, set -o pipefail, a Job Control section and trap -p. It also gave the utilities read -d, the test operators -nt, -ot and -ef and numbered printf conversions such as %2$s, and it added new programs, among them realpath, readlink and timeout.

The sections below place each change in its edition, read the recorded runs of bash 5.3.20 and dash 0.5.13.5 against the standard, and end with a guard that switches an Issue 8 option on only where the running shell has it.

Issue 8 mostly adds to the Issue 7 shell standard

The shell every POSIX system runs is specified in one volume of The Open Group Base Specifications, which The Open Group publishes together with the IEEE. The whole set is revised in editions called issues, and each issue is also an IEEE Std 1003.1 edition. Issue 7: The seventh edition of The Open Group Base Specifications, aligned with IEEE Std 1003.1-2008 and later corrigenda (POSIX.1-2017). Debian policy still names this edition as the baseline for /bin/sh scripts. began as IEEE Std 1003.1-2008, and its 2018 edition, IEEE Std 1003.1-2017, a revision of that text, is POSIX.1-2017; Issue 8 is IEEE Std 1003.1-2024. Each Issue 8 page ends with a change history that names the Austin Group defects behind its changes, which is where the defect numbers in this lesson come from. Walk through the three editions a script is most likely to meet, oldest first:

Timeline · 3 events in order Three editions of the POSIX shell standard Walk through the editions oldest first and note which of this lesson's features each one brought.
  1. Issue 6

    An earlier edition of the base specifications base

    The sixth edition of The Open Group Base Specifications, matching IEEE Std 1003.1-2001. The shell features most scripts lean on were already there: parameter expansion, the test utility, pipelines, and the set options -e and -u.

  2. Issue 7

    POSIX.1-2017, the edition Debian policy names baseline

    The seventh edition began as IEEE Std 1003.1-2008, and its 2018 edition is IEEE Std 1003.1-2017, a revision of that text with two technical corrigenda applied. The Debian Policy Manual still lets a /bin/sh script assume this edition plus a few extras it names, so a Debian package may not rely on what Issue 8 added.

  3. Issue 8

    POSIX.1-2024, the newest edition newest

    The eighth edition, published as IEEE Std 1003.1-2024. It adds dollar-single-quotes, set -o pipefail, a Job Control section (2.11) and trap -p to the shell language, new options to read, test and printf, and the new utilities realpath, readlink and timeout. Shells adopt its changes at their own pace.

The timeline has 3 events, from Issue 6 to Issue 8.

Each edition mostly adds to the one before it, so a script written for Issue 7 keeps working in an Issue 8 shell. The few removals had been marked obsolescent first: Issue 8 dropped the -a and -o operators and the parentheses of test (Austin Group Defect 1330), so a script that still uses them relies on its shell rather than on the standard. The risk runs the other way: a script written against Issue 8 may use a feature that a shell built for Issue 7 never had.

Policies lag the standard too. The Debian Policy Manual still lets a /bin/sh script assume POSIX.1-2017 plus a few extras it names, such as echo -n, local and the very -a and -o of test that Issue 8 removed. The POSIX sh versus Bash lesson shows how to choose the first line of a POSIX shell: A shell that implements the shell command language the POSIX standard defines for the sh utility. Bash and dash both do; Bash adds many extras the standard does not require. More in “POSIX sh vs Bash: spotting and fixing bashisms” script; this one adds the question of which edition that script may rely on.

Why Issue 8 does not mean portable

A feature in the standard is a promise about shells that conform to Issue 8, not a report on the shells already installed. The manual of the dash 0.5.12 that Ubuntu 24.04 ships does not list a pipefail option at all.

The probes in the next sections measure that gap one feature at a time, so the matrix later on shows both what Issue 8 asks for and what bash 5.3.20 and dash 0.5.13.5 already run.

Key idea

Issue 8 mostly adds to Issue 7, so a script that must also run on older shells can use each new feature only after checking that the running shell has it.

Checkpoint

Check: Issue 7 and Issue 8 editions

  1. Question 1 of 2 Which name belongs to POSIX.1-2024?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Issue 8 of The Open Group Base Specifications

    The Issue 8 index page names POSIX.1-2024, IEEE Std 1003.1-2024 and Issue 8 as one edition. The Issue 7 page names IEEE Std 1003.1-2017 as its 2018 edition and says that a newer edition exists.

  2. Question 2 of 2 A Debian maintainer updates a /bin/sh installer today. Which edition of the shell standard does Debian policy let the script assume?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Issue 7, plus the short list of extras the policy names

    The policy lets a /bin/sh script assume POSIX.1-2017 plus echo -n, local, the -a and -o operators of test and two XSI extensions of kill and trap. Anything else needs a first line that names its shell.

Four shell-language changes reach scripts first

Four changes to the shell language matter most to everyday scripts, because the shell reads them itself before any utility runs:

  • ANSI-C quoting: The $'…' form of quoting, in which backslash escapes such as \t, \n, \x41 and \u00e9 become the characters they name; part of POSIX.1-2024 and supported by Bash and dash. More in “Quoting: single, double, backslash and $'...'”, section 2.2.4 Dollar-Single-Quotes: inside $'...', escapes such as \t, \n, \xHH and \ddd turn into the bytes they name just before the word is expanded.
  • pipefail: Bash's set -o pipefail option: the exit status of a pipeline is the rightmost non-zero stage's status, or 0 when every stage succeeded. A stage killed by SIGPIPE (because its reader closed early) is reported as 128 + 13 = 141. More in “set -e, set -u and pipefail: what they really do”, an option of set added by Austin Group Defect 789: with it on, a pipeline takes the status of its rightmost stage that failed, so one failing stage is enough to fail the whole pipeline.
  • Section 2.11 Job Control, new in Issue 8: it gathers the shell’s job-control rules, from set -m to the fg and bg utilities and the signals that stop and continue a job, in one place.
  • trap -p, added by Austin Group Defects 1211 and 1212. Plain trap already listed the traps a script had set; -p prints the action of each condition you name, or of every condition, as commands the shell can read back in.

The next session runs seven probes under bash 5.3.20, the same bash in POSIX mode, and dash 0.5.13.5. Each prints supported when the shell does what Issue 8 asks; the first three lines of every block are the shell-language probes:

Which POSIX.1-2024 features each shell supports Python · issue8_matrix_session.py
from session import run, write

# Seven probes, one per POSIX.1-2024 addition the lesson names. The runner starts each probe under three shells:
# GNU Bash 5.3.20 (built from source), the same Bash with --posix, and dash 0.5.13.5. It prints "supported" when the
# probe does what POSIX.1-2024 says and "unsupported" when it does not, so the table stays stable between runs.
# Every probe sends its error messages to /dev/null: the table records what each shell did, not how it complained.
write("probe.sh", r"""set +e
label=$1
case $label in
  dollar_single)
    out=$(printf %s $'a\tb') ; expected=$(printf 'a\tb')
    [ "$out" = "$expected" ] && echo supported || echo unsupported
    ;;
  pipefail)
    (set -o pipefail) 2>/dev/null || { echo unsupported; exit 0; }
    (set -o pipefail; false | true) >/dev/null 2>&1
    [ $? -ne 0 ] && echo supported || echo unsupported
    ;;
  trap_p_exit)
    trap 'echo bye' EXIT 2>/dev/null
    out=$(trap -p EXIT 2>/dev/null)
    case $out in *bye*) echo supported;; *) echo unsupported;; esac
    trap - EXIT 2>/dev/null
    ;;
  read_d_delim)
    out=$(printf 'a:b:c' | { IFS= read -r -d : first 2>/dev/null && echo "$first"; }) 2>/dev/null
    [ "$out" = a ] && echo supported || echo unsupported
    ;;
  test_nt_ot)
    : > f2 ; sleep 1 ; : > f1
    if test f1 -nt f2 2>/dev/null && test f2 -ot f1 2>/dev/null; then echo supported; else echo unsupported; fi
    rm -f f1 f2
    ;;
  test_ef)
    : > f1
    if test f1 -ef f1 2>/dev/null; then echo supported; else echo unsupported; fi
    rm -f f1
    ;;
  printf_numbered)
    out=$(printf '%1$s\n' x 2>/dev/null)
    [ "$out" = x ] && echo supported || echo unsupported
    ;;
esac
""")
write("sweep.sh", r"""shell=$1
for p in dollar_single pipefail trap_p_exit read_d_delim test_nt_ot test_ef printf_numbered; do
  printf '  %-16s %s\n' "$p" "$($shell probe.sh $p)"
done
""")
run("""
# bash 5.3.20: which of the seven POSIX.1-2024 probes pass
echo "== bash 5.3.20 =="
bash sweep.sh bash
# the same bash in POSIX mode: it changes what differs from the standard and keeps its own features
echo "== bash --posix =="
bash sweep.sh "bash --posix"
# dash 0.5.13.5, a recent release of the shell that Ubuntu installs as sh
echo "== dash 0.5.13.5 =="
bash sweep.sh dash
""")

Output

# bash 5.3.20: which of the seven POSIX.1-2024 probes pass
$ echo "== bash 5.3.20 =="
== bash 5.3.20 ==
$ bash sweep.sh bash
  dollar_single    supported
  pipefail         supported
  trap_p_exit      supported
  read_d_delim     supported
  test_nt_ot       supported
  test_ef          supported
  printf_numbered  unsupported
# the same bash in POSIX mode: it changes what differs from the standard and keeps its own features
$ echo "== bash --posix =="
== bash --posix ==
$ bash sweep.sh "bash --posix"
  dollar_single    supported
  pipefail         supported
  trap_p_exit      supported
  read_d_delim     supported
  test_nt_ot       supported
  test_ef          supported
  printf_numbered  unsupported
# dash 0.5.13.5, a recent release of the shell that Ubuntu installs as sh
$ echo "== dash 0.5.13.5 =="
== dash 0.5.13.5 ==
$ bash sweep.sh dash
  dollar_single    supported
  pipefail         supported
  trap_p_exit      unsupported
  read_d_delim     unsupported
  test_nt_ot       supported
  test_ef          supported
  printf_numbered  unsupported

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 issue8_matrix_session.py

bash 5.3.20 passed all three shell-language probes in both modes. dash 0.5.13.5 passed $'...' and pipefail and failed trap -p EXIT: the probe set an EXIT trap, asked trap -p EXIT for it and got nothing back. Its manual gives trap an action and signals only, with no -p option. Job Control describes behaviour rather than adding a command, so no probe covers it.

bash --posix printed the same column as plain bash. The Bash manual says POSIX mode changes Bash only where its default differs from the standard, to make Bash a strict superset of it, so Bash keeps its own features in that mode. That is why a run under bash --posix cannot stand in for dash: trap -p and read -d pass there and fail in dash.

Guessing which column dash fills in

Wrong: reading Issue 8 as a list of what dash runs. Right: run the probe on the shell itself. The pipefail row printed supported on dash and the trap -p row did not, although both features arrived in the same edition.

Checkpoint

Check: Shell-language additions in Issue 8

  1. Question 1 of 2 The probe (set -o pipefail; false | true) ran in dash 0.5.13.5. What status did it return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 1, the status of false, the stage that failed

    The matrix session recorded "pipefail supported" under dash, and the guard session printed "pipeline status: 1" for false | true under both dash and bash once pipefail was on.

  2. Question 2 of 2 Which choice lists the four shell-language changes of POSIX.1-2024 that the lesson names?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: $'...' quoting, pipefail, trap -p and the Job Control section

    The lesson splits the Issue 8 changes into the shell language, which the shell reads itself, and the utilities, which are programs or built-ins with options of their own.

The utilities gained options, and new programs arrived

Issue 8 changed the utility pages as well. read gains -d delim, which ends the record at a single byte of your choice instead of the newline (Austin Group Defect 243). test gains -nt and -ot, which compare the modification times of two files, and -ef, which is true when two paths lead to the same file (Defect 375). printf gains numbered conversions such as %1$s and %2$s, which pick an argument by its position (Defect 1592). Among the programs new in Issue 8, three serve scripts directly: realpath turns a path into its canonical form, readlink prints the target of a symbolic link and timeout runs a command under a time limit. Issue 8 also made options standard that scripts had long used, among them find -print0, xargs -0 and sed -E.

Step through what read -d : changes for one short input:

Interactive · step by step What read -d delim changes Step through one read of the bytes a:b:c and see where the record ends with Issue 7's read and with Issue 8's.
What read -d delim changes, step 4 of 4. Showing Input: a:b:c, Issue 7: read, Issue 8: read -d :, first = a:b:c (the whole string), first = a (the first field). In focus: first = a.ends at newlineends at colontakes it allstops at :Input: a:b:cIssue 7: readIssue 8: read -d :first = a:b:cthe whole stringfirst = athe first field

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. The whole string reaches read. A pipeline sends the bytes a, colon, b, colon, c to read. There is no newline in the data, so where read stops depends on what counts as the end of a record.
  2. Issue 7 read stops at a newline. In Issue 7 the only record delimiter read knows is the newline. The input has none, so the first variable takes every byte, and the colons stay part of the value.
  3. Issue 8 adds -d delim. The Issue 8 read page adds -d delim: a delim of one single-byte character takes the place of the newline, and an empty delim means the null byte. With -d : the record ends at the first colon.
  4. first now holds one field. In the probe, printf a:b:c | { IFS= read -r -d : first; echo "$first"; } printed a under bash 5.3.20. dash 0.5.13.5 failed the same probe, and its manual gives read only -p and -r, so a script for that dash keeps an IFS=: read -r a b c line instead.

The probes split along the same lines. dash printed supported for both test rows, whose operators its manual already lists, and unsupported for read -d: its manual gives read only the -p and -r options. Both shells printed unsupported for numbered printf, because the printf builtins of bash and dash both refused the directive. The next session hands one numbered format to both builtins and to the external printf of macOS:

Numbered printf conversions on bash and macOS Python · printf_numbered_session.py
from session import run, write

# POSIX.1-2024 added numbered printf conversions (such as %2$s and %1$s) to the printf utility. On this Mac three
# printfs are compared: the Bash 5.3.20 builtin and the dash 0.5.13.5 builtin, which both reject the directive, and
# /usr/bin/printf, the BSD printf of macOS 26, which prints the arguments by position. The exit status is kept with the recorded output so the session
# shows where each printf stopped.
write("show.sh", r"""set +e
echo "== bash 5.3.20 builtin printf =="
printf '%2$s %1$s\n' A B
echo "[exit status $?]"
echo "== /usr/bin/printf on macOS =="
/usr/bin/printf '%2$s %1$s\n' A B
echo "[exit status $?]"
""")
# The same three lines for the dash builtin: dash names the script and the line in its message.
write("show-dash.sh", r"""set +e
echo "== dash 0.5.13.5 builtin printf =="
printf '%2$s %1$s\n' A B
echo "[exit status $?]"
""")
run("""
bash show.sh
dash show-dash.sh
""")

Output

$ bash show.sh
== bash 5.3.20 builtin printf ==
show.sh: line 3: printf: `$': invalid format character
[exit status 1]
== /usr/bin/printf on macOS ==
B A
[exit status 0]
$ dash show-dash.sh
== dash 0.5.13.5 builtin printf ==
show-dash.sh: 3: printf: %2$: invalid directive
[exit status 2]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 printf_numbered_session.py

The bash builtin printed invalid format character and ended with status 1, and the dash builtin printed invalid directive and ended with status 2. /usr/bin/printf on macOS printed B A with status 0, which is what the Issue 8 printf page asks for: a conversion that starts with %, a number and a $ takes the argument with that number.

Builtin versus utility

A shell’s builtin printf and /usr/bin/printf are separate programs that read the same format. For numbered conversions today, call the external printf by its path, as the session does, and try it on each platform you target.

Checkpoint

Check: Utility additions in Issue 8

  1. Question 1 of 2 A script runs printf a:b:c | { read -r -d : first; echo "$first"; }. What does bash 5.3.20 print?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: a

    The matrix session recorded "read_d_delim supported" under bash and "unsupported" under dash 0.5.13.5.

  2. Question 2 of 2 The printf numbered session ran printf '%2$s %1$s\n' A B under bash 5.3.20. What did the builtin print?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: An error, invalid format character, and then exit status 1

    Only /usr/bin/printf printed "B A" with status 0 in the recorded session; the bash builtin stopped at the numbered directive.

Four of the seven features already run in dash

The table sets what each edition requires beside what the recorded probes printed. Four features are new in Issue 8 and already run in dash 0.5.13.5, so a POSIX sh script may rely on them wherever that dash release or a newer one runs it; three are still missing there, and numbered printf is missing from the bash builtin as well. Answer the two questions under the table to see which rows your own script may use:

Interactive · answer and compare Which Issue 8 feature does which shell have? Read a row to compare what each edition requires with what the probes recorded, then answer the two questions about your script.
Does your script name Bash on its first line, or does it have to be plain POSIX sh?
Must the script also run under a /bin/sh older than dash 0.5.13.5, such as the dash of Ubuntu 24.04?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Seven POSIX.1-2024 additions compared across two editions and two shells
Feature POSIX 2017 POSIX 2024 bash 5.3.20 dash 0.5.13.5
$'...' quoting What it adds: escapes such as \t and \n inside $'...' Poor not defined by Issue 7 Good section 2.2.4 Dollar-Single-Quotes defines it Good ran in the probe and expanded the tab Good ran in the probe and expanded the tab
set -o pipefail What it adds: the rightmost failing stage sets the pipeline status Poor absent from the Issue 7 set builtin Good added by Austin Group Defect 789 Good false | true returned 1 in the probe Good false | true returned 1 in the probe
trap -p EXIT What it adds: prints the action of each named condition for reinput Poor trap had no -p option in Issue 7 Good added by Austin Group Defects 1211 and 1212 Good trap -p EXIT printed the action Poor trap -p EXIT printed nothing
read -d delim What it adds: read stops at delim instead of newline Poor read had only -r in Issue 7 Good added by Austin Group Defect 243 Good read -d : stopped at the colon Poor the probe failed: read -d is not in dash yet
test -nt and -ot What it adds: compare the modification times of two files Poor not a primary of the Issue 7 test utility Good added by Austin Group Defect 375 Good f1 -nt f2 and f2 -ot f1 were both true Good f1 -nt f2 and f2 -ot f1 were both true
test -ef What it adds: true when two paths name the same file Poor not a primary of the Issue 7 test utility Good added by Austin Group Defect 375 Good test f1 -ef f1 returned true Good test f1 -ef f1 returned true
%1$s and %2$s What it adds: printf picks the argument by position in the format Poor no numbered conversions in Issue 7 Good added by Austin Group Defect 1592 Poor builtin printed "invalid format character" Poor its builtin said "invalid directive"
How the answers decide
  • “Does your script name Bash on its first line, or does it have to be plain POSIX sh?” plain POSIX sh: rules out trap -p EXIT, read -d delim and %1$s and %2$s. dash 0.5.13.5 failed the trap -p, read -d and numbered printf probes, so a plain sh script cannot rely on them even on the newest dash it was tested with.
  • “Does your script name Bash on its first line, or does it have to be plain POSIX sh?” plain POSIX sh and “Must the script also run under a /bin/sh older than dash 0.5.13.5, such as the dash of Ubuntu 24.04?” Yes, older sh too: adds a caveat to $'...' quoting, set -o pipefail, test -nt and -ot and test -ef. The probes ran dash 0.5.13.5 only, and the manual of Ubuntu 24.04's dash 0.5.12 lists no pipefail option, so probe each feature on the oldest shell before the script relies on it.
  • “Does your script name Bash on its first line, or does it have to be plain POSIX sh?” Bash, named on the shebang: rules out %1$s and %2$s. The bash 5.3.20 printf builtin refused the numbered conversion with "invalid format character"; call the external printf utility instead, after trying it on each platform.

Every row reads poor under POSIX 2017 and good under POSIX 2024: each feature is new in Issue 8, so a shell built for the older edition may lack it and still conform to that edition. The bash and dash cells are what the probes recorded, not what the shells promise.

For a Debian package, the POSIX 2017 column is what the policy allows a /bin/sh script to assume, while the dash column is what dash 0.5.13.5 runs. ShellCheck lags in the same way: its SC3003 page says the rule was last applicable to POSIX.1-2017, and ShellCheck 0.11.0 still flagged $'...' in an sh script in the POSIX sh versus Bash session, although Issue 8 defines the form and dash 0.5.13.5 runs it.

Checkpoint

Check: What dash 0.5.13.5 runs of Issue 8

  1. Question 1 of 2 Which three of the seven probed features are still missing from dash 0.5.13.5?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: trap -p, read -d and numbered printf

    The matrix session recorded four "supported" rows in dash and three "unsupported" rows: trap_p_exit, read_d_delim and printf_numbered.

  2. Question 2 of 2 Why does the lesson say that a feature in Issue 8 is not yet a portable feature?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Policy and tools still point at the earlier edition, and installed shells lag behind it

    Debian policy, the ShellCheck SC3003 page and the recorded dash column all lag Issue 8, each in its own way.

A subshell probe tests an option before the script sets it

The pattern is short: the script asks the running shell whether it has a feature, and turns the feature on only when the answer is yes. Step through it for pipefail:

Interactive · stage by stage The probe-then-set pattern for an Issue 8 option Step through the four stages the guard script runs before it relies on an Issue 8 option.
The probe-then-set pattern for an Issue 8 option, step 4 of 4. Showing Probe in a subshell, Read its status, Enable the option, Run the rest (no pipefail, noted). In focus: to-rest, Run the rest.no: print a noteProbe in a subshell(set -o pipefail)Read its statusdid it exit 0?Enable the optionset -o pipefailRun the restno pipefail, noted

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. Probe in a subshell. The parentheses start a child shell. Setting pipefail there changes only the child, so a shell that does not know the option fails without touching the outer script, and 2>/dev/null hides the error message it would print.
  2. Read the probe status. The exit status of the subshell is the exit status of its last command. If the shell accepted set -o pipefail, the subshell ended with 0; a shell that rejected it ended non-zero and left the outer options as they were.
  3. Set it when the probe passed. When the status is 0 the main script runs set -o pipefail itself. From then on false | true returns 1, the status of its rightmost failing stage, and a script running under set -e stops there.
  4. Otherwise, print a note. When the probe failed, the script skips the set stage along the branch, prints a note and goes on without pipefail. A script that cannot run safely without the option stops here instead, with a message that names it.

The probe runs in a subshell because a rejected option can end a script. set is one of the special built-ins, and the standard’s table of shell errors says a non-interactive shell exits on an error in one of them, so a bare set -o pipefail on a shell without the option would stop the script on that line; the Bash manual lists the same rule for Bash in POSIX mode. Inside the parentheses only the child shell ends, and its exit status, the status of its last command, answers the question. In the recorded session both bash 5.3.20 and dash 0.5.13.5 took the pipefail enabled branch and printed pipeline status: 1 for the failing pipeline:

pipefail-guard.sh under bash and dash Python · pipefail_guard_session.py
from session import run, write

# A POSIX sh script that only switches on pipefail after a probe shell has shown that the current shell supports it.
# The probe runs in a subshell so a failure does not leak into the script's own options. The main script then prints
# the exit status of a failing pipeline, which is 1 only when pipefail took effect. Recorded under GNU Bash 5.3.20
# and dash 0.5.13.5: the probe passes on both, so both runs end with status 1.
write("guard.sh", r"""#!/bin/sh
# probe pipefail in a child shell; the current shell's options stay as they are
if (set -o pipefail) 2>/dev/null; then
  set -o pipefail
  echo "pipefail enabled"
else
  echo "pipefail is not supported here"
fi
# a pipeline whose first stage fails; without pipefail its status would be 0
set +e
false | true
echo "pipeline status: $?"
""")
run("""
chmod +x guard.sh
# POSIX sh is dash on Ubuntu; pipefail reached POSIX in 2024 and dash 0.5.13.5 supports it
dash guard.sh
# the same script under bash 5.3.20
bash guard.sh
# a shell that lacks pipefail would take the else branch; the pipeline status would then be 0
bash -c 'if (set -o pipefail) 2>/dev/null; then echo supports; else echo missing; fi'
""")

Output

$ chmod +x guard.sh
# POSIX sh is dash on Ubuntu; pipefail reached POSIX in 2024 and dash 0.5.13.5 supports it
$ dash guard.sh
pipefail enabled
pipeline status: 1
# the same script under bash 5.3.20
$ bash guard.sh
pipefail enabled
pipeline status: 1
# a shell that lacks pipefail would take the else branch; the pipeline status would then be 0
$ bash -c 'if (set -o pipefail) 2>/dev/null; then echo supports; else echo missing; fi'
supports

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pipefail_guard_session.py

The same shape works for every Issue 8 feature a script wants: (trap -p EXIT) >/dev/null 2>&1, a one-shot printf '%1$s\n' x whose output is compared with x, or a read -r -d : that reads a:b and checks that it got a. When the script needs the feature, the else branch stops with a message that names it; when the feature is only nice to have, the else branch sets a flag that the rest of the script reads before it uses the feature.

The tools lag as well. ShellCheck 0.11.0 still reports $'...' in sh scripts as SC3003. Keep that warning while the script must run on older shells, and silence it with a # shellcheck disable=SC3003 line above the one line that uses the feature once every shell you target has it. The Debian Policy Manual also names checkbashisms, from the devscripts package, as a second checker for /bin/sh scripts.

One feature, one guard

dash 0.5.13.5 already runs four of the seven probed features and still lacks three, so give each feature its own guard next to the lines that use it. The script then gains features one at a time as the shells you target catch up.

Checkpoint

Check: Probing before using an Issue 8 feature

  1. Question 1 of 2 Why does the probe start with (set -o pipefail) in parentheses rather than set -o pipefail directly?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The parentheses start a subshell, so a shell that rejects the option does not change or end the outer script

    The pipefail guard session printed "pipefail enabled" and "pipeline status: 1" under both bash and dash: the probe passed, and the outer script then set pipefail itself.

  2. Question 2 of 2 ShellCheck 0.11.0 still warns SC3003 on tab=$'\t' in a /bin/sh script, although Issue 8 defines the quoting form. What does the lesson say to do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Keep the warning while old shells matter; silence that one line with # shellcheck disable=SC3003 once every target shell has the feature

    The lesson cites the ShellCheck SC3003 page, which says POSIX.1-2024 introduced dollar-single-quotes and that the rule was last applicable to POSIX.1-2017.

Practise: Issue 8, probes and portability

These questions mix the editions with the recorded probe results, and every wrong choice says what it misses. Answer from the sessions on this page rather than from memory:

Practice that adapts to you

Practise this lesson

17 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 17 Which pair names the same edition of the standard?

    Choose one answer.

    Show the answer: Question 1 of 17

    Answer: POSIX.1-2024 and Issue 8

    Each issue of The Open Group Base Specifications is also an IEEE Std 1003.1 edition: Issue 7 ended as IEEE Std 1003.1-2017 and Issue 8 is IEEE Std 1003.1-2024.

  2. Question 2 of 17 An old /bin/sh script tests [ -f a -a -f b ]. What did Issue 8 change for that line?

    Choose one answer.

    Show the answer: Question 2 of 17

    Answer: Issue 8 removed the -a operator of test, so the line now relies on the shell rather than on the standard

    The change history of the Issue 8 test page lists Defect 1330. Written as [ -f a ] && [ -f b ], the same check uses only what every edition defines.

  3. Question 3 of 17 POSIX.1-2024 added a section that collects the rules of job control. Which number does it carry in the Shell Command Language chapter?

    Choose one answer.

    Show the answer: Question 3 of 17

    Answer: 2.11, Job Control

    The Issue 8 chapter numbers Job Control 2.11, which moved Signals and Error Handling to 2.12 and the sections after it one place down.

  4. Question 4 of 17 Which section number of POSIX.1-2024 defines the $'...' quoting form?

    Choose one answer.

    Show the answer: Question 4 of 17

    Answer: 2.2.4 Dollar-Single-Quotes

    Section 2.2 Quoting of Issue 8 has four parts: backslash, single-quotes, double-quotes and, new, 2.2.4 Dollar-Single-Quotes.

  5. Question 5 of 17 With set -o pipefail on, what status does (exit 2) | (exit 1) | true return?

    Choose one answer.

    Show the answer: Question 5 of 17

    Answer: 1, the status of the rightmost stage that failed

    Section 2.9.2 of Issue 8 gives a pipeline under pipefail the status of its last command that returned non-zero, or zero when every command succeeded.

  6. Question 6 of 17 A script uses trap -p EXIT and passes every test under bash --posix. Will it run under dash 0.5.13.5?

    Choose one answer.

    Show the answer: Question 6 of 17

    Answer: No: trap -p passed under bash --posix and failed under dash in the probes, because POSIX mode keeps Bash features

    The bash --posix column matched plain bash on every row, while dash printed unsupported for trap_p_exit, so only a run under dash itself shows what dash does.

  7. Question 7 of 17 Which standard page defines read -d delim?

    Choose one answer.

    Show the answer: Question 7 of 17

    Answer: The read utility page of POSIX.1-2024

    The read page of POSIX.1-2024, which the lesson cites, adds -d delim through Austin Group Defect 243.

  8. Question 8 of 17 When does test f1 -ef f2 return 0?

    Choose one answer.

    Show the answer: Question 8 of 17

    Answer: When f1 and f2 lead to the same file, for example a file and a hard link to it

    The matrix probe ran test f1 -ef f1, the same path twice, and both shells printed "supported".

  9. Question 9 of 17 How many of the three printfs in the numbered printf session ended with status 0?

    Type a number.

    Show the answer: Question 9 of 17

    Answer: 1 printfs

    The bash builtin ended with status 1 ("invalid format character") and the dash builtin with status 2 ("invalid directive"); only /usr/bin/printf printed "B A" with status 0.

  10. Question 10 of 17 A script needs numbered printf today on bash 5.3.20. What does the lesson suggest?

    Choose one answer.

    Show the answer: Question 10 of 17

    Answer: Call the external printf utility by its path (/usr/bin/printf on the Mac) instead of the builtin

    The numbered printf session shows /usr/bin/printf accepting the directive; try the external printf on each platform the script targets.

  11. Question 11 of 17 How many of the seven probed features printed "supported" in dash 0.5.13.5?

    Type a number.

    Show the answer: Question 11 of 17

    Answer: 4 features

    dollar_single, pipefail, test_nt_ot and test_ef each printed "supported" in the dash column.

  12. Question 12 of 17 Which group of features already runs in dash 0.5.13.5, as the matrix recorded it?

    Choose one answer.

    Show the answer: Question 12 of 17

    Answer: $'...', pipefail, test -nt with -ot, and test -ef

    The dash column of the matrix session holds "supported" on four rows and "unsupported" on three.

  13. Question 13 of 17 The probe trap "echo bye" EXIT; trap -p EXIT got no "bye" back in dash. Why does the matrix call that unsupported?

    Choose one answer.

    Show the answer: Question 13 of 17

    Answer: The trap was set, but dash has no -p option to print it back

    The matrix session recorded "trap_p_exit unsupported" under dash, while both bash columns printed "supported".

  14. Question 14 of 17 The probe for pipefail is (set -o pipefail) 2>/dev/null. Why the parentheses and the 2>/dev/null together?

    Choose one answer.

    Show the answer: Question 14 of 17

    Answer: The parentheses keep the attempt in a subshell, and 2>/dev/null hides the error line of a shell without the option

    The pipefail guard session shows the probe passing under both bash and dash, and the outer script then enabling the option itself.

  15. Question 15 of 17 The probe fails on an older /bin/sh. Which two shapes of the else branch does the lesson name?

    Choose one answer.

    Show the answer: Question 15 of 17

    Answer: Stop with a message that names the feature, or set a flag the rest of the script reads

    The probe section names "stops with a message that names it" for a feature the script needs and "sets a flag" for a feature that is only nice to have.

  16. Question 16 of 17 A /bin/sh script uses $'\t'. ShellCheck 0.11.0 flags it as SC3003. What does the ShellCheck page say about the rule?

    Choose one answer.

    Show the answer: Question 16 of 17

    Answer: It was last applicable to POSIX.1-2017, which did not define the form

    The lesson cites the SC3003 page, and the posix-sh-versus-bash session recorded ShellCheck 0.11.0 still reporting the rule.

  17. Question 17 of 17 Which line probes whether the current shell supports pipefail without changing the outer script?

    Choose one answer.

    Show the answer: Question 17 of 17

    Answer: (set -o pipefail) 2>/dev/null

    The pipefail guard session uses exactly (set -o pipefail) 2>/dev/null as its probe.

Interview questions

Warm-up (fresher to 2–5 years): name four shell-language changes that POSIX.1-2024 added, and one utility change.

POSIX.1-2024, Issue 8 of The Open Group Base Specifications, made four shell-language changes that scripts meet first: $'...' quoting with backslash escapes (section 2.2.4), the pipefail option of set (Austin Group Defect 789), which gives a pipeline the status of its rightmost failing stage, a Job Control section (2.11), and trap -p, which prints the actions of the conditions it names in a form the shell can read back. One utility change is read -d delim, which ends a record at a chosen single byte instead of a newline. In the recorded probes dash 0.5.13.5 ran $'...' and pipefail, while trap -p and read -d still printed unsupported there, so a /bin/sh script needs a probe before it uses either.

Two harder questions start where this lesson stops, on images whose dash lacks a feature the team wanted. Sketch the commands you would try on such an image before you open each question’s first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to staff

    A /bin/sh backup job runs dump_orders | gzip > orders.gz. The oldest image's dash has no pipefail, so the guard leaves it off there. When the dump fails half-way, the job still ends with status 0 and a valid-looking archive. How do you catch the failed first stage without pipefail, and keep only complete archives?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to staff

    A shared POSIX sh library function sets INT and EXIT traps while it holds a lock, and must give the caller's traps back when it returns. The oldest images run dash 0.5.13.5 as /bin/sh, which has no trap -p. How do you save and restore the caller's traps there, and show that the restore is exact?

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What is POSIX.1-2024, and how does it relate to the previous edition?

    Issue 8 of The Open Group Base Specifications. It mostly adds to Issue 7, POSIX.1-2017, and its few removals, such as test's -a and -o, had been marked obsolescent first.

  2. Which four shell-language changes of POSIX.1-2024 do scripts meet first?

    $'...' quoting with backslash escapes, the pipefail option of set, a Job Control section, and trap -p, which prints the current trap actions.

  3. Which utility changes of POSIX.1-2024 reach shell scripts?

    read -d, the test operators -nt, -ot and -ef, numbered printf conversions such as %1$s, and new utilities, among them realpath, readlink and timeout.

  4. How many of the seven probed Issue 8 features did bash 5.3.20 and dash 0.5.13.5 run?

    bash ran six and refused %1$s in its printf builtin; dash ran four and refused trap -p, read -d and %1$s.

  5. Does a feature's place in POSIX.1-2024 make it safe in a #!/bin/sh script?

    Not yet: Debian policy still lets sh scripts assume only POSIX.1-2017 plus named extras, and ShellCheck still flags $'...' in sh, so check that the running shell has the feature.

  6. How does a script test for an option such as pipefail before it relies on it?

    Probe it in a subshell, as in (set -o pipefail) 2>/dev/null, turn it on only when the probe passes, and give the other branch a clear message or a flag.

Key takeaways

  • POSIX.1-2024 is Issue 8 of The Open Group Base Specifications; it mostly adds to Issue 7 (POSIX.1-2017), and its few removals, such as the -a and -o operators of test, had been marked obsolescent first.
  • The four shell-language changes scripts meet first are $'...' quoting, set -o pipefail, the Job Control section (2.11) and trap -p, and the first two already run in both bash 5.3.20 and dash 0.5.13.5.
  • The utilities gained read -d, the test operators -nt, -ot and -ef and numbered printf conversions, and new programs such as realpath, readlink and timeout arrived.
  • Of the seven probed features, bash 5.3.20 ran six and refused %1$s in its printf builtin, while dash 0.5.13.5 ran four and refused trap -p, read -d and %1$s.
  • Debian policy still lets /bin/sh scripts assume only POSIX.1-2017 plus named extras, and ShellCheck still flags $'...' in sh, so a feature’s place in Issue 8 does not make it portable yet.
  • Probe each feature in a subshell, such as (set -o pipefail) 2>/dev/null, turn it on only when the probe passes, and give the else branch either a clear message or a flag.

Terms in this lesson

Issue 7
The seventh edition of The Open Group Base Specifications, aligned with IEEE Std 1003.1-2008 and later corrigenda (POSIX.1-2017). Debian policy still names this edition as the baseline for /bin/sh scripts.
Issue 8
The eighth edition of The Open Group Base Specifications, published jointly as IEEE Std 1003.1-2024 (POSIX.1-2024). It replaces Issue 7 and adds dollar-single-quotes, pipefail, trap -p, a Job Control section (2.11) and new behaviour to read, test and printf.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.