Variables & Strings
Variables
NAME="Alice" # assign (no spaces!)
echo "Hello $NAME" # expand
echo "Hello ${NAME}" # braces: safer
unset NAME # delete
readonly PI=3.14 # constant
export PATH # pass to child procs
String Operations
s="hello world"
echo ${#s} # length: 11
echo ${s:6} # slice: "world"
echo ${s:0:5} # "hello"
echo ${s/hello/hi} # replace first
echo ${s^^} # HELLO WORLD
echo ${s,,} # lowercase
Default Values
${VAR:-default} # use default if unset/null
${VAR:=default} # assign default if unset
${VAR:?error} # error if unset
${VAR:+other} # use other if set
Path / Filename Expansion
f="/home/user/script.sh"
echo ${f##*/} # script.sh (basename)
echo ${f%/*} # /home/user (dirname)
echo ${f##*.} # sh (extension)
echo ${f%.*} # strip extension
Arrays
Indexed Arrays
arr=("a" "b" "c") # declare
echo ${arr[0]} # a
echo ${arr[@]} # all elements
echo ${#arr[@]} # length: 3
arr+=("d") # append
unset 'arr[1]' # delete element
Associative Arrays
declare -A map
map["key"]="value"
map["host"]="localhost"
echo ${map["key"]} # value
echo ${!map[@]} # all keys
echo ${map[@]} # all values
Looping Arrays
for item in "${arr[@]}"; do
echo "$item"
done
# indexed loop
for i in "${!arr[@]}"; do
echo "$i: ${arr[$i]}"
done
Control Flow
if / elif / else
if [[ "$x" == "yes" ]]; then
echo "yes"
elif [[ $x -gt 10 ]]; then
echo "big"
else
echo "other"
fi
Test Operators
# strings
[[ "$a" == "$b" ]] [[ "$a" != "$b" ]]
[[ -z "$a" ]] # empty string
[[ -n "$a" ]] # non-empty
# numbers
[[ $n -eq 5 ]] [[ $n -lt 10 ]]
[[ -f "$f" ]] # is file
[[ -d "$d" ]] # is dir
[[ -x "$f" ]] # executable
Loops
# C-style for
for (( i=0; i<10; i++ )); do
echo $i
done
# while loop
while read -r line; do
echo "$line"
done < file.txt
case Statement
case "$opt" in
start) echo "starting" ;;
stop) echo "stopping" ;;
re*) echo "restart*" ;;
-v|--verbose) VERBOSE=1 ;;
*) echo "unknown" ;;
esac
Functions
Defining & Calling
greet() {
local name="$1"
echo "Hello, $name!"
return 0
}
greet "World"
echo "Exit: $?"
Returning Values
# capture stdout
get_date() { date +"%Y-%m-%d"; }
TODAY=$(get_date)
# nameref (bash 4.3+)
add() {
local -n result=$1
result=$(( $2 + $3 ))
}
add SUM 3 4; echo $SUM # 7
Special Parameters
$0 # script name
$1..$9 # positional args
$# # number of args
$@ # all args (array-safe)
$? # last exit code
$$ # current PID
$! # last background PID
$- # current shell options
I/O & Redirection
Redirects
cmd > file # stdout → file (overwrite)
cmd >> file # stdout append
cmd 2> err # stderr → file
cmd 2>&1 # stderr → stdout
cmd &> file # both → file
cmd < file # stdin from file
cmd << EOF # here-doc
multi line
EOF
cmd <<< "str" # here-string
Pipes & Process Sub
cmd1 | cmd2 # pipe stdout
cmd1 |& cmd2 # pipe stdout+stderr
# process substitution
diff <(sort a) <(sort b)
tee >(gzip > out.gz)
# subshell capture
result=$(echo "hello" | tr a-z A-Z)
printf
printf "%-10s %5d\n" "Name" 42
printf "%08.2f\n" 3.14
printf "%b" "line\n" # interpret \n
printf "%q" "a b" # shell-escape
Arithmetic
Integer Math
(( x = 5 + 3 )) # 8
(( x++ )) # post-increment
(( x **= 2 )) # power
echo $(( 17 % 5 )) # modulo: 2
echo $(( 0xFF )) # hex: 255
echo $(( 2#1010 )) # binary: 10
let & bc
let "x = 5 * 3"
echo $x # 15
# floating point with bc
echo "scale=4; 22/7" | bc
echo "sqrt(2)" | bc -l
Useful Patterns
Error Handling
set -euo pipefail # strict mode
# -e: exit on error
# -u: error on unset vars
# -o pipefail: pipe errors count
trap 'echo "ERR on line $LINENO"' ERR
trap 'cleanup' EXIT # always runs
Script Template
#!/usr/bin/env bash
set -euo pipefail
DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
usage() { echo "Usage: $0 <arg>"; exit 1; }
[[ $# -lt 1 ]] && usage
main() { echo "arg=$1 dir=$DIR"; }
main "$@"
getopts (Arg Parsing)
while getopts ":hv:o:" opt; do
case "$opt" in
h) usage ;;
v) VERBOSE="$OPTARG" ;;
o) OUT="$OPTARG" ;;
\?) echo "Bad opt"; usage ;;
esac
done
shift $(( OPTIND - 1 ))
Common Idioms
# check command exists
command -v docker &>/dev/null || exit 1
# run if not root
[[ $EUID -ne 0 ]] && exec sudo "$0" "$@"
# temp file with cleanup
TMP=$(mktemp)
trap "rm -f $TMP" EXIT
Special Operators
| Operator | Meaning | Example |
|---|---|---|
| && | AND — run if previous succeeds | make && make install |
| || | OR — run if previous fails | cd /tmp || exit 1 |
| ; | Sequence — always run next | cd /; ls |
| & | Background — run async | sleep 10 & |
| | | Pipe stdout to next stdin | cat f | grep x |
| $() | Command substitution | NOW=$(date) |
| $(()) | Arithmetic expansion | echo $((2**10)) |
| {} | Brace expansion | echo {a,b,c}.txt |
| ~ | Home directory | cd ~/projects |
| ! | Negate exit status | if ! grep -q x f; then |
Timeline
1971
Thompson shell (sh) — Ken Thompson writes the first Unix shell for Unix V1 at Bell Labs. It introduces pipes and simple I/O redirection, the core ideas bash inherits.
1979
Bourne Shell (sh)sh — Stephen Bourne rewrites the shell for Unix V7 at Bell Labs. Introduces here-docs, variables, functions, and structured control flow. Still the POSIX baseline.
1983
IEEE POSIX work begins — The 1003.2 committee starts standardizing shell behavior, ensuring portability across Unix variants. Bourne shell becomes the de facto reference.
1987
Brian Fox starts bash — The Free Software Foundation assigns Brian Fox to write a free, open-source replacement for sh. The name "Bourne Again SHell" is a nod to both its origin and the FSF's ethos.
1989
Bash 1.0 releasedv1.0 — January 10, 1989. Ships with GNU/Linux from the start. Adds readline for command editing, job control, and history. Immediately becomes the default shell on Linux.
1993
Chet Ramey takes over — Brian Fox steps back; Chet Ramey at Case Western Reserve University becomes the primary maintainer. He still maintains bash today — over 30 years of stewardship.
1996
Bash 2.0v2.0 — Indexed arrays,
$FUNCNAME, ${!var} indirect expansion, improved getopts, and programmable completion framework. Bash diverges meaningfully from sh.
2004
Bash 3.0v3.0 — Regex matching with
[[ $x =~ pattern ]], BASH_REMATCH capture groups. Apple ships bash 3.x on macOS (it stays there until 2019 due to licensing).
2009
Bash 4.0v4.0 — Associative arrays (
declare -A), ** globstar recursion, coprocesses (coproc), mapfile/readarray. Substantial language expansion.
2014
ShellShockCVE-2014-6271 — Critical RCE vulnerability: bash executes trailing code after function definitions in environment variables. Affects web servers, DHCP clients, SSH, and more. Patched within 24 hours; additional variants patched over following weeks.
2016
Bash 4.4v4.4 —
printf -v improvements, wait -n (wait for any job), ${!name[@]} nameref fix, @Q/@E parameter transformation operators.
2019
Bash 5.0v5.0 —
$EPOCHSECONDS, $EPOCHREALTIME, $BASH_ARGV0, improved associative array handling, history timestamps, and over 70 bug fixes.
2019
macOS switches to zshmacOS Catalina — Apple changes the default login shell from bash 3.2 to zsh. Reason: bash 4+ uses GPL v3 which Apple won't ship; bash 3.2 stays available but deprecated. Bash remains the scripting standard on Linux.
2022
Bash 5.2v5.2 —
${array@K} key-value output, better error messages for common mistakes, noexpand_translation, new GLOBIGNORE patterns. Bug fixes from 5.1.
Why Bash?
Ubiquity
Pre-installed on virtually every Linux distribution and available on macOS. Write a bash script and it runs on your laptop, a cloud VM, a container, or a supercomputer without installing anything.
No Runtime
No virtual machine, no interpreter package, no dependency tree. Bash is a single binary. Your script starts instantly — no JVM warm-up, no package manager, no
pip install.DevOps Standard
GitHub Actions, GitLab CI, Jenkins, CircleCI, and Kubernetes all default to bash for shell steps. Every Dockerfile
RUN layer is bash. It's the lingua franca of automation pipelines.Unix Philosophy
Bash is the glue between tools. Pipe grep into awk into sort into uniq — each program does one thing well, and bash connects them. Text streams as a universal interface.
Process Control
Job control, background tasks (
&), wait, trap for signals, process substitution, coprocesses. Bash can orchestrate complex multi-process workflows natively.Text Processing
Direct integration with grep, sed, awk, cut, tr, sort, uniq. Parse log files, transform CSVs, extract fields — without launching Python or installing libraries.
Rapid Automation
Turn a one-off sequence of commands into a repeatable script in minutes. Cron it. Add error handling with
set -euo pipefail. Zero boilerplate to get something running.Portability
A bash 4+ script runs identically on Ubuntu, CentOS, Debian, Alpine, and any POSIX Linux. Targeting bash 3.2 even covers macOS. Write once, run everywhere Linux exists.
Bash vs. Other Scripting Languages
| Capability | Bash | Python | Node.js |
|---|---|---|---|
| Pre-installed on Linux | ✓ Always | Often (varies) | Rarely |
| Startup time | ~10ms | ~50-100ms | ~100-200ms |
| Pipe / process integration | Native | subprocess module | child_process module |
| Docker / CI default | ✓ Yes | If installed | If installed |
| Complex data structures | Limited | ✓ Rich | ✓ Rich |
| Readability at scale | Degrades | ✓ Excellent | ✓ Good |
bash — ~/projects
(reverse-i-search)`
':
user@myhost:~/projects$
Escape Sequences
| Sequence | Expands to | Example output |
|---|---|---|
| \u | Current username | alice |
| \h | Hostname up to first dot | myhost |
| \H | Full hostname | myhost.local |
| \w | Current working directory (~ abbreviated) | ~/projects/app |
| \W | Basename of current directory | app |
| \$ | $ for normal user, # for root | $ |
| \n | Newline — start a second prompt line | (newline) |
| \t | Current time in HH:MM:SS (24h) | 14:32:07 |
| \T | Current time in HH:MM:SS (12h) | 02:32:07 |
| \@ | Current time in 12h am/pm | 02:32 PM |
| \d | Date as "Weekday Month Date" | Thu Apr 3 |
| \! | History number of this command | 142 |
| \# | Command number in this session | 17 |
| \j | Number of active jobs | 2 |
| \s | Name of the shell | bash |
| \v | Bash version (major.minor) | 5.2 |
| \V | Bash version (full release) | 5.2.15 |
| \[ \] | Wrap non-printing chars (required for colors) | \[\e[32m\] |
ANSI Color Codes
Color Syntax
# format: \[\e[CODEm\] or \[\033[CODEm\]
# reset: \[\e[0m\]
PS1='\[\e[1;32m\]\u\[\e[0m\]@\h:\w\$ '
# ↑ bold green ↑ reset
# multiple attrs: bold + color
PS1='\[\e[1;36m\]\u\[\e[0m\]'
# 1=bold, 36=cyan
Code Reference
# Attributes
0 reset 1 bold 2 dim
4 underline 5 blink 7 reverse
# Foreground colors
30 black 31 red 32 green
33 yellow 34 blue 35 magenta
36 cyan 37 white 90-97 bright
# Background colors: add 10
40 black 41 red 42 green
44 blue 46 cyan 47 white
PS1 Gallery
Minimal
PS1=
'\w\$ '
Preview
~/projects$
Classic
PS1=
'\u@\h:\w\$ '
Preview
user@myhost:~/projects$
Colorful
PS1=
'\[\e[1;32m\]\u\[\e[0m\]@\[\e[36m\]\h\[\e[0m\]:\[\e[34m\]\w\[\e[0m\]\$ '
Preview
user@myhost:~/projects$
With Timestamp
PS1=
'\[\e[2m\][\t]\[\e[0m\] \[\e[32m\]\u@\h\[\e[0m\]:\[\e[34m\]\w\[\e[0m\]\$ '
Preview
[14:32:07] user@myhost:~/projects$
Two-Line
PS1=
'\[\e[32m\]\u@\h\[\e[0m\] \[\e[34m\]\w\[\e[0m\]\n\$ '
Preview
user@myhost ~/projects
$
$
Git-Aware (with __git_ps1)
PS1=
'\[\e[32m\]\u@\h\[\e[0m\]:\[\e[34m\]\w\[\e[33m\]$(__git_ps1 " (%s)")\[\e[0m\]\$ '
Preview (on a git branch)
user@myhost:~/projects (main)$
Dynamic Prompts & PROMPT_COMMAND
Exit Code in Prompt
# show ✓ or ✗ based on last exit code
__status() {
if [[ $? -eq 0 ]]; then
echo '\[\e[32m\]✓\[\e[0m\]'
else
echo '\[\e[31m\]✗\[\e[0m\]'
fi
}
PS1='$(__status) \w\$ '
PROMPT_COMMAND
# runs before each prompt is displayed
PROMPT_COMMAND='history -a'
# appends history to file immediately
# multiple commands
PROMPT_COMMAND='history -a; __set_title'
# set terminal title to current dir
__set_title() {
printf '\e]2;%s\a' "bash: $PWD"
}
PS2, PS3, PS4
# PS2: continuation prompt (default: "> ")
PS2='\[\e[33m\]→ \[\e[0m\]'
# PS3: select statement menu prompt
PS3="Choose: "
# PS4: set -x trace prefix (default: "+ ")
PS4='\[\e[35m\]+${BASH_SOURCE}:${LINENO}: \[\e[0m\]'