Bash (Bourne Again SHell) was written by Brian Fox for the GNU Project in 1989 as a free replacement for the Bourne shell (sh). It is maintained by Chet Ramey at Case Western Reserve University, who has been the primary maintainer since the early 1990s. It is the default login shell on most Linux distributions and was the default on macOS until Catalina switched to zsh in 2019.
Bash is four things at once: a command interpreter, a scripting language, a job controller, and a readline-based interactive environment. Knowing which role it is playing at a given moment explains most of its surprising behaviour.
The first half of this page covers how bash works; the second half is practical recipes. For other shells, see sh, dash, ash, zsh, and fish.
Architecture¶
Every line of input goes through lexing, parsing, expansion, and execution. Expansion happens in this order:
- Brace expansion:
{a,b,c},{1..10} - Tilde, parameter, arithmetic, and command substitution, plus process substitution:
~,$VAR,${VAR:-default},$(( expr )),$(command),<(command). These are performed in a single left-to-right pass, not as separate ordered steps. - Word splitting on
$IFS, applied only to the results of unquoted expansions from step 2 - Pathname expansion (globbing):
*,?,[...] - Quote removal
Many subtle bugs come from wrong assumptions about this order. Brace expansion runs before variables are expanded, so {1..$n} does not work; use seq 1 "$n" or a C-style for (( i=1; i<=n; i++ )) loop. Word splitting and globbing run on unquoted expansions, which is why "$var" should be quoted by default.
Builtins: what they are and why they exist¶
A builtin is a command implemented inside the bash process rather than as a separate executable on disk. Builtins exist for two reasons. Either the operation is impossible as an external process (a child process cannot change its parent's working directory, so cd must be a builtin), or forking a process would be too slow for something called constantly.
When you type a command, bash resolves the name in this order:
- Aliases
- Functions
- Builtins
- External commands, found via the hash table and then
$PATH
In POSIX mode, special builtins (export, set, eval, exec, and a few others) are found before functions.
Force the external version with a full path (/bin/echo) or command echo. Force the builtin with builtin echo. type -a echo shows every match.
Reserved words are not builtins¶
if, then, elif, else, fi, case, esac, for, select, while, until, do, done, in, function, time, !, {, }, [[, and ]] are keywords parsed by the shell grammar, not commands. type if reports if is a shell keyword. List them with compgen -k, and list builtins with compgen -b. Don't use either as function or variable names.
Builtins by category¶
Navigation and environment
- cd: change the current directory (must be a builtin)
- pwd: print the working directory (/bin/pwd also exists)
- export: mark variables for export to child processes
- unset: remove variables or functions
- set: set shell options or positional parameters
- declare / typeset: declare variables with attributes (integer, array, readonly, etc.)
- local: declare function-scoped variables
- readonly: mark variables as read-only
- shift: shift positional parameters left
- Not builtins: env (coreutils) runs a command with a modified environment
I/O
- echo: print arguments (behaviour differs from /bin/echo on -e)
- printf: formatted output; more portable than echo
- read: read a line from stdin into variables
- readarray / mapfile: read lines from stdin into an array
Control flow (builtins that work with the keywords above)
- break / continue: loop control
- return: return from a function or sourced script
- exit: exit the shell with a status
Test and evaluation
- test / [: evaluate conditional expressions (POSIX)
- [[ ]]: extended conditional expression (bash keyword, not POSIX)
- (( )): arithmetic evaluation (a compound command)
- $[ ]: obsolete arithmetic syntax; use $(( ))
- true / false: return 0 or 1 (coreutils versions also exist; the builtins shadow them)
Job control
- jobs: list active jobs
- fg / bg: move a job to the foreground or background
- wait: wait for a job or process to finish
- kill: send a signal (/bin/kill from util-linux or procps-ng also exists; the builtin can take job specs like %1)
- disown: remove a job from the job table so it survives the shell exiting
- suspend: suspend the shell
Functions and sourcing
- source / .: execute a script in the current shell context (must be a builtin)
- function: keyword for defining a function; name() { ...; } is the POSIX form
History and readline
- history: display or manipulate the command history
- fc: edit and re-execute history entries
- bind: bind a readline key sequence to a function
Shell introspection
- type: describe how a name would be interpreted (alias, keyword, function, builtin, file)
- command: run a command, bypassing aliases and functions
- builtin: run a shell builtin explicitly
- enable: enable or disable builtins (enable -n echo makes echo resolve to /bin/echo)
- help: help for builtins
- caller: the context of the current subroutine call (useful for stack traces)
- hash: remember or report full paths of commands (hash -r after moving a binary)
Options and debugging
- set -e: exit on error
- set -u: treat unset variables as errors
- set -x: print commands as they execute (debug trace)
- set -o pipefail: a pipeline fails if any command in it fails
- shopt: set or unset bash-specific options (separate from set)
- trap: set signal handlers or cleanup functions (trap cleanup EXIT)
- times: print accumulated user and system times
Misc
- alias / unalias: define and remove aliases
- getopts: parse short options in scripts
- eval: evaluate a string as a shell command (use carefully)
- exec: replace the shell with a command, or redirect the shell's own file descriptors
- ulimit: set or display resource limits
- umask: set the file creation mask
- compgen / complete / compopt: programmable completion
- Not builtins: xargs (findutils)
Key behavioural distinctions¶
[ vs [[: [ is a POSIX builtin that also exists as /usr/bin/[. [[ is a bash keyword and handles more cases safely. Unquoted variables don't word-split inside [[, regex matching with =~ works, and && / || work directly instead of -a / -o. Prefer [[ in bash-only scripts and [ in scripts targeting /bin/sh or dash.
echo portability: echo -e is bash behaviour. Dash's echo prints -e literally and always interprets escapes, and POSIX does not define -e at all. Use printf in anything meant to run under sh.
source vs .: identical in bash. . is the POSIX form and works in sh; source works in bash and zsh.
declare vs local: local only works inside functions. declare inside a function is equivalent to local, also works at global scope, and supports type attributes like -i (integer), -a (indexed array), -A (associative array), -r (readonly), -x (export), and -g (force global from inside a function).
(( )) vs $(( )): (( expr )) is a command that returns exit status 0 if the result is non-zero, which makes it useful in if and while. $(( expr )) is an expansion that substitutes the numeric result. Under set -e, a bare (( count++ )) when count is 0 exits the script, because the expression evaluates to 0.
Startup files and execution modes¶
Bash reads different files depending on how it is invoked, which explains a large class of "works in my terminal but not in my script" bugs.
| Mode | Files read |
|---|---|
| Interactive login shell | /etc/profile, then the first found of ~/.bash_profile, ~/.bash_login, ~/.profile; ~/.bash_logout on exit |
| Interactive non-login shell | ~/.bashrc (Debian/Ubuntu also read /etc/bash.bashrc first; Fedora/RHEL source /etc/bashrc from ~/.bashrc) |
| Non-interactive (script) | Only the file named in $BASH_ENV, if set |
Invoked as sh |
Login: /etc/profile, ~/.profile. Interactive: the file named in $ENV |
Most distributions' default ~/.bash_profile sources ~/.bashrc, so login shells end up with both. The practical consequence: anything in ~/.bashrc is not available in cron jobs, systemd units, or bash script.sh unless the script sources it explicitly.
Versions¶
The current release is 5.3 (July 2025). Check with bash --version or echo "$BASH_VERSION". Most distributions ship 5.x. macOS still ships 3.2 because bash 4.0 moved to GPLv3, and Apple has not updated it in nearly two decades, which is why brew install bash is a common first step on a Mac. Scripts that must run on stock macOS can't use anything from 4.0 onwards.
Notable milestones:
- 4.0: associative arrays (declare -A), mapfile / readarray, case modification (${var,,}, ${var^^}), globstar (**), coproc, |&
- 4.2: lastpipe option, declare -g, negative array subscripts, \u Unicode escapes
- 4.3: namerefs (declare -n), wait -n
- 4.4: ${parameter@operator} transformations (@Q, @E, @P, @A, @a)
- 5.0: $EPOCHSECONDS, $EPOCHREALTIME, $BASH_ARGV0
- 5.1: $SRANDOM, wait -p, @U / @u / @L / @K transformations
- 5.2: @k transformation, patsub_replacement and globskipdots shell options
- 5.3: in-shell command substitution (${ cmd; } captures output without forking a subshell; ${| cmd; } returns $REPLY), GLOBSORT variable, read -E (readline completion while reading), source -p PATH, compgen -V (results into a variable)
Safer scripts¶
#!/bin/bash
set -euo pipefail # exit on error, on unset variables, and on failures inside pipelines
IFS=$'\n\t'
Quote every variable expansion ("$var") and use -- before user-supplied file names. Run scripts through ShellCheck (shellcheck script.sh) to catch quoting and portability bugs.
History¶
history | grep systemctl # find an old command
!1864 # re-run history entry 1864
fc -s 1864 # same, via fc
!! # the previous command
sudo !! # ... with sudo
^status^reload # repeat the last command, replacing "status" with "reload"
!$ # last argument of the previous command
set +o history # stop recording this shell session's history
# ...commands you don't want recorded...
set -o history # resume
Put a space before a command to keep it out of history if HISTCONTROL=ignorespace (or ignoreboth) is set.
Timestamp every history entry by adding this to ~/.bashrc:
export HISTTIMEFORMAT="%Y-%m-%d %T " # history now shows: 1864 2026-10-03 14:02:11 systemctl status nginx
Getting user input¶
read -rp "Postgres user name: " name
read -rsp "Postgres user password: " password; echo # -s hides input; echo restores the newline
export POSTGRES_USER="$name" POSTGRES_PASSWORD="$password"
read -rp "Continue? [y/N] " answer
[[ ${answer,,} == y* ]] || exit 1 # ,, lowercases
-r stops backslashes being treated as escapes. -t 10 adds a timeout and -n 1 returns after a single keypress.
Emptying a file¶
: > file.txt # truncate to zero bytes
cat /dev/null > file.txt
truncate -s 0 file.txt
Show only "real" lines of a config¶
grep -Ev '^\s*(#|$)' file.conf # drop comments and blank lines
awk '!/^ *#/ && NF' file.conf # awk equivalent: not a comment AND has fields
Loops¶
# Same command on several hosts
hosts=(app01 app02 app03)
for h in "${hosts[@]}"; do
printf '#--- %s\n' "$h"
ssh "$h" 'rpm -qa | grep bash-completion'
done
# Numbered sequence: galera0, galera1, galera2
for n in $(seq 0 2); do virsh dominfo "galera${n}"; done
for n in {0..2}; do echo "node${n}"; done # brace expansion; {01..10} zero-pads
# Files whose names contain spaces: glob, don't parse ls
for f in *; do printf '%s\n' "$f"; done
for f in /media/music/*; do printf '%s\n' "$f"; done
for i in $(ls) splits on whitespace, so names with spaces break into pieces. Loop over the glob instead.
Command output into an array¶
var=$(cmd) gives a single string, not an array, so "${var[@]}" loops once over the whole thing. Use mapfile to get one element per line:
mapfile -t pods < <(kubectl -n openstack get pods --no-headers -o custom-columns=:metadata.name | grep neutron-server)
for p in "${pods[@]}"; do
kubectl -n openstack delete pod "$p"
done
Arrays primer: You don't know Bash: arrays (opensource.com).
One-liners¶
# Convert flac to mp3
for f in *.flac; do ffmpeg -i "$f" -codec:a libmp3lame -qscale:a 2 "${f%.flac}.mp3"; done
# Symlink a directory of files into another directory
for f in ~/Pictures/Trip/*; do ln -s "$f" ~/Pictures/; done
# Strip the first 3 and last 4 lines of several files
for f in host*_chkconfig.txt; do tail -n +4 "$f" | head -n -4 > "$f.new" && mv "$f.new" "$f"; done
# Rename every readme.md in a tree to home.md (-execdir runs in each file's own directory)
find . -type f -name 'readme.md' -execdir mv -- {} home.md \;
Add echo before destructive commands to preview them. See rename.
Subcommand dispatch with case¶
The classic SysV init-script shape is still a handy skeleton for any script that takes a verb. For an actual service, write a systemd unit instead.
#!/bin/bash
set -euo pipefail
PIDFILE=/run/myapp/myapp.pid
LOGFILE=/var/log/myapp.log
is_running() { [[ -f $PIDFILE ]] && kill -0 "$(<"$PIDFILE")" 2>/dev/null; }
start() {
if is_running; then echo "already running" >&2; return 1; fi
nohup /opt/myapp/server.sh >>"$LOGFILE" 2>&1 &
echo $! > "$PIDFILE"
}
stop() {
if ! is_running; then echo "not running" >&2; return 1; fi
kill -TERM "$(<"$PIDFILE")" && rm -f "$PIDFILE"
}
case "${1:-}" in
start) start ;;
stop) stop ;;
restart) stop || true; start ;;
status) if is_running; then echo "running"; else echo "stopped"; exit 3; fi ;;
*) echo "Usage: ${0##*/} {start|stop|restart|status}" >&2; exit 1 ;;
esac
Copy a file with a backup suffix (brace expansion)¶
cp config.txt{,.bak} # config.txt.bak
cp config.txt{,.bak.$(date +%Y%m%d.%H%M)} # dated backup
cp config.txt{,.bak.$(date +%Y%m%d)} ~/backup/ # ... into a directory (put the destination last)
Awkward file and directory names¶
cd -- '-[ Shaman' # names starting with a dash: use --
ls | grep -v '\.txt$' # not ending in .txt
shopt -s extglob; printf '%s\n' !(*.txt) # extended glob for the same
ls -d *Arcade* # -d lists matching directories without descending into them
ls {745..769}* # number ranges in names (swap ls for scp to copy them)
A directory whose name contains newlines can be removed by pasting exactly the same quoted string that created it, or by matching with a glob: rm -rf ./Introducing*. Inspect with ls -b.
Subshells¶
( ... ) runs commands in a child shell. Changes to variables and the working directory do not leak out, which is handy for cd inside a script ((cd /tmp && tar xf x.tar)). $( ... ) is command substitution, which also runs in a subshell. Overview: Linux subshells for beginners.
Pretty-print a messy CSV¶
column -t -s, file.csv | less -S
Compare directories on two hosts¶
diff -b <(ssh host1 'ls -l /srv/app') <(ssh host2 'ls -l /srv/app')
String manipulation and parameter expansion¶
References: Advanced Bash Guide: string manipulation, Advanced Bash Guide: parameter substitution, Bash parameter expansion (opensource.com), Bash reference manual: shell parameter expansion.
f="report.final.txt"
echo "${f%.txt}" # report.final (remove suffix, shortest)
echo "${f%%.*}" # report (remove suffix, longest)
echo "${f#*.}" # final.txt (remove prefix, shortest)
echo "${f##*.}" # txt (extension)
echo "${f/final/v2}" # report.v2.txt
echo "${f^^}" # REPORT.FINAL.TXT (uppercase)
echo "${#f}" # 16 (length)
echo "${f:0:6}" # report (substring)
echo "${var:-default}" # value or default if unset/empty
echo "${PWD##*/}" # name of the current directory (like basename "$PWD")
Directory colours in ls¶
ls colours come from LS_COLORS. Override entries in ~/.bashrc, then source ~/.bashrc:
export LS_COLORS="$LS_COLORS:di=1;33:ln=36" # directories bold yellow, symlinks cyan
For a full custom scheme, dump the defaults, edit, and load them (most distros' .bashrc already runs dircolors if ~/.dircolors exists):
dircolors -p > ~/.dircolors
eval "$(dircolors -b ~/.dircolors)"
On Fedora/RHEL the system-wide file is /etc/DIR_COLORS:
#DIR 01;34 # default: bold blue
DIR 32 # plain green
A value is style;foreground;background, separated by semicolons, e.g. di=1;4;31;42 is bold, underlined red on green.
| Style | Foreground | Background | |||
|---|---|---|---|---|---|
| 0 | default | 30 / 90 | black / dark grey | 40 / 100 | black / dark grey |
| 1 | bold | 31 / 91 | red / light red | 41 / 101 | red / light red |
| 4 | underline | 32 / 92 | green / light green | 42 / 102 | green / light green |
| 5 | blink | 33 / 93 | brown / yellow | 43 / 103 | brown / yellow |
| 7 | reverse | 34 / 94 | blue / light blue | 44 / 104 | blue / light blue |
| 8 | concealed | 35 / 95 | purple / light purple | 45 / 105 | purple / light purple |
| 36 / 96 | cyan / turquoise | 46 / 106 | cyan / turquoise | ||
| 37 / 97 | grey / white | 47 / 107 | grey / white |
Common keys: di directory, fi regular file, ln symlink (target colours it like its target), or broken symlink, mi missing link target, ex executable, pi FIFO, so socket, bd/cd block/char device, su/sg setuid/setgid, st sticky dir, ow other-writable dir (often unreadable by default; worth recolouring), tw sticky + other-writable, *.ext any extension.
Preview every combination in your terminal:
for i in 00{2..8} {0{3,4,9},10}{0..7}; do
for j in 0 1; do echo -e "$j;$i \e[$j;${i}mSample text\e[00m"; done
done
Sources: Ask Ubuntu: change directory colour with ls, LS_COLORS reference.
Quiet logins¶
touch ~/.hushlogin suppresses the MOTD and "last login" banner, leaving just the prompt.
Documentation and further reading¶
Primary
- man bash: one of the longest man pages in existence, and the definitive description of the shell
- GNU bash manual, available as single-page HTML, PDF, or locally via info bash
- Source repository and release tarballs and changelogs
Builtins specifically
help # list all builtins
help cd # help for one builtin
help set # all set options
help shopt # all shopt options
help is the fastest reference for builtin syntax, quicker than searching man bash when you only need to check a flag.
Chet Ramey's pages - Bash home page: current source, documentation, and the official bash FAQ
Community references (often more practical than the manual)
- BashGuide (Greg's wiki): the most respected community guide
- BashPitfalls: a catalogue of common mistakes with explanations
- BashFAQ: community FAQ on Greg's wiki (separate from Chet's)
- Advanced Bash-Scripting Guide: very comprehensive, but some examples use older style; treat it as reference, not gospel
- ShellCheck: static analysis for shell scripts, online or as the local shellcheck tool
Books - Learning the bash Shell by Cameron Newham (O'Reilly): introductory reference - bash Cookbook by Carl Albing and JP Vossen (O'Reilly): practical recipes - The Linux Command Line by William Shotts: free online, covers bash thoroughly in a practical context - Wicked Cool Shell Scripts by Dave Taylor and Brandon Perry (No Starch Press): practical scripts. It is commercial, so buy it from the publisher instead of keeping copies of the ebook files here.
Portability and POSIX
- POSIX Shell Command Language: the shell grammar and special builtins specification, the definitive reference for what is portable versus bash-specific
- dash: reading what dash implements versus what bash adds is a fast way to learn the POSIX/bash divide. Run checkbashisms (from Debian's devscripts) to find bash-only syntax in #!/bin/sh scripts.
Cheatsheets, style, and libraries - Google Shell Style Guide - Bash cheatsheet (devhints) and Learn X in Y minutes: bash - Function libraries: shelf, applemcg/bash, Shell functions library (cyberciti), Bash shell scripting libraries - terminals-are-sexy, a curated list of terminal tools and resources - Related: xargs, parallel, find, text processing tools