Print a line
printf is a shell builtin. %s inserts a string; \n ends the line. Keep the format string constant.
Back to example galleryCode
printf '%s\n' 'Hello, shell!'
Actual output
bash · exit 0
Hello, shell!
zsh · exit 0
Hello, shell!
A visual reference · Bash
From a first command to small, reliable shell programs. Complete source. A small change. Its actual result.
The Org file is the canonical source. Read the HTML guide beside your work, or download its PDF and source for offline reading.
Last changes: 2026-10-19. Added a searchable HTML gallery with code-and-output previews, labelled Zsh comparisons and links to complete examples and back. Every tour now links to tours.emaix.org. Unified tour–chapter–example IDs, numbered chapter anchors and updated direct links, downloads and comparisons. Added nine worked examples for here-strings, variable-name expansion and ERR traps, with an expanded quick reference and a Devhints cheatsheet link. Reader pages now omit publishing instructions and highlight PDF downloads.
Browse by code and result
152 examples · 21 topics
Choose a preview to open the complete program and captured result. Search examples or select a topic to narrow the gallery. An ellipsis marks shortened code or output.
printf '%s\n' 'Hello, shell!'Hello, shell!
printf '%s\n' 'alpha' 'beta'alpha beta
printf '<%s>\n' 'alpha beta' gamma<alpha beta> <gamma>
printf '%-8s %04d\n' apples 7 pears 12apples 0007 pears 0012
printf 'mean = %.2f\n' 3.14159mean = 3.14
# Two physical lines, one command
printf '%s\n' \
'continued command'continued command
label='sample A'
printf '<%s>\n' "$label"<sample A>
label='sample A'
printf '%s\n' '$label' "$label"$label sample A
label='sample A'
printf '<%s>\n' $label<sample> <A>
label='sample A'
printf '<%s>\n' $label<sample A>
label='sample A'
printf '<%s>\n' ${=label}<sample> <A>
label=sample
printf '%s\n' "${label}_final"sample_final
unset label
printf '%s\n' "${label:-untitled}"untitled
label=''
printf 'colon=<%s> plain=<%s>\n' "${label:-fallback}" "${label-fallback}"colon=<fallback> plain=<>
unset label
printf '%s\n' "${label:=untitled}" "$label"untitled untitled
file='sample.raw.csv'
printf '%s\n' "${file%.csv}"sample.raw
file='sample.raw.csv'
printf '%s\n' "${file%.*}" "${file%%.*}"sample.raw sample
file='data/raw/sample.csv'
printf '%s\n' "${file##*/}"sample.csv
label='a-b-c'
printf '%s\n' "${label/-/_}" "${label//-/_}"a_b-c a_b_c
n=7
printf '%s\n' "$((n + 2))" "$((n / 2))" "$((n % 2))"9 3 1
mkdir data
printf '%s\n' alpha > data/input.txt
cp data/input.txt data/copy.txt
cat data/copy.txtalpha
touch a.txt b.txt c.csv
printf '%s\n' *.txta.txt b.txt
touch a.txt b.txt c.csv
printf '%s\n' '*.txt'*.txt
touch a1.txt a2.txt b1.txt aa1.txt
printf '%s\n' [ab]?.txta1.txt a2.txt b1.txt
printf '%s\n' sample_{A,B}.csvsample_A.csv sample_B.csv
printf '<%s>\n' missing*.csv<missing*.csv>
printf '<%s>\n' missing*.csv(no stdout)Standard error
zsh:1: no matches found: missing*.csv
shopt -s nullglob
files=(missing*.csv)
printf 'files=%s\n' "${#files[@]}"files=0
files=(missing*.csv(N))
printf 'files=%s\n' "${#files[@]}"files=0
printf '%s\n' alpha > notes.txt
cat notes.txtalpha
printf '%s\n' alpha > notes.txt
printf '%s\n' beta >> notes.txt
cat notes.txtalpha beta
printf '%s\n' alpha > notes.txt
tr 'a-z' 'A-Z' < notes.txtALPHA
printf '%s\n' alpha beta | tr 'a-z' 'A-Z'ALPHA BETA
printf '%s\n' pear apple pear | sort | uniq -c1 apple 2 pear
printf '%s\n' result
printf '%s\n' diagnostic >&2resultStandard error
diagnostic
{ printf 'result\n'; printf 'diagnostic\n' >&2; } > log.txt 2>&1
cat log.txtresult diagnostic
{ printf 'result\n'; printf 'diagnostic\n' >&2; } 2>&1 > log.txt
printf 'FILE:\n'
cat log.txtdiagnostic FILE: result
label=sample
cat <<'TEXT'
$label is literal here.
TEXT$label is literal here.
label=sample
cat <<TEXT
$label is expanded here.
TEXTsample is expanded here.
printf 'alpha\n' | tee copy.txt | tr 'a-z' 'A-Z'
cat copy.txtALPHA alpha
text='alpha beta'
tr '[:lower:]' '[:upper:]' <<< "$text"ALPHA BETA
text=''
IFS= read -r line <<< "$text"
printf 'here-string: status=%s length=%s\n' "$?" "${#line}"
printf '%s' "$text" | {
…here-string: status=0 length=0 printf: status=1 length=0
false
printf 'status=%s\n' "$?"status=1
true && printf 'runs\n'
false && printf 'skipped\n'
printf 'done\n'runs done
false || printf 'fallback\n'fallback
n=7
if [[ "$n" -gt 5 ]]; then
printf 'large\n'
else
…large
file='sample.csv'
if [[ "$file" == *.csv ]]; then
printf 'CSV input\n'
fiCSV input
file='sample.tsv'
case "$file" in
*.csv) printf 'comma\n' ;;
*.tsv) printf 'tab\n' ;;
…tab
false | cat
printf 'pipeline=%s\n' "$?"pipeline=0
set -o pipefail
false | cat
printf 'pipeline=%s\n' "$?"pipeline=1
for item in alpha 'beta gamma'; do
printf '<%s>\n' "$item"
done<alpha> <beta gamma>
printf '%s\n' ' alpha' 'a\b' > lines.txt
while IFS= read -r line; do
printf '<%s>\n' "$line"
done < lines.txt< alpha> <a\b>
n=0
while [[ "$n" -lt 3 ]]; do
printf '%s\n' "$n"
n=$((n + 1))
…0 1 2
set -- alpha 'beta gamma'
printf 'count=%s\n' "$#"
printf '<%s>\n' "$@"count=2 <alpha> <beta gamma>
set -- alpha 'beta gamma'
printf '<%s>\n' "$*"<alpha beta gamma>
greet() {
local person="$1"
printf 'Hello, %s!\n' "$person"
}
…Hello, Ada Lovelace!
greet() { printf 'Hello, %s!\n' "$1"; }
message=$(greet Ada)
printf '<%s>\n' "$message"<Hello, Ada!>
is_csv() { [[ "$1" == *.csv ]]; }
if is_csv sample.csv; then printf 'accepted\n'; fiaccepted
items=(alpha 'beta gamma')
printf '%s\n' "${items[0]}" "${items[1]}"alpha beta gamma
items=(alpha 'beta gamma')
printf '%s\n' "${items[1]}" "${items[2]}"alpha beta gamma
items=(alpha 'beta gamma')
printf 'count=%s\n' "${#items[@]}"
printf '<%s>\n' "${items[@]}"count=2 <alpha> <beta gamma>
items=(alpha)
items+=('beta gamma')
printf '<%s>\n' "${items[@]}"<alpha> <beta gamma>
declare -A units
units[mass]=kg
units[time]=s
printf '%s\n' "${units[mass]}"kg
typeset -A units
units[mass]=kg
units[time]=s
printf '%s\n' "${units[mass]}"kg
text='alpha,beta,gamma'
items=("${(@s:,:)text}")
printf '%s\n' "${(j: / :)items}"alpha / beta / gamma
file='data/sample.csv'
printf '%s\n' "${file:t}" "${file:h}" "${file:r}"sample.csv data data/sample
n=1
(n=2; printf 'inside=%s\n' "$n")
printf 'outside=%s\n' "$n"inside=2 outside=1
n=1
{ n=2; printf 'inside=%s\n' "$n"; }
printf 'outside=%s\n' "$n"inside=2 outside=2
export TOUR_LABEL=sample
bash --noprofile --norc -c 'printf "%s\n" "$TOUR_LABEL"'sample
(exit 7) &
job_pid=$!
wait "$job_pid"
printf 'job status=%s\n' "$?"job status=7
diff <(printf 'b\na\n' | sort) <(printf 'a\nb\n')
printf 'diff status=%s\n' "$?"diff status=0
set -u
unset missing
printf '%s\n' "$missing"(no stdout)Standard error
bash: line 3: missing: unbound variable
set -u
unset missing
printf '%s\n' "${missing:-fallback}"fallback
printf 'temporary\n' > scratch.txt
trap 'rm -f scratch.txt; printf "cleaned up\n"' EXIT
cat scratch.txttemporary cleaned up
cat > measurements.tsv <<'DATA'
sample value
A 3
B 7
…sample value A 3 B 7 C 5
printf 'sample\tvalue\nA\t3\n' > measurements.tsv
if [[ ! -r measurements.tsv || ! -s measurements.tsv ]]; then
printf 'missing input\n' >&2
exit 1
…input accepted
printf 'sample\tvalue\nA\t3\nB\t7\nC\t5\n' > measurements.tsv
awk 'NR>1 {sum+=$2; n++}
END {printf "samples=%d\nmean=%.2f\n", n, sum/n}' \
measurements.tsv > report.txt
…samples=3 mean=5.00
touch 'sample A.txt' 'sample B.txt'
files=(*.txt)
for file in "${files[@]}"; do
printf '<%s>\n' "$file"
…<sample A.txt> <sample B.txt>
printf 'contents\n' > ./-n
cat ./-ncontents
printf '%s\0' 'sample A.txt' 'sample B.txt' > names.bin
while IFS= read -r -d '' file; do
printf '<%s>\n' "$file"
done < names.bin<sample A.txt> <sample B.txt>
printf 'alpha\nbeta' > lines.txt
while IFS= read -r line || [[ -n "$line" ]]; do
printf '<%s>\n' "$line"
done < lines.txt<alpha> <beta>
printf 'sample:7:kg\n' > data.txt
while IFS=: read -r name rest; do
printf 'name=%s rest=%s\n' "$name" "$rest"
done < data.txtname=sample rest=7:kg
for file in a.csv notes.txt b.csv; do
[[ "$file" == *.csv ]] || continue
printf 'process %s\n' "$file"
doneprocess a.csv process b.csv
set -- alpha beta gamma
printf 'before=%s\n' "$1"
shift
printf 'after=%s count=%s\n' "$1" "$#"before=alpha after=beta count=2
set -- -v
OPTIND=1
verbose=no
while getopts ':v' opt; do
…verbose=yes
set -- -n 'sample A'
OPTIND=1
while getopts ':n:' opt; do
case "$opt" in n) printf 'name=%s\n' "$OPTARG" ;; esac
…name=sample A
set -- -n demo -- 'sample A.csv' b.csv
OPTIND=1
while getopts ':n:' opt; do
case "$opt" in n) printf 'name=%s\n' "$OPTARG" ;; esac
…name=demo file=<sample A.csv> file=<b.csv>
set -- -n
OPTIND=1
while getopts ':n:' opt; do
case "$opt" in
…(no stdout)Standard error
missing value: -n
set -- -x
OPTIND=1
while getopts ':n:' opt; do
case "$opt" in
…(no stdout)Standard error
unknown option: -x
value=before
printf 'after\n' | read -r value
printf '%s\n' "$value"before
value=before
printf 'after\n' | read -r value
printf '%s\n' "$value"after
value=before
read -r value < <(printf 'after\n')
printf '%s\n' "$value"after
false | true | false
codes=("${PIPESTATUS[@]}")
printf '%s\n' "${codes[@]}"1 0 1
false | true | false
codes=("${pipestatus[@]}")
printf '%s\n' "${codes[@]}"1 0 1
value=$(printf 'alpha\n\n')
printf '<%s> length=%s\n' "$value" "${#value}"<alpha> length=5
touch visible.txt .hidden.txt
shopt -s dotglob nullglob
files=(*.txt)
printf '%s\n' "${files[@]}".hidden.txt visible.txt
touch visible.txt .hidden.txt
files=(*.txt(DN))
printf '%s\n' "${files[@]}".hidden.txt visible.txt
mkdir data
touch a.txt
printf 'file=%s\n' *(N.)
printf 'dir=%s\n' *(N/)file=a.txt dir=data
mkdir -p data/raw
touch top.csv data/raw/sample.csv
printf '%s\n' **/*.csv(N)data/raw/sample.csv top.csv
mkdir -p data/raw
touch top.csv data/raw/sample.csv
shopt -s globstar nullglob
printf '%s\n' **/*.csvdata/raw/sample.csv top.csv
printf 'alpha\n\nbeta gamma\n' > lines.txt
mapfile -t items < lines.txt
printf 'count=%s\n' "${#items[@]}"
printf '<%s>\n' "${items[@]}"count=3 <alpha> <> <beta gamma>
label='Sample A'
printf '%s\n' "${(U)label}" "$label"SAMPLE A Sample A
label='Sample A'
printf '%s\n' "${label^^}" "$label"SAMPLE A Sample A
PS4='+ trace: '
set -x
label=sample
printf '%s\n' "$label"
…sampleStandard error
+ trace: label=sample + trace: printf '%s\n' sample …
PS4='+ trace: '
set -x
label=sample
printf '%s\n' "$label"
…sampleStandard error
+ trace: label=sample + trace: printf '%s\n' sample …
set -e
printf 'before\n'
false
printf 'unreachable\n'before
set -e
if false; then
printf 'unreachable\n'
else
…handled continues
false
exit_code=$?
printf 'command failed\n' >&2
printf 'saved=%s now=%s\n' "$exit_code" "$?"saved=1 now=0Standard error
command failed
if ! false; then
printf 'negated status=%s\n' "$?"
finegated status=0
exec 3> log.txt
printf 'log entry\n' >&3
printf 'terminal entry\n'
exec 3>&-
…terminal entry log entry
cleanup() {
local exit_code=$?
printf 'cleanup after status=%s\n' "$exit_code"
return "$exit_code"
…cleanup after status=3
trap 'printf "old handler\n"' EXIT
trap - EXIT
printf 'done\n'done
cmd=(printf '<%s>\n' 'sample A' 'sample B')
"${cmd[@]}"<sample A> <sample B>
cat > helpers.sh <<'LIB'
greet() { printf 'Hello, %s!\n' "$1"; }
LIB
. ./helpers.sh
…Hello, Ada!
collect() {
emulate -L zsh
setopt null_glob
local files=(missing*.csv)
…files=0 option restored
trap 'status=$?; printf "ERR: status=%s\n" "$status"' ERR
false
printf 'script continues\n'ERR: status=1 script continues
trap 'status=$?; printf "ERR: status=%s\n" "$status"' ERR
if false; then
printf 'unexpected success\n'
else
…failure handled
set +E
trap 'status=$?; printf "ERR: status=%s\n" "$status"' ERR
run_step() {
false
…function continues caller continues
set -E
trap 'status=$?; printf "ERR: status=%s\n" "$status"' ERR
run_step() {
false
…ERR: status=1 function continues caller continues
set -e
trap 'status=$?; printf "ERR: status=%s\n" "$status" >&2' ERR
printf 'before failure\n'
false
…before failureStandard error
ERR: status=1
text='sample-2026.csv'
printf '%s\n' "${text:7:4}"2026
text='sample-2026.csv'
printf '%s\n' "${text: -4}".csv
printf -v sample 'sample-%03d' 7
printf '%s\n' "$sample"sample-007
file='sample A.csv'
printf '%q\n' "$file"sample\ A.csv
value='123'
pattern='^[0-9]+$'
if [[ "$value" =~ $pattern ]]; then
printf 'integer text\n'
…integer text
file='sample-042.csv'
pattern='^([a-z]+)-([0-9]+)\.csv$'
if [[ "$file" =~ $pattern ]]; then
printf 'label=%s number=%s\n' \
…label=sample number=042
value='123'
pattern='^[0-9]+$'
if [[ "$value" =~ "$pattern" ]]; then
printf 'literal match\n'
…no literal match
(( 2 + 3 ))
printf 'nonzero status=%s\n' "$?"
(( 2 - 2 ))
printf 'zero status=%s\n' "$?"nonzero status=0 zero status=1
n=0
((n++))
printf 'post: n=%s status=%s\n' "$n" "$?"
n=0
…post: n=1 status=1 pre: n=1 status=0
value=008
printf '%s\n' "$((10#$value + 1))"9
for ((i=0; i<3; i++)); do
printf '%s\n' "$i"
done0 1 2
for value in 3 7 9; do
if ((value > 5)); then
printf 'first=%s\n' "$value"
break
…first=7
awk 'BEGIN {printf "%.2f\n", 7 / 2}'3.50
items=()
items[2]=alpha
items[5]=beta
printf 'count=%s\n' "${#items[@]}"
…count=2 index=2 index=5
items=(alpha beta gamma)
unset 'items[1]'
printf 'index=%s\n' "${!items[@]}"
printf 'value=%s\n' "${items[@]}"index=0 index=2 value=alpha value=gamma
items=(alpha 'beta gamma' delta epsilon)
printf '<%s>\n' "${items[@]:1:2}"<beta gamma> <delta>
label=sample
name=label
printf '%s\n' "${!name}"sample
label=before
declare -n ref=label
ref=after
printf '%s\n' "$label"after
make_items() {
local -n output_ref="$1"
output_ref=(alpha 'beta gamma')
}
…<alpha> <beta gamma>
tour_demo_alpha=12
tour_demo_beta=7
printf '%s\n' "${!tour_demo_@}" | sorttour_demo_alpha tour_demo_beta
tour_demo_alpha=12
tour_demo_beta=7
for variable in "${!tour_demo_@}"; do
printf '%s=%s\n' "$variable" "${!variable}"
…tour_demo_alpha=12 tour_demo_beta=7
printf 'original\n' > report.txt
set -C
if { printf 'replacement\n' > report.txt; } 2>/dev/null; then
printf 'written\n'
…overwrite refused original
printf 'original\n' > report.txt
set -C
printf 'replacement\n' >| report.txt
cat report.txtreplacement
shopt -u nullglob
(
shopt -s nullglob
files=(missing*.csv)
…inside=0 parent unchanged
shopt -s nocasematch
file='SAMPLE.CSV'
if [[ "$file" == *.csv ]]; then printf 'matched\n'; fimatched
set +m
shopt -s lastpipe
value=before
printf 'after\n' | read -r value
…after
touch a.txt b.txt
set -f
printf '%s\n' *.txt
set +f
…*.txt a.txt b.txt
if command -v awk >/dev/null 2>&1; then
printf 'awk available\n'
else
printf 'awk missing\n' >&2
…awk available
require_one() {
if (( $# != 1 )); then
printf 'expected one argument\n' >&2
return 2
…input=sample A validation status=2Standard error
expected one argument
produce() {
printf 'processing sample\n' >&2
printf '7\n'
}
…DATA: 7 LOG: processing sample
run() {
if [[ "$dry_run" == yes ]]; then
printf 'would run:'
printf ' %q' "$@"
…would run: printf %s\\n sample\ A
stem() { printf '%s\n' "${1%.csv}"; }
actual=$(stem sample.csv)
if [[ "$actual" != sample ]]; then
printf 'FAIL: stem\n' >&2
…PASS: stem
Reading the tour
Program
Read a complete script with its own input and an explicit interpreter.
Captured result
Compare standard output, standard error and exit status with the code.
One change
Follow stable identifiers and focused explanations without an accumulating shell session.
Bash is the main interpreter. Amber Zsh difference notes are optional comparisons. They are explicitly labelled; a Bash block requires Bash even when your interactive terminal normally starts Zsh.
Read from the beginning or jump to a stable example ID. Each example includes its complete input and captured result. Copy link preserves the address of the guide you are reading, including its current domain and path.
Use these reminders to find a complete program and its captured result. Each example link opens the corresponding section of this tour, including offline. For another compact reference, see the Devhints Bash scripting cheatsheet.
| Topic | Reminder | Worked example |
|---|---|---|
| Quoting | Keep a scalar together with "$value". |
BSH-02-003 |
| Defaults | Use ${value:-fallback} for an unset or empty value. |
BSH-03-001 |
| Path suffixes | Remove a matching suffix with ${file%.*}. |
BSH-03-004 |
| Substrings | Extract a slice with ${text:offset:length}. |
BSH-17-001 |
| Script arguments | Preserve argument boundaries with "$@". |
BSH-07-004 |
| Arrays | Keep elements separate with "${items[@]}". |
BSH-08-003 |
| Reading lines | Use IFS= read -r line to preserve spaces and backslashes. |
BSH-07-002 |
| Pipeline failures | Enable set -o pipefail to expose an earlier failure. |
BSH-06-008 |
| Function status | Test success explicitly with if task; then. |
BSH-21-005 |
| Process substitution | Read command output through <(command). |
BSH-09-005 |
| Regular expressions | Read captures from BASH_REMATCH after a successful match. |
BSH-17-006 |
| Short options | Parse flags and their values with getopts. |
BSH-12-002 |
| Here-strings | Feed a string to standard input with <<< "$text". |
BSH-05-012 |
| Variable names | Discover names with "${!tour_demo_@}". |
BSH-19-007 |
| Error traps | Report an unchecked failure with trap 'handler' ERR. |
BSH-16-007 |
The reminders describe Bash. Follow the labelled Zsh comparisons in the worked examples when using Zsh; array indexing and expansion rules can differ.
This is a Bash reference, not an exercise book. The main examples use Bash; amber Zsh difference notes are optional comparisons, not required steps. Read each small program together with its actual output. Stable BSH-CC-NNN identifiers survive changes to titles and page order. Each example includes its own input data and runs independently.
The Org source contains each complete program. Read its captured stdout, stderr and exit status beside the code. Bash is the main interpreter; explicitly marked Zsh difference notes show alternative syntax or behavior.
Execution profile: noninteractive Bash --noprofile --norc, or native Zsh -f; LC_ALL=C; fresh temporary HOME. Zsh may still read a system zshenv. Interactive configuration and plugins are outside the test profile. The examples use shell builtins and common Unix utilities. External tool behavior can differ on macOS/BSD; tested versions are recorded in the build report. A Bash block always requires Bash even if your terminal normally starts Zsh.
The build isolates working files but is not a security sandbox. Review code before executing it. In Emacs, load ob-shell (M-x load-library RET ob-shell), then use C-c C-c on a bash or zsh block. Run file-writing blocks in a disposable directory: unlike the supplied builder, plain Babel execution does not create an isolated directory automatically. :results output captures stdout; the custom builder additionally records stderr and exit status.
These actions require a real terminal and are not part of the executable tests. Tab requests completion; Ctrl-R searches history in common Emacs-style keymaps; Ctrl-A and Ctrl-E move to the start and end of the editing line. Bindings depend on Readline (Bash), ZLE (Zsh), vi/emacs mode and personal configuration.
Use type printf to inspect command resolution. Use bash --version or zsh --version to check the actual interpreter; $SHELL usually describes the login shell, not necessarily the running interpreter. Use bash script.sh or zsh script.zsh explicitly. For an executable script, add #!/usr/bin/env bash or #!/usr/bin/env zsh as its first line, then chmod +x script.sh and run ./script.sh. A shebang is ignored when you explicitly invoke a different interpreter.
Bash interactive configuration commonly belongs in ~/.bashrc; interactive Zsh configuration in ~/.zshrc. Login startup differs. Do not put tour experiments into startup files. Completion setup and prompt themes are outside this scripting tour.
The explanations were checked against the official Bash Reference Manual (quoting, expansions, arrays, pipelines), the official Zsh manual (expansion, parameters and options) and the Org Babel shell documentation. Output panels are generated by running the supplied code, not copied from those sources.
Do not treat set -e as universal error handling: its behavior depends on command context. Prefer explicit validation and deliberate status handling; pipefail helps expose pipeline failures. Never parse ls output to obtain filenames. Arrays and quoted expansions preserve argument boundaries. This expanded tour covers scripting fundamentals and selected advanced patterns. Interactive job control, completion systems and terminal styling remain outside the executable reference.
printf is a shell builtin. %s inserts a string; \n ends the line. Keep the format string constant.
Back to example galleryprintf '%s\n' 'Hello, shell!'
bash · exit 0
Hello, shell!
zsh · exit 0
Hello, shell!
Change: supply two arguments. printf reuses the format for remaining arguments.
Back to example galleryprintf '%s\n' 'alpha' 'beta'
bash · exit 0
alpha beta
zsh · exit 0
alpha beta
Change: spaces inside quotes remain part of one argument. Angle brackets expose argument boundaries.
Back to example galleryprintf '<%s>\n' 'alpha beta' gamma
bash · exit 0
<alpha beta> <gamma>
zsh · exit 0
<alpha beta> <gamma>
%8s right-aligns in eight characters; %-8s left-aligns. %04d pads an integer with zeros.
Back to example galleryprintf '%-8s %04d\n' apples 7 pears 12
bash · exit 0
apples 0007 pears 0012
zsh · exit 0
apples 0007 pears 0012
%.2f prints two digits after the decimal point. Locale is fixed to C throughout this tour.
Back to example galleryprintf 'mean = %.2f\n' 3.14159
bash · exit 0
mean = 3.14
zsh · exit 0
mean = 3.14
A backslash immediately before a newline continues a command. Do not put spaces after it.
Back to example gallery# Two physical lines, one command printf '%s\n' \ 'continued command'
bash · exit 0
continued command
zsh · exit 0
continued command
No spaces around = in an assignment. Double quotes expand the variable while preserving one argument.
Back to example gallerylabel='sample A' printf '<%s>\n' "$label"
bash · exit 0
<sample A>
zsh · exit 0
<sample A>
Change: single quotes preserve the dollar sign literally; double quotes expand the variable.
Back to example gallerylabel='sample A' printf '%s\n' '$label' "$label"
bash · exit 0
$label sample A
zsh · exit 0
$label sample A
Change: remove double quotes. Default Bash splits on IFS whitespace. Use quoted expansions for filenames.
Back to example gallerylabel='sample A' printf '<%s>\n' $label
bash · exit 0
<sample> <A>
Same code, different shell: native Zsh does not normally split scalar expansions. Quoting is still a good shared convention.
Back to example gallerylabel='sample A' printf '<%s>\n' $label
zsh · exit 0
<sample A>
Change: ${=label} explicitly requests word splitting in Zsh.
Back to example gallerylabel='sample A' printf '<%s>\n' ${=label}
zsh · exit 0
<sample> <A>
Braces delimit the name. Without braces, $label_final would name a different variable.
Back to example gallerylabel=sample printf '%s\n' "${label}_final"
bash · exit 0
sample_final
zsh · exit 0
sample_final
${x:-word} uses word when x is unset or empty. It does not assign x.
Back to example galleryunset label printf '%s\n' "${label:-untitled}"
bash · exit 0
untitled
zsh · exit 0
untitled
Change: remove the colon. ${x-word} substitutes only when x is unset, not when it is an empty string.
Back to example gallerylabel='' printf 'colon=<%s> plain=<%s>\n' "${label:-fallback}" "${label-fallback}"
bash · exit 0
colon=<fallback> plain=<>
zsh · exit 0
colon=<fallback> plain=<>
Change: := also assigns the default. Later uses see the assigned value.
Back to example galleryunset label printf '%s\n' "${label:=untitled}" "$label"
bash · exit 0
untitled untitled
zsh · exit 0
untitled untitled
% removes the shortest matching suffix. Here .csv is a literal suffix.
Back to example galleryfile='sample.raw.csv' printf '%s\n' "${file%.csv}"
bash · exit 0
sample.raw
zsh · exit 0
sample.raw
% removes the shortest matching suffix; %% removes the longest. * is a shell pattern here.
Back to example galleryfile='sample.raw.csv' printf '%s\n' "${file%.*}" "${file%%.*}"
bash · exit 0
sample.raw sample
zsh · exit 0
sample.raw sample
## removes the longest matching prefix. This is a string operation, not a filesystem lookup.
Back to example galleryfile='data/raw/sample.csv' printf '%s\n' "${file##*/}"
bash · exit 0
sample.csv
zsh · exit 0
sample.csv
/ replaces the first match; // replaces all matches. These are patterns, not regular expressions.
Back to example gallerylabel='a-b-c' printf '%s\n' "${label/-/_}" "${label//-/_}"
bash · exit 0
a_b-c a_b_c
zsh · exit 0
a_b-c a_b_c
$((...)) expands an integer result. Integer division truncates toward zero.
Back to example galleryn=7 printf '%s\n' "$((n + 2))" "$((n / 2))" "$((n % 2))"
bash · exit 0
9 3 1
zsh · exit 0
9 3 1
mkdir creates a directory; cp copies a file; cat writes its contents. All files are local to the example sandbox.
Back to example gallerymkdir data printf '%s\n' alpha > data/input.txt cp data/input.txt data/copy.txt cat data/copy.txt
bash · exit 0
alpha
zsh · exit 0
alpha
The unquoted *.txt is expanded by the shell before printf runs. Results use C-locale ordering.
Back to example gallerytouch a.txt b.txt c.csv printf '%s\n' *.txt
bash · exit 0
a.txt b.txt
zsh · exit 0
a.txt b.txt
Change: quote the pattern. The shell now passes a literal asterisk.
Back to example gallerytouch a.txt b.txt c.csv printf '%s\n' '*.txt'
bash · exit 0
*.txt
zsh · exit 0
*.txt
? matches one character; [ab] matches one character from a set.
Back to example gallerytouch a1.txt a2.txt b1.txt aa1.txt printf '%s\n' [ab]?.txt
bash · exit 0
a1.txt a2.txt b1.txt
zsh · exit 0
a1.txt a2.txt b1.txt
Braces generate words even if no matching files exist. This feature is shared by Bash and Zsh, not all sh implementations.
Back to example galleryprintf '%s\n' sample_{A,B}.csv
bash · exit 0
sample_A.csv sample_B.csv
zsh · exit 0
sample_A.csv sample_B.csv
Default Bash retains an unmatched pattern as literal text.
Back to example galleryprintf '<%s>\n' missing*.csv
bash · exit 0
<missing*.csv>
Default native Zsh reports an error and does not run printf. This example intentionally exits nonzero.
Back to example galleryprintf '<%s>\n' missing*.csv
zsh · exit 1
(no stdout)
zsh:1: no matches found: missing*.csv
nullglob removes unmatched patterns. An array then accurately contains zero filenames.
Back to example galleryshopt -s nullglob files=(missing*.csv) printf 'files=%s\n' "${#files[@]}"
bash · exit 0
files=0
The (N) glob qualifier removes this pattern when it has no matches; it does not change all future globs.
Back to example galleryfiles=(missing*.csv(N)) printf 'files=%s\n' "${#files[@]}"
zsh · exit 0
files=0
> redirects stdout and replaces existing contents. cat then displays the file.
Back to example galleryprintf '%s\n' alpha > notes.txt cat notes.txt
bash · exit 0
alpha
zsh · exit 0
alpha
Change: >> appends rather than replacing.
Back to example galleryprintf '%s\n' alpha > notes.txt printf '%s\n' beta >> notes.txt cat notes.txt
bash · exit 0
alpha beta
zsh · exit 0
alpha beta
< connects a file to stdin. tr replaces lowercase letters with uppercase letters.
Back to example galleryprintf '%s\n' alpha > notes.txt tr 'a-z' 'A-Z' < notes.txt
bash · exit 0
ALPHA
zsh · exit 0
ALPHA
| connects stdout of the left command to stdin of the right command. It does not pipe stderr by default. |
Back to example galleryprintf '%s\n' alpha beta | tr 'a-z' 'A-Z'
bash · exit 0
ALPHA BETA
zsh · exit 0
ALPHA BETA
sort groups equal adjacent lines; uniq -c counts each group. Both are external utilities.
Back to example galleryprintf '%s\n' pear apple pear | sort | uniq -c
bash · exit 0
1 apple 2 pear
zsh · exit 0
1 apple 2 pear
File descriptor 1 is stdout; 2 is stderr. >&2 sends this printf to stderr.
Back to example galleryprintf '%s\n' result printf '%s\n' diagnostic >&2
bash · exit 0
result
diagnostic
zsh · exit 0
result
diagnostic
> captures stdout, then 2>&1 points stderr at that destination. The braces group commands in the current shell.
Back to example gallery{ printf 'result\n'; printf 'diagnostic\n' >&2; } > log.txt 2>&1 cat log.txt
bash · exit 0
result diagnostic
zsh · exit 0
result diagnostic
Change: reverse the redirections. stderr keeps the original stdout destination while stdout goes to the file.
Back to example gallery{ printf 'result\n'; printf 'diagnostic\n' >&2; } 2>&1 > log.txt printf 'FILE:\n' cat log.txt
bash · exit 0
diagnostic FILE: result
zsh · exit 0
diagnostic FILE: result
Quoting the delimiter prevents expansion in the document. The closing delimiter starts at column one.
Back to example gallerylabel=sample cat <<'TEXT' $label is literal here. TEXT
bash · exit 0
$label is literal here.
zsh · exit 0
$label is literal here.
Change: leave the delimiter unquoted. Variable and command substitutions are now expanded.
Back to example gallerylabel=sample cat <<TEXT $label is expanded here. TEXT
bash · exit 0
sample is expanded here.
zsh · exit 0
sample is expanded here.
tee writes a copy to a file and passes the same stream onward. The second command reads the saved copy.
Back to example galleryprintf 'alpha\n' | tee copy.txt | tr 'a-z' 'A-Z' cat copy.txt
bash · exit 0
ALPHA alpha
zsh · exit 0
ALPHA alpha
<<< sends one expanded string to standard input and appends a newline. Double quotes make the intended input clear; no temporary file or producer pipeline is needed.
Back to example gallerytext='alpha beta' tr '[:lower:]' '[:upper:]' <<< "$text"
bash · exit 0
ALPHA BETA
Change: use an empty string. The here-string still supplies its appended newline, so read succeeds with an empty line. printf without a newline supplies no bytes, so the second read reaches EOF and returns 1. The group prints that status without treating EOF as a successful read.
Back to example gallerytext='' IFS= read -r line <<< "$text" printf 'here-string: status=%s length=%s\n' "$?" "${#line}" printf '%s' "$text" | { IFS= read -r line printf 'printf: status=%s length=%s\n' "$?" "${#line}" }
bash · exit 0
here-string: status=0 length=0 printf: status=1 length=0
$? is the most recent command status. Zero means success; nonzero has command-specific meaning.
Back to example galleryfalse printf 'status=%s\n' "$?"
bash · exit 0
status=1
zsh · exit 0
status=1
&& runs its right-hand command only if the left-hand command succeeds.
Back to example gallerytrue && printf 'runs\n' false && printf 'skipped\n' printf 'done\n'
bash · exit 0
runs done
zsh · exit 0
runs done
| | runs its right-hand command only if the left-hand command fails. |
Back to example galleryfalse || printf 'fallback\n'
bash · exit 0
fallback
zsh · exit 0
fallback
[[...]] is a Bash/Zsh conditional construct; -gt means numerically greater than.
Back to example galleryn=7 if [[ "$n" -gt 5 ]]; then printf 'large\n' else printf 'small\n' fi
bash · exit 0
large
zsh · exit 0
large
An unquoted pattern on the right of == performs pattern matching inside [[...]].
Back to example galleryfile='sample.csv' if [[ "$file" == *.csv ]]; then printf 'CSV input\n' fi
bash · exit 0
CSV input
zsh · exit 0
CSV input
Patterns before ) select a branch; ;; ends a branch; * provides a catch-all.
Back to example galleryfile='sample.tsv' case "$file" in *.csv) printf 'comma\n' ;; *.tsv) printf 'tab\n' ;; *) printf 'unknown\n' ;; esac
bash · exit 0
tab
zsh · exit 0
tab
Without pipefail, the final command determines the pipeline status.
Back to example galleryfalse | cat printf 'pipeline=%s\n' "$?"
bash · exit 0
pipeline=0
zsh · exit 0
pipeline=0
Change: pipefail makes the rightmost failing command determine status, or zero when all succeed.
Back to example galleryset -o pipefail false | cat printf 'pipeline=%s\n' "$?"
bash · exit 0
pipeline=1
zsh · exit 0
pipeline=1
for assigns each argument to item, then runs the body. Quoted words remain single items.
Back to example galleryfor item in alpha 'beta gamma'; do printf '<%s>\n' "$item" done
bash · exit 0
<alpha> <beta gamma>
zsh · exit 0
<alpha> <beta gamma>
IFS= avoids trimming; read -r preserves backslashes. Input redirection avoids a pipeline subshell.
Back to example galleryprintf '%s\n' ' alpha' 'a\b' > lines.txt while IFS= read -r line; do printf '<%s>\n' "$line" done < lines.txt
bash · exit 0
< alpha> <a\b>
zsh · exit 0
< alpha> <a\b>
-lt means less than. Assignment with arithmetic expansion increments the counter.
Back to example galleryn=0 while [[ "$n" -lt 3 ]]; do printf '%s\n' "$n" n=$((n + 1)) done
bash · exit 0
0 1 2
zsh · exit 0
0 1 2
set -- sets this shell's arguments. "$@" preserves each argument, including spaces.
Back to example galleryset -- alpha 'beta gamma' printf 'count=%s\n' "$#" printf '<%s>\n' "$@"
bash · exit 0
count=2 <alpha> <beta gamma>
zsh · exit 0
count=2 <alpha> <beta gamma>
Change: "$*" forms one argument, joined by the first character of IFS (normally a space).
Back to example galleryset -- alpha 'beta gamma' printf '<%s>\n' "$*"
bash · exit 0
<alpha beta gamma>
zsh · exit 0
<alpha beta gamma>
Functions receive their own positional arguments. "$1" is the first; local limits a variable to function scope.
Back to example gallerygreet() { local person="$1" printf 'Hello, %s!\n' "$person" } greet 'Ada Lovelace'
bash · exit 0
Hello, Ada Lovelace!
zsh · exit 0
Hello, Ada Lovelace!
$(...) captures stdout and removes trailing newlines. Quote the result when expanding it.
Back to example gallerygreet() { printf 'Hello, %s!\n' "$1"; } message=$(greet Ada) printf '<%s>\n' "$message"
bash · exit 0
<Hello, Ada!>
zsh · exit 0
<Hello, Ada!>
return sets an exit status, not a text value. Use stdout for data and status for success/failure.
Back to example galleryis_csv() { [[ "$1" == *.csv ]]; } if is_csv sample.csv; then printf 'accepted\n'; fi
bash · exit 0
accepted
zsh · exit 0
accepted
Indexed Bash arrays normally start at 0. Use braces around indexed expansions.
Back to example galleryitems=(alpha 'beta gamma') printf '%s\n' "${items[0]}" "${items[1]}"
bash · exit 0
alpha beta gamma
Native Zsh arrays normally start at 1. This assumes KSH_ARRAYS is not enabled.
Back to example galleryitems=(alpha 'beta gamma') printf '%s\n' "${items[1]}" "${items[2]}"
zsh · exit 0
alpha beta gamma
"${items[@]}" passes each element separately in both shells, including elements containing spaces.
Back to example galleryitems=(alpha 'beta gamma') printf 'count=%s\n' "${#items[@]}" printf '<%s>\n' "${items[@]}"
bash · exit 0
count=2 <alpha> <beta gamma>
zsh · exit 0
count=2 <alpha> <beta gamma>
+= appends elements to an indexed array.
Back to example galleryitems=(alpha) items+=('beta gamma') printf '<%s>\n' "${items[@]}"
bash · exit 0
<alpha> <beta gamma>
zsh · exit 0
<alpha> <beta gamma>
declare -A requires Bash 4 or newer. A system Bash 3.2 cannot run this example.
Back to example gallerydeclare -A units units[mass]=kg units[time]=s printf '%s\n' "${units[mass]}"
bash · exit 0
kg
Change: declare the mapping with typeset -A in Zsh.
Back to example gallerytypeset -A units units[mass]=kg units[time]=s printf '%s\n' "${units[mass]}"
zsh · exit 0
kg
(s:,:) splits on a comma; (j: / :) joins the resulting array with a chosen separator.
Back to example gallerytext='alpha,beta,gamma' items=("${(@s:,:)text}") printf '%s\n' "${(j: / :)items}"
zsh · exit 0
alpha / beta / gamma
:t selects the tail (basename); :h the directory; :r removes the final extension. No external basename command is needed.
Back to example galleryfile='data/sample.csv' printf '%s\n' "${file:t}" "${file:h}" "${file:r}"
zsh · exit 0
sample.csv data data/sample
(...) runs in a subshell environment. Changes to n do not propagate back.
Back to example galleryn=1 (n=2; printf 'inside=%s\n' "$n") printf 'outside=%s\n' "$n"
bash · exit 0
inside=2 outside=1
zsh · exit 0
inside=2 outside=1
Change: {...;} groups commands in the current shell; the assignment persists.
Back to example galleryn=1 { n=2; printf 'inside=%s\n' "$n"; } printf 'outside=%s\n' "$n"
bash · exit 0
inside=2 outside=2
zsh · exit 0
inside=2 outside=2
export adds a variable to the environment of future child processes. The child here is explicitly Bash.
Back to example galleryexport TOUR_LABEL=sample bash --noprofile --norc -c 'printf "%s\n" "$TOUR_LABEL"'
bash · exit 0
sample
zsh · exit 0
sample
& starts a job asynchronously. $! identifies the last background job; wait retrieves its exit status.
Back to example gallery(exit 7) & job_pid=$! wait "$job_pid" printf 'job status=%s\n' "$?"
bash · exit 0
job status=7
zsh · exit 0
job status=7
zsh:1: nice(5) failed: operation not permitted
<(...) supplies a path from which a command can read generated output. diff compares the two sorted streams.
Back to example gallerydiff <(printf 'b\na\n' | sort) <(printf 'a\nb\n') printf 'diff status=%s\n' "$?"
bash · exit 0
diff status=0
zsh · exit 0
diff status=0
set -u rejects an unset variable expansion. This example intentionally fails; it does not demonstrate all errexit rules.
Back to example galleryset -u unset missing printf '%s\n' "$missing"
bash · exit 127
(no stdout)
bash: line 3: missing: unbound variable
zsh · exit 1
(no stdout)
zsh:3: missing: parameter not set
Change: supply a default for the potentially unset variable.
Back to example galleryset -u unset missing printf '%s\n' "${missing:-fallback}"
bash · exit 0
fallback
zsh · exit 0
fallback
trap registers an exit action. Only the file created by this example is removed.
Back to example galleryprintf 'temporary\n' > scratch.txt trap 'rm -f scratch.txt; printf "cleaned up\n"' EXIT cat scratch.txt
bash · exit 0
temporary cleaned up
zsh · exit 0
temporary cleaned up
A quoted here-document creates a predictable input file. This is TSV data with actual tab characters.
Back to example gallerycat > measurements.tsv <<'DATA' sample value A 3 B 7 C 5 DATA cat measurements.tsv
bash · exit 0
sample value A 3 B 7 C 5
zsh · exit 0
sample value A 3 B 7 C 5
A readable nonempty file passes -r and -s. Explicit checks give a meaningful failure message.
Back to example galleryprintf 'sample\tvalue\nA\t3\n' > measurements.tsv if [[ ! -r measurements.tsv || ! -s measurements.tsv ]]; then printf 'missing input\n' >&2 exit 1 fi printf 'input accepted\n'
bash · exit 0
input accepted
zsh · exit 0
input accepted
awk is an external tool: NR>1 skips the header; sum/count yields the mean. Every fixture is repeated so this example stands alone.
Back to example galleryprintf 'sample\tvalue\nA\t3\nB\t7\nC\t5\n' > measurements.tsv awk 'NR>1 {sum+=$2; n++} END {printf "samples=%d\nmean=%.2f\n", n, sum/n}' \ measurements.tsv > report.txt cat report.txt
bash · exit 0
samples=3 mean=5.00
zsh · exit 0
samples=3 mean=5.00
An array stores filenames as separate elements. Quoting the array expansion preserves spaces without parsing directory listings.
Back to example gallerytouch 'sample A.txt' 'sample B.txt' files=(*.txt) for file in "${files[@]}"; do printf '<%s>\n' "$file" done
bash · exit 0
<sample A.txt> <sample B.txt>
zsh · exit 0
<sample A.txt> <sample B.txt>
Prefixing a relative name with ./ prevents cat from treating -n as an option. This convention also works with utilities that lack --.
Back to example galleryprintf 'contents\n' > ./-n cat ./-n
bash · exit 0
contents
zsh · exit 0
contents
read -d sets the delimiter; an empty delimiter means NUL here. NUL can separate names containing spaces or newlines. Shell variables cannot store NUL bytes.
Back to example galleryprintf '%s\0' 'sample A.txt' 'sample B.txt' > names.bin while IFS= read -r -d '' file; do printf '<%s>\n' "$file" done < names.bin
bash · exit 0
<sample A.txt> <sample B.txt>
zsh · exit 0
<sample A.txt> <sample B.txt>
Change: the || test retains the final nonempty record even when read reaches EOF without a newline.
Back to example galleryprintf 'alpha\nbeta' > lines.txt while IFS= read -r line || [[ -n "$line" ]]; do printf '<%s>\n' "$line" done < lines.txt
bash · exit 0
<alpha> <beta>
zsh · exit 0
<alpha> <beta>
IFS=: splits a simple colon-delimited line. The last variable receives the remaining fields. This is not a general CSV parser.
Back to example galleryprintf 'sample:7:kg\n' > data.txt while IFS=: read -r name rest; do printf 'name=%s rest=%s\n' "$name" "$rest" done < data.txt
bash · exit 0
name=sample rest=7:kg
zsh · exit 0
name=sample rest=7:kg
continue skips the rest of the current iteration. The pattern is a shell pattern, not a regular expression.
Back to example galleryfor file in a.csv notes.txt b.csv; do [[ "$file" == *.csv ]] || continue printf 'process %s\n' "$file" done
bash · exit 0
process a.csv process b.csv
zsh · exit 0
process a.csv process b.csv
shift removes the first positional argument and renumbers the rest. Check the argument count before shifting in real scripts.
Back to example galleryset -- alpha beta gamma printf 'before=%s\n' "$1" shift printf 'after=%s count=%s\n' "$1" "$#"
bash · exit 0
before=alpha after=beta count=2
zsh · exit 0
before=alpha after=beta count=2
getopts scans short options; opt receives the option letter. OPTIND=1 starts a fresh scan. The leading colon selects silent error reporting.
Back to example galleryset -- -v OPTIND=1 verbose=no while getopts ':v' opt; do case "$opt" in v) verbose=yes ;; esac done printf 'verbose=%s\n' "$verbose"
bash · exit 0
verbose=yes
zsh · exit 0
verbose=yes
Change: n: means -n requires an argument. OPTARG receives it; quotes preserve spaces.
Back to example galleryset -- -n 'sample A' OPTIND=1 while getopts ':n:' opt; do case "$opt" in n) printf 'name=%s\n' "$OPTARG" ;; esac done
bash · exit 0
name=sample A
zsh · exit 0
name=sample A
After option parsing, shift by OPTIND-1 removes consumed arguments. -- marks the end of options.
Back to example galleryset -- -n demo -- 'sample A.csv' b.csv OPTIND=1 while getopts ':n:' opt; do case "$opt" in n) printf 'name=%s\n' "$OPTARG" ;; esac done shift "$((OPTIND - 1))" printf 'file=<%s>\n' "$@"
bash · exit 0
name=demo file=<sample A.csv> file=<b.csv>
zsh · exit 0
name=demo file=<sample A.csv> file=<b.csv>
With a leading colon in optstring, a missing value produces opt=: and OPTARG containing the option letter. The script chooses status 2.
Back to example galleryset -- -n OPTIND=1 while getopts ':n:' opt; do case "$opt" in :) printf 'missing value: -%s\n' "$OPTARG" >&2; exit 2 ;; esac done
bash · exit 2
(no stdout)
missing value: -n
zsh · exit 2
(no stdout)
missing value: -n
With silent reporting, an unknown option produces opt=? and OPTARG contains its letter. Quote ? in case so it is literal.
Back to example galleryset -- -x OPTIND=1 while getopts ':n:' opt; do case "$opt" in '?') printf 'unknown option: -%s\n' "$OPTARG" >&2; exit 2 ;; esac done
bash · exit 2
(no stdout)
unknown option: -x
zsh · exit 2
(no stdout)
unknown option: -x
Default Bash runs pipeline components in subshells. The assignment made by read does not reach the parent shell. lastpipe is not enabled.
Back to example galleryvalue=before printf 'after\n' | read -r value printf '%s\n' "$value"
bash · exit 0
before
Native Zsh runs this final builtin in the current shell. The same source therefore changes value.
Back to example galleryvalue=before printf 'after\n' | read -r value printf '%s\n' "$value"
zsh · exit 0
after
Feed read with process substitution instead of placing it in a pipeline. The builtin then runs in the current shell in both interpreters.
Back to example galleryvalue=before read -r value < <(printf 'after\n') printf '%s\n' "$value"
bash · exit 0
after
zsh · exit 0
after
PIPESTATUS is an indexed array of statuses from the latest foreground pipeline. Copy it immediately: the next command replaces it.
Back to example galleryfalse | true | false codes=("${PIPESTATUS[@]}") printf '%s\n' "${codes[@]}"
bash · exit 0
1 0 1
The corresponding native Zsh array is pipestatus, in lowercase. Copy it before another command changes it.
Back to example galleryfalse | true | false codes=("${pipestatus[@]}") printf '%s\n' "${codes[@]}"
zsh · exit 0
1 0 1
Command substitution removes all trailing newlines, not just the final one. Internal newlines remain.
Back to example galleryvalue=$(printf 'alpha\n\n') printf '<%s> length=%s\n' "$value" "${#value}"
bash · exit 0
<alpha> length=5
zsh · exit 0
<alpha> length=5
dotglob makes * include dotfiles, except . and ..; nullglob removes unmatched patterns. Both settings affect later expansions in this shell.
Back to example gallerytouch visible.txt .hidden.txt shopt -s dotglob nullglob files=(*.txt) printf '%s\n' "${files[@]}"
bash · exit 0
.hidden.txt visible.txt
Change: the D glob qualifier includes dotfiles for this one pattern; N removes an unmatched pattern.
Back to example gallerytouch visible.txt .hidden.txt files=(*.txt(DN)) printf '%s\n' "${files[@]}"
zsh · exit 0
.hidden.txt visible.txt
(.) selects regular files; (/) selects directories. N makes either selection empty instead of raising a no-match error.
Back to example gallerymkdir data touch a.txt printf 'file=%s\n' *(N.) printf 'dir=%s\n' *(N/)
zsh · exit 0
file=a.txt dir=data
**/ searches zero or more directory levels. This example matches both the top-level file and a nested file; N allows no matches.
Back to example gallerymkdir -p data/raw touch top.csv data/raw/sample.csv printf '%s\n' **/*.csv(N)
zsh · exit 0
data/raw/sample.csv top.csv
globstar enables ** to match directory levels; nullglob handles no matches. Requires Bash 4 or newer.
Back to example gallerymkdir -p data/raw touch top.csv data/raw/sample.csv shopt -s globstar nullglob printf '%s\n' **/*.csv
bash · exit 0
data/raw/sample.csv top.csv
mapfile -t reads lines and removes their trailing delimiter. It requires Bash 4 or newer. Empty lines become empty array elements.
Back to example galleryprintf 'alpha\n\nbeta gamma\n' > lines.txt mapfile -t items < lines.txt printf 'count=%s\n' "${#items[@]}" printf '<%s>\n' "${items[@]}"
bash · exit 0
count=3 <alpha> <> <beta gamma>
The (U) expansion flag transforms the result to uppercase without assigning back to the variable.
Back to example gallerylabel='Sample A' printf '%s\n' "${(U)label}" "$label"
zsh · exit 0
SAMPLE A Sample A
The ^^ expansion transforms all lowercase characters. Requires Bash 4 or newer; the original variable is unchanged.
Back to example gallerylabel='Sample A' printf '%s\n' "${label^^}" "$label"
bash · exit 0
SAMPLE A Sample A
set -x traces commands to stderr after expansion; PS4 sets the trace prefix. set +x stops tracing. Avoid tracing secrets.
Back to example galleryPS4='+ trace: ' set -x label=sample printf '%s\n' "$label" set +x
bash · exit 0
sample
+ trace: label=sample + trace: printf '%s\n' sample + trace: set +x
Native Zsh also supports set -x and PS4. Formatting can differ; this example is tested independently.
Back to example galleryPS4='+ trace: ' set -x label=sample printf '%s\n' "$label" set +x
zsh · exit 0
sample
+ trace: label=sample + trace: printf '%s\n' sample + trace: set +x
set -e exits on this unhandled failure. The final printf is never reached. This is only one context of errexit behavior.
Back to example galleryset -e printf 'before\n' false printf 'unreachable\n'
bash · exit 1
before
zsh · exit 1
before
Change: put false in an if condition. The shell expects a status to test and does not exit.
Back to example galleryset -e if false; then printf 'unreachable\n' else printf 'handled\n' fi printf 'continues\n'
bash · exit 0
handled continues
zsh · exit 0
handled continues
Save $? immediately. Logging with printf would otherwise replace the status you intend to report.
Back to example galleryfalse exit_code=$? printf 'command failed\n' >&2 printf 'saved=%s now=%s\n' "$exit_code" "$?"
bash · exit 0
saved=1 now=0
command failed
zsh · exit 0
saved=1 now=0
command failed
! inverts success/failure. Inside this branch $? belongs to the negated condition; it is not the original failing status.
Back to example galleryif ! false; then printf 'negated status=%s\n' "$?" fi
bash · exit 0
negated status=0
zsh · exit 0
negated status=0
exec 3> opens descriptor 3 in the current shell. >&3 writes to it; exec 3>&- closes it. No new process replaces the shell here.
Back to example galleryexec 3> log.txt printf 'log entry\n' >&3 printf 'terminal entry\n' exec 3>&- cat log.txt
bash · exit 0
terminal entry log entry
zsh · exit 0
terminal entry log entry
The EXIT handler records the incoming status, performs cleanup, then returns that same status. The shell retains its explicit exit status of 3.
Back to example gallerycleanup() { local exit_code=$? printf 'cleanup after status=%s\n' "$exit_code" return "$exit_code" } trap 'cleanup' EXIT exit 3
bash · exit 3
cleanup after status=3
zsh · exit 3
cleanup after status=3
trap - EXIT removes the exit handler. The message registered with the old handler is never printed.
Back to example gallerytrap 'printf "old handler\n"' EXIT trap - EXIT printf 'done\n'
bash · exit 0
done
zsh · exit 0
done
An array preserves the command and each argument. Execute it with "${cmd[@]}". A string plus eval would reparse data as shell syntax.
Back to example gallerycmd=(printf '<%s>\n' 'sample A' 'sample B') "${cmd[@]}"
bash · exit 0
<sample A> <sample B>
zsh · exit 0
<sample A> <sample B>
The dot builtin evaluates a file in the current shell. Its function definition remains available; source is an equivalent Bash/Zsh spelling.
Back to example gallerycat > helpers.sh <<'LIB' greet() { printf 'Hello, %s!\n' "$1"; } LIB . ./helpers.sh greet Ada
bash · exit 0
Hello, Ada!
zsh · exit 0
Hello, Ada!
emulate -L zsh sets a known option baseline and localizes option changes to the function. Here null_glob does not persist afterward.
Back to example gallerycollect() { emulate -L zsh setopt null_glob local files=(missing*.csv) printf 'files=%s\n' "${#files[@]}" } collect [[ -o null_glob ]] || printf 'option restored\n'
zsh · exit 0
files=0 option restored
An ERR trap runs after an unhandled failing command. Save $? as the first action in the handler. The trap reports the status; without errexit, the script continues.
Back to example gallerytrap 'status=$?; printf "ERR: status=%s\n" "$status"' ERR false printf 'script continues\n'
bash · exit 0
ERR: status=1 script continues
Change: test false in an if condition. The else branch handles its status, so the ERR trap does not run. A trap cannot replace explicit checks in these contexts.
Back to example gallerytrap 'status=$?; printf "ERR: status=%s\n" "$status"' ERR if false; then printf 'unexpected success\n' else printf 'failure handled\n' fi
bash · exit 0
failure handled
set +E disables ERR-trap inheritance. The failure inside the function is followed by a successful printf, so the function itself returns zero and no outer ERR trap fires.
Back to example galleryset +E trap 'status=$?; printf "ERR: status=%s\n" "$status"' ERR run_step() { false printf 'function continues\n' } run_step printf 'caller continues\n'
bash · exit 0
function continues caller continues
Change only +E to -E. Error tracing inherits the ERR trap inside the function, so its unchecked false is reported. This option enables inheritance, not automatic exit.
Back to example galleryset -E trap 'status=$?; printf "ERR: status=%s\n" "$status"' ERR run_step() { false printf 'function continues\n' } run_step printf 'caller continues\n'
bash · exit 0
ERR: status=1 function continues caller continues
set -e exits after this unchecked false. The ERR handler reports status 1 to stderr before exit; the final printf is never reached. The example intentionally exits 1. Checked conditions still follow the exception illustrated in BSH-16-008.
Back to example galleryset -e trap 'status=$?; printf "ERR: status=%s\n" "$status" >&2' ERR printf 'before failure\n' false printf 'unreachable\n'
bash · exit 1
before failure
ERR: status=1
${text:offset:length} extracts length characters starting at zero-based offset. It does not change text.
Zsh difference: Scalar substring offsets are also zero-based in Zsh; this differs from native Zsh array indexing.
text='sample-2026.csv' printf '%s\n' "${text:7:4}"
bash · exit 0
2026
A negative offset counts from the end. Keep a space before -4 to distinguish this from the :- default-value operator.
Back to example gallerytext='sample-2026.csv' printf '%s\n' "${text: -4}"
bash · exit 0
.csv
Bash printf -v assigns its formatted output without a command substitution. %03d pads to at least three digits.
Back to example galleryprintf -v sample 'sample-%03d' 7 printf '%s\n' "$sample"
bash · exit 0
sample-007
printf %q produces a representation that can be read as one Bash argument. This is shell quoting, not JSON or URL encoding.
Zsh difference: Zsh also offers quoting facilities, but printed quoting styles can differ. Do not use a %q representation as a cross-language serialization format.
file='sample A.csv' printf '%q\n' "$file"
bash · exit 0
sample\ A.csv
=~ uses an extended regular expression. ^ and $ anchor the whole string; + requires one or more digits. Leave the regex variable unquoted on the right.
Back to example galleryvalue='123' pattern='^[0-9]+$' if [[ "$value" =~ $pattern ]]; then printf 'integer text\n' fi
bash · exit 0
integer text
BASH_REMATCH[0] is the entire match; [1] and [2] contain parenthesized groups. Read captures only after a successful match.
Zsh difference: Native Zsh normally uses MATCH for the full match and match[1], match[2], ... for captures. BASH_REMATCH is not its default capture interface.
file='sample-042.csv' pattern='^([a-z]+)-([0-9]+)\.csv$' if [[ "$file" =~ $pattern ]]; then printf 'label=%s number=%s\n' \ "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" fi
bash · exit 0
label=sample number=042
Change: quote the expanded pattern on the right of =~. Its metacharacters lose their regex meaning.
Back to example galleryvalue='123' pattern='^[0-9]+$' if [[ "$value" =~ "$pattern" ]]; then printf 'literal match\n' else printf 'no literal match\n' fi
bash · exit 0
no literal match
((expression)) succeeds when the value is nonzero. Zero produces status 1. This can interact unexpectedly with set -e.
Back to example gallery(( 2 + 3 )) printf 'nonzero status=%s\n' "$?" (( 2 - 2 )) printf 'zero status=%s\n' "$?"
bash · exit 0
nonzero status=0 zero status=1
n++ evaluates to the old value, whereas ++n evaluates to the new value. Both increment n, but their exit statuses can differ.
Back to example galleryn=0 ((n++)) printf 'post: n=%s status=%s\n' "$n" "$?" n=0 ((++n)) printf 'pre: n=%s status=%s\n' "$n" "$?"
bash · exit 0
post: n=1 status=1 pre: n=1 status=0
10# forces base ten. Without an explicit base, a leading zero denotes octal in Bash arithmetic. Validate input before arithmetic evaluation.
Back to example galleryvalue=008 printf '%s\n' "$((10#$value + 1))"
bash · exit 0
9
The three clauses initialize, test and advance the counter. This loop prints 0, 1 and 2.
Back to example galleryfor ((i=0; i<3; i++)); do printf '%s\n' "$i" done
bash · exit 0
0 1 2
break leaves the nearest enclosing loop. Here the final iteration is never reached.
Back to example galleryfor value in 3 7 9; do if ((value > 5)); then printf 'first=%s\n' "$value" break fi done
bash · exit 0
first=7
Bash arithmetic is integer arithmetic. Use a numeric tool for decimal work; awk is used here with C-locale output.
Zsh difference: Zsh supports floating-point arithmetic natively, for example $((7.0 / 2)). Bash does not accept that expression.
awk 'BEGIN {printf "%.2f\n", 7 / 2}'
bash · exit 0
3.50
Assigning index 5 does not create indices 0-4. ${#items[@]} counts assigned elements, not the largest index plus one.
Zsh difference: Native Zsh arrays use different indexing and gap semantics. Do not translate sparse Bash arrays by simply adding one to each index.
items=() items[2]=alpha items[5]=beta printf 'count=%s\n' "${#items[@]}" printf 'index=%s\n' "${!items[@]}"
bash · exit 0
count=2 index=2 index=5
Quote the unset argument so the brackets are not treated as filename syntax. Remaining Bash array indices are not renumbered.
Zsh difference: Native Zsh array element deletion and indexing differ; the sparse-array behavior shown here is Bash-specific.
items=(alpha beta gamma) unset 'items[1]' printf 'index=%s\n' "${!items[@]}" printf 'value=%s\n' "${items[@]}"
bash · exit 0
index=0 index=2 value=alpha value=gamma
${items[@]:offset:length} selects length elements. Quotes keep each selected element separate.
Zsh difference: Zsh native array subscript ranges are inclusive and normally one-based, for example "${items[2,3]}". Do not confuse them with Bash offset/length notation.
items=(alpha 'beta gamma' delta epsilon) printf '<%s>\n' "${items[@]:1:2}"
bash · exit 0
<beta gamma> <delta>
${!name} reads the variable whose name is stored in name. Use known variable names; this is not a reason to use eval.
Zsh difference: Zsh uses the (P) parameter expansion flag for this form of indirection, for example "${(P)name}".
label=sample name=label printf '%s\n' "${!name}"
bash · exit 0
sample
declare -n creates a reference to another variable. Writing through ref updates label. Requires Bash 4.3 or newer.
Zsh difference: Do not assume Bash declare -n is available in Zsh; this syntax is not supported by the Zsh 5.9 comparison environment.
label=before declare -n ref=label ref=after printf '%s\n' "$label"
bash · exit 0
after
A local nameref lets a function populate the caller-supplied array without flattening it into text. Requires Bash 4.3+. Use a reference name that cannot collide with the target.
Zsh difference: This uses Bash namerefs; it is not a portable Zsh function interface.
make_items() { local -n output_ref="$1" output_ref=(alpha 'beta gamma') } items=() make_items items printf '<%s>\n' "${items[@]}"
bash · exit 0
<alpha> <beta gamma>
${!tour_demo_@} expands to variable names beginning with tour_demo_, rather than their values. Quoted @ expansion keeps each name separate. sort makes the display order explicit.
Back to example gallerytour_demo_alpha=12 tour_demo_beta=7 printf '%s\n' "${!tour_demo_@}" | sort
bash · exit 0
tour_demo_alpha tour_demo_beta
Change: iterate over the discovered names and use ${!variable} to read each value. The names come from local variables with the chosen prefix; no eval or command-string construction is needed. sort fixes the presentation order.
Back to example gallerytour_demo_alpha=12 tour_demo_beta=7 for variable in "${!tour_demo_@}"; do printf '%s=%s\n' "$variable" "${!variable}" done | sort
bash · exit 0
tour_demo_alpha=12 tour_demo_beta=7
set -C enables noclobber: > refuses to overwrite an existing regular file. Catch the status explicitly. Error text is suppressed here so the intentional outcome is clear.
Back to example galleryprintf 'original\n' > report.txt set -C if { printf 'replacement\n' > report.txt; } 2>/dev/null; then printf 'written\n' else printf 'overwrite refused\n' fi cat report.txt
bash · exit 0
overwrite refused original
Change: >| permits this overwrite even when noclobber is enabled. It is a deliberate exception for this redirection.
Back to example galleryprintf 'original\n' > report.txt set -C printf 'replacement\n' >| report.txt cat report.txt
bash · exit 0
replacement
Set nullglob inside (...) to keep the option change local. The parent retains its previous value.
Zsh difference: Zsh uses setopt rather than shopt; its emulate -L zsh idiom localizes options inside a function.
shopt -u nullglob ( shopt -s nullglob files=(missing*.csv) printf 'inside=%s\n' "${#files[@]}" ) shopt -q nullglob || printf 'parent unchanged\n'
bash · exit 0
inside=0 parent unchanged
nocasematch affects [[...]] and case matching. It is distinct from nocaseglob, which changes filename expansion.
Zsh difference: The equivalent native Zsh option is NO_CASE_MATCH, set with setopt nocasematch.
shopt -s nocasematch file='SAMPLE.CSV' if [[ "$file" == *.csv ]]; then printf 'matched\n'; fi
bash · exit 0
matched
lastpipe lets the last foreground pipeline command run in the current shell when job control is disabled. Requires Bash 4.2+.
Zsh difference: Native Zsh already runs this final builtin in the current shell. It does not require Bash lastpipe.
set +m shopt -s lastpipe value=before printf 'after\n' | read -r value printf '%s\n' "$value"
bash · exit 0
after
set -f disables globbing. The literal asterisk reaches printf even though matching files exist. set +f enables globbing again.
Back to example gallerytouch a.txt b.txt set -f printf '%s\n' *.txt set +f printf '%s\n' *.txt
bash · exit 0
*.txt a.txt b.txt
command -v tests whether a command is available. Redirect both streams when only its status matters.
Back to example galleryif command -v awk >/dev/null 2>&1; then printf 'awk available\n' else printf 'awk missing\n' >&2 exit 1 fi
bash · exit 0
awk available
Use $# before accessing required arguments. A function can return a usage-error status without exiting its caller.
Back to example galleryrequire_one() { if (( $# != 1 )); then printf 'expected one argument\n' >&2 return 2 fi printf 'input=%s\n' "$1" } require_one 'sample A' require_one printf 'validation status=%s\n' "$?"
bash · exit 0
input=sample A validation status=2
expected one argument
stdout remains suitable for further processing; diagnostics go to stderr. Capture each stream independently.
Back to example galleryproduce() { printf 'processing sample\n' >&2 printf '7\n' } produce > values.txt 2> progress.log printf 'DATA: '; cat values.txt printf 'LOG: '; cat progress.log
bash · exit 0
DATA: 7 LOG: processing sample
The wrapper shows the argument boundaries using %q, then optionally executes the original array of arguments. It does not reparse the printed representation.
Back to example galleryrun() { if [[ "$dry_run" == yes ]]; then printf 'would run:' printf ' %q' "$@" printf '\n' else "$@" fi } dry_run=yes run printf '%s\n' 'sample A'
bash · exit 0
would run: printf %s\\n sample\ A
A small assertion compares the computed value with an independently stated expected result. On failure the script would exit nonzero.
Back to example gallerystem() { printf '%s\n' "${1%.csv}"; } actual=$(stem sample.csv) if [[ "$actual" != sample ]]; then printf 'FAIL: stem\n' >&2 exit 1 fi printf 'PASS: stem\n'
bash · exit 0
PASS: stem
Content: CC0 1.0. Structure and publishing framework: CC BY 4.0 by Lukas C. Bossert. Repository source · Publishing framework source · License scope and third-party notices.