Bash Scripting Cheat Sheet: Variables, Loops, Functions and More
A practical reference for writing Bash scripts, from the first line to loops and functions. Everything here works in Bash 4+ (Ubuntu, Debian, RHEL and most others).
Your first script
#!/usr/bin/env bash
echo "Hello, $USER"
chmod +x hello.sh
./hello.sh
The first line (the shebang) tells the system which interpreter to use. #!/usr/bin/env bash finds Bash wherever it is installed.
Variables
name="Alice" # NO spaces around =
echo "Hello, $name" # double quotes expand variables
echo 'Hello, $name' # single quotes do not
echo "${name}_backup" # braces when text follows the name
today=$(date +%F) # capture a command's output
count=$((2 + 3)) # arithmetic
Always quote variables: "$name". Unquoted values break on spaces and wildcards.
Defaults and checks:
echo "${PORT:-8080}" # use 8080 if PORT is unset or empty
: "${API_KEY:?API_KEY is required}" # abort with a message if missing
Arguments
echo "Script: $0"
echo "First arg: $1"
echo "All args: $@" # use "$@" to preserve each argument
echo "Number of args: $#"
shift # drop $1, so $2 becomes $1
Conditions
if [[ -f "$file" ]]; then
echo "file exists"
elif [[ -d "$file" ]]; then
echo "it's a directory"
else
echo "nothing there"
fi
Use [[ ... ]] in Bash instead of the old [ ... ]; it's safer with empty values and supports &&, ||, and pattern matching.
| Test | True when |
|---|---|
-f f / -d f / -e f |
regular file / directory / exists |
-r f / -w f / -x f |
readable / writable / executable |
-s f |
file exists and isn't empty |
-z "$s" / -n "$s" |
string is empty / not empty |
"$a" == "$b" |
strings equal |
"$s" == *.txt |
glob match |
"$s" =~ ^[0-9]+$ |
regex match |
$a -eq $b, -ne, -lt, -le, -gt, -ge |
numeric compare |
Numbers are also easy with (( )):
if (( count > 10 )); then echo "many"; fi
case
case "$1" in
start) echo "starting" ;;
stop) echo "stopping" ;;
restart|reload) echo "restarting" ;;
*) echo "Usage: $0 {start|stop|restart}" >&2; exit 1 ;;
esac
Loops
for i in 1 2 3; do echo "$i"; done
for i in {1..5}; do echo "$i"; done
for f in *.log; do gzip "$f"; done # quote "$f" in the command
while read -r line; do
echo "Line: $line"
done < file.txt # read a file line by line
until ping -c1 -W1 example.com >/dev/null; do sleep 2; done
Functions
log() {
local level="$1"; shift # local keeps variables inside the function
echo "$(date '+%F %T') [$level] $*"
}
is_root() { [[ $EUID -eq 0 ]]; }
log INFO "starting"
is_root || { log ERROR "run as root"; exit 1; }
A function returns an exit status (0 = success). To return text, echo it and capture with $(...).
Exit codes and chaining
Every command returns a number: 0 means success, anything else means failure. $? holds the last one.
mkdir /tmp/x && echo "created" # run second only if first succeeded
grep -q root /etc/passwd || echo "no" # run second only if first failed
cmd1; cmd2 # run both regardless
exit 0
Arrays and strings
files=(a.txt b.txt "my file.txt")
echo "${files[0]}" # first element
echo "${#files[@]}" # length
for f in "${files[@]}"; do echo "$f"; done
path="/var/log/syslog.1"
echo "${path##*/}" # syslog.1 (strip longest prefix up to /)
echo "${path%.*}" # /var/log/syslog (strip shortest suffix .*)
echo "${path/log/LOG}" # replace first match
echo "${#path}" # length
echo "${path^^}" # UPPERCASE
Redirects and pipes
cmd > out.txt # overwrite stdout
cmd >> out.txt # append
cmd 2> err.txt # stderr only
cmd > all.txt 2>&1 # stdout and stderr together
cmd > /dev/null 2>&1 # discard everything
cmd1 | cmd2 # pipe stdout into the next command
Here-docs and input
cat > /etc/myapp.conf <<EOF
port=$PORT
EOF
read -r -p "Continue? [y/N] " answer
[[ "$answer" =~ ^[Yy]$ ]] || exit 0
Quote the delimiter (<<'EOF') to stop variable expansion inside the block.
Next step
Scripts run as a service or from cron deserve extra safety. See Write safer Bash scripts.