Advanced Bash-Scripting guide: a learning path for Bash 5
This guide is a map for learning Bash scripting today: five stages from first script to robust tools, a tested script skeleton to start from, a list of what changed since the Bash 2 era, and pointers into the official GNU Bash manual for each topic.
The address once held the Advanced Bash-Scripting Guide, a book-length tutorial written for Bash 2. That classic guide is kept by the LDP at tldp.org. This page is a new, shorter overview written for current Bash.
- 5.3current Bash release (July 2025), as of November 2026
- 5.3.20latest official patch level, September 2026
- 5.2shipped by Debian 13 and Ubuntu 24.04 LTS
- 5.3shipped by Ubuntu 26.04 LTS
Check your own version with bash --version or echo "$BASH_VERSION". Bash patches are published on the GNU servers between releases and reach you through your distribution's updates; you do not need to build Bash yourself.
A learning path in five stages
- Commands, quoting and variables. Write and run a script, understand double versus single quotes, use
"$var"everywhere and read arguments with"$@". Start with the Bash scripting introduction. - Decisions and loops. Exit status,
ifwith[[ ]]and(( )),case,forover globs, andwhile read -rover lines. Same page; practise by rewriting a task you do by hand every week. - Functions and error handling.
localvariables, returning data by printing it, checking every command that matters, and the real behavior ofset -euo pipefail. The introduction's "honest" section onset -ematters more than any other. - Data and text. Indexed and associative arrays,
mapfile, parameter expansion instead ofsed/basename, process substitution and here-documents. See the advanced Bash scripting HOWTO. - Tools other people use.
getoptsoptions and a usage message, traps for cleanup, debugging withset -xandPS4, ShellCheck in your editor. Same HOWTO. Make your own shell pleasant along the way with the Bash prompt HOWTO.
At each stage, run ShellCheck on what you write and read the explanation behind every warning. That habit teaches the quoting rules faster than any book.
A script skeleton
This template collects the patterns from the HOWTOs: a usage message, getopts, error helpers, a temporary file removed by an EXIT trap, and all logic inside a main function. It passes ShellCheck and runs unchanged in Bash 5.2 and 5.3.
#!/usr/bin/env bash
# name.sh - one line saying what this script does
# Usage: name.sh [-n] [-v] FILE...
set -uo pipefail
readonly PROG=${0##*/}
dry_run=0
verbose=0
tmp= # global, so the EXIT trap can still see it
die() { printf '%s: %s\n' "$PROG" "$*" >&2; exit 1; }
log() { (( verbose )) && printf '%s: %s\n' "$PROG" "$*" >&2; return 0; }
usage() { printf 'Usage: %s [-n] [-v] FILE...\n' "$PROG"; }
main() {
local opt
while getopts ':nvh' opt; do
case $opt in
n) dry_run=1 ;;
v) verbose=1 ;;
h) usage; exit 0 ;;
*) usage >&2; exit 2 ;;
esac
done
shift $(( OPTIND - 1 ))
(( $# > 0 )) || { usage >&2; exit 2; }
tmp=$(mktemp) || die "cannot create a temporary file"
trap 'rm -f -- "$tmp"' EXIT
local f
for f in "$@"; do
[[ -r $f ]] || die "cannot read $f"
log "processing $f"
if (( dry_run )); then
printf 'would process %s\n' "$f"
else
sort -u -- "$f" > "$tmp" && wc -l < "$tmp"
fi
done
}
main "$@"
Try it with ./name.sh -v /etc/hostname /etc/shells (prints the number of unique lines in each file) and ./name.sh -n /etc/hostname (dry run). Notes on the choices:
tmpis a global variable because theEXITtrap runs aftermainhas returned; alocalvariable would be gone by then and, underset -u, the trap would fail.logends withreturn 0so that a disabled log call never returns "false" to its caller.- The script uses
set -uo pipefailbut not-e; failures that matter are handled with||anddie. Add-eif you prefer, after reading its caveats. - Putting the code in
main "$@"on the last line means a script truncated during download or editing does not run half of its commands.
What changed since Bash 2
Old tutorials, including the classic one, were written when Bash 2 was current. These features arrived since then (from the Bash NEWS file); prefer them over the old workarounds.
| Release | Feature | Replaces |
|---|---|---|
| 4.0 (2009) | associative arrays (declare -A), mapfile/readarray, ${var^^} and ${var,,} case changes, globstar (**), coproc, |& | parallel arrays, while read loops into arrays, tr, find for simple recursion |
| 4.3 | namerefs (declare -n) | eval to reach variables by name |
| 4.4 | ${var@Q} and the other @ transformations, PS0, inherit_errexit, mapfile -d | hand-written quoting code |
| 5.0 | EPOCHSECONDS, EPOCHREALTIME, BASH_ARGV0 | $(date +%s) |
| 5.1 | SRANDOM (32-bit random numbers), PROMPT_COMMAND as an array, ${var@U}/@u/@L, wait -p | $RANDOM for anything that matters, long PROMPT_COMMAND strings |
| 5.2 | patsub_replacement (& in ${var/pat/rep} means the match), @k | (watch out: changes old scripts that used a literal &) |
| 5.3 (2025) | ${ command; } and ${|command;} substitution without a subshell, GLOBSORT, BASH_MONOSECONDS, BASH_TRAPSIG, source -p PATH | $(…) where a fork is too slow, ls -t for sorting by time |
Things to stop doing: backticks (use $(…)), [ ] with unquoted variables (use [[ ]] in Bash, or quote everything), expr and let (use $(( ))), function name () mixtures (use name() { …; }), and parsing ls.
Bash or POSIX sh?
A script that starts with #!/bin/sh must use only POSIX shell features, because /bin/sh is dash on Debian and Ubuntu and Bash in POSIX mode on Fedora and the RHEL family. Write #!/usr/bin/env bash (or #!/bin/bash) whenever you use arrays, [[ ]], local with options, or any feature in the table above. To check a script meant for sh, run shellcheck -s sh script; it flags every Bash-only construct.
Bash is a poor fit once a script needs complex data structures, floating-point math, or more than a few hundred lines of logic. At that point a general-purpose language such as Python is easier to test and maintain, and can still call the same commands.
Where the Bash manual covers it
The Bash Reference Manual (edition 5.3) is the definitive reference; man bash and info bash give you the same material offline, and help BUILTIN (for example help getopts) prints a quick summary of any builtin.
| Topic | Manual section |
|---|---|
| Quoting | 3.1.2 Quoting |
if, case, [[ ]], loops | 3.2.5 Compound Commands; 6.4 Bash Conditional Expressions |
| Functions | 3.3 Shell Functions |
| Parameter expansion | 3.5.3 Shell Parameter Expansion |
| Command and process substitution | 3.5.4 Command Substitution; 3.5.6 Process Substitution |
| Redirections, here-documents | 3.6 Redirections (3.6.6 Here Documents, 3.6.7 Here Strings) |
| Exit status and signals | 3.7.5 Exit Status; 3.7.6 Signals |
getopts, trap, read, mapfile | 4.1 Bourne Shell Builtins; 4.2 Bash Builtin Commands |
set -e, pipefail, shopt options | 4.3.1 The Set Builtin; 4.3.2 The Shopt Builtin |
| Special variables | 5.2 Bash Variables |
Startup files (~/.bashrc, ~/.bash_profile) | 6.2 Bash Startup Files |
| Arithmetic and arrays | 6.5 Shell Arithmetic; 6.7 Arrays |
| Prompts | 6.9 Controlling the Prompt |
| POSIX mode and compatibility levels | 6.11 Bash and POSIX; 6.12 Shell Compatibility Mode |
| Line editing and history | 8 Command Line Editing; 9 Using History Interactively |
Getting help
- help-bash@gnu.org, the GNU mailing list for questions about Bash and Bash scripting; bug reports go to bug-bash@gnu.org. Both are listed on the GNU Bash home page.
- The BashFAQ on Greg's Wiki answers the classic questions, such as why
set -esurprises people. - ShellCheck's wiki explains each warning with examples of wrong and right code.
- When asking for help, include your Bash version, the smallest script that shows the problem, what you expected and what happened.
Sources
- GNU Bash home page, GNU Project
- Bash Reference Manual, edition 5.3 (table of contents), GNU Project
- GNU Bash releases and Bash 5.3 official patches, ftp.gnu.org
- Bash NEWS file (features added in each release), Chet Ramey, Case Western Reserve University
- bash(1) manual page (GNU Bash 5.3), man7.org
- bash package versions, packages.debian.org
- bash package versions, packages.ubuntu.com
- ShellCheck wiki
- BashFAQ/105, Greg's Wiki