CTCaliberTek
Cheat Sheets

Shell Scripting Cheat Sheet: Bash Variables, Tests, Loops, Functions, and Text Tools

Published November 10, 2026·Updated November 10, 2026·10 min read

Edited by , Senior Data Analyst. How we make our content

Like SaveLog in to like, save, and track progress.

Introduction

Use this Bash cheat sheet when you need the syntax fast. Each section is a compact reference you can copy and adapt, with a short note on the mistakes that most often cause trouble. It targets Bash 4 or newer on Linux, and marks the places where macOS, which ships Bash 3.2, or plain sh behaves differently. For explanations, exercises, and complete projects, follow the Shell Scripting course in the Programming section of this site.

Prerequisites

  • A Linux or macOS terminal, or Windows with WSL
  • Bash 4 or newer for associative arrays and case conversion

Step-by-Step Guide

Step 1: Script skeleton and running a script

Start every script with a shebang line so the system knows which interpreter to use, then turn on strict mode so errors are not ignored silently. Make the file executable once with chmod and run it with a path. A script runs in its own process, so it cannot change the working directory or variables of the terminal that started it unless you use source. Keep the logic in a main function and pass along the arguments at the bottom.

#!/usr/bin/env bash
set -euo pipefail

main() {
  echo "Hello, $1"
}
main "$@"

# run it
chmod +x script.sh
./script.sh World          # run as a program
bash script.sh World       # run without the execute bit
source script.sh           # run inside the current shell (also: . script.sh)
bash -n script.sh          # syntax check only
bash -x script.sh          # trace every command

Step 2: Variables and special parameters

Assign without spaces around the equals sign and read with a dollar sign. Use braces when text follows the name. The special parameters describe the arguments and the result of the last command. Prefer the quoted at-sign form to pass every argument along unchanged, because it keeps arguments that contain spaces intact. The exit status of the last command is in the question mark parameter, and zero always means success.

name="Ada"; count=3
echo "$name has ${count} items"
readonly SITE="calibertek.com"
export APP_ENV=production        # pass to child processes

$0        # script name            $1 $2 ...  arguments
$#        # number of arguments    "$@"       all arguments, each one a separate word
$?        # exit status of the last command
$$        # process ID of the shell
$!        # process ID of the last background job

Step 3: Quoting and substitution

Double quotes allow expansion but keep the value as one word, single quotes keep everything literal, and the dollar-parenthesis form captures the output of a command. Unquoted expansions are split on whitespace and matched against files, which is the root of many bugs, so quote by default. Arithmetic uses double parentheses and works on whole numbers only. Use awk or bc when you need decimals.

"$var"            # expanded, stays one word (use this by default)
'$var'            # literal text, nothing is expanded
today=$(date +%F)                 # command substitution
echo $(( 7 * 6 ))                 # 42  integer arithmetic
count=$(( count + 1 ))
awk 'BEGIN { printf "%.2f\n", 7/3 }'    # decimals: 2.33

diff <(sort a.txt) <(sort b.txt)   # process substitution (Bash)
echo {a,b,c}.txt                   # brace expansion: a.txt b.txt c.txt
echo {1..5}                        # 1 2 3 4 5

Step 4: Parameter expansion

Parameter expansion transforms values without starting another program. Defaults make arguments optional, the question-mark form makes them required, and the pound and percent forms trim prefixes and suffixes, which is how you get a file name or an extension. Case conversion and substring features need Bash 4 or newer for some forms.

${var:-default}      # use default if var is unset or empty
${var:=default}      # same, and also assign it to var
${var:?message}      # exit with message if var is unset or empty
${#var}              # length of the value
${var#prefix}   ${var##prefix}     # remove shortest / longest prefix match
${var%suffix}   ${var%%suffix}     # remove shortest / longest suffix match
${var/old/new}  ${var//old/new}    # replace first / every match
${var:2:3}           # substring: 3 characters starting at offset 2
${var^^}  ${var,,}   # uppercase / lowercase (Bash 4 and newer)

file="/data/report.final.txt"
echo "${file##*/}"   # report.final.txt   (file name)
echo "${file%.*}"    # /data/report.final (without the extension)
echo "${file##*.}"   # txt                (extension)

Step 5: Tests and conditions

Use the double-bracket test in Bash, because it handles unquoted variables, patterns, and regular expressions. Compare numbers with the numeric operators or with double parentheses, since the less-than symbol inside double brackets compares text. Spaces inside brackets are required. Combine tests with and, or, and not, and remember that the condition of an if is just a command whose exit status is checked.

[[ -e f ]]  exists      [[ -f f ]]  regular file     [[ -d d ]]  directory
[[ -r f ]]  readable    [[ -w f ]]  writable          [[ -x f ]]  executable
[[ -s f ]]  not empty   [[ -L f ]]  symbolic link

[[ -z $s ]]  empty      [[ -n $s ]]  not empty
[[ $a == $b ]]  [[ $a != $b ]]  [[ $s == pre* ]]  [[ $s =~ ^[0-9]+$ ]]
[[ $n -eq 5 ]]  -ne  -lt  -le  -gt  -ge        (( n >= 5 && n < 10 ))
[[ -f $f && -r $f ]]    [[ -d $a || -d $b ]]   [[ ! -e $f ]]

if grep -q "error" app.log; then echo "found"; else echo "clean"; fi
command -v git > /dev/null || { echo "git missing" >&2; exit 1; }

Step 6: if and case

An if statement ends with fi and can use any command as its condition. The case statement matches a value against patterns, ends each branch with a double semicolon, uses a vertical bar for alternatives, and treats a lone asterisk as the default. Avoid writing a && b || c as a replacement for if and else, because the last command also runs when b fails.

if [[ $n -gt 10 ]]; then
  echo "big"
elif [[ $n -gt 5 ]]; then
  echo "medium"
else
  echo "small"
fi

case "$1" in
  start|run) echo "starting" ;;
  stop)      echo "stopping" ;;
  *)         echo "usage: $0 {start|run|stop}" >&2; exit 2 ;;
esac

Step 7: Loops

A for loop works on lists, wildcards, ranges, and counters. Use a wildcard instead of parsing the output of ls, and skip the literal pattern when nothing matches. A while loop with read processes a file line by line, and setting IFS splits each line into fields. A loop at the end of a pipe runs in a subshell, so variables set inside it are lost. Feed it with a redirect or process substitution when you need them afterwards.

for x in a b c; do echo "$x"; done
for f in *.txt; do [[ -e $f ]] || continue; echo "$f"; done
for i in {1..5}; do echo "$i"; done
for ((i = 0; i < 3; i++)); do echo "$i"; done

while (( n < 5 )); do n=$(( n + 1 )); done
until ping -c 1 example.com > /dev/null 2>&1; do sleep 5; done

while IFS= read -r line; do echo "$line"; done < file.txt
while IFS=, read -r a b; do echo "$a / $b"; done < data.csv
while read -r x; do total=$(( total + x )); done < <(cut -d' ' -f1 sizes.txt)

break       # leave the loop        continue    # next iteration

Step 8: Arrays and associative arrays

Indexed arrays keep each element intact, including elements with spaces. Always expand them with the quoted at-sign form. Associative arrays map text keys to values and must be declared first, and they require Bash 4 or newer, so they are missing from the default shell on macOS. The order of keys is not guaranteed.

arr=(one "two words" three)
arr+=(four)
echo "${arr[0]}"            # one
echo "${arr[@]}"            # all elements
echo "${#arr[@]}"           # number of elements
echo "${!arr[@]}"           # indexes
for item in "${arr[@]}"; do echo "$item"; done
unset 'arr[1]'

declare -A age=([ada]=36 [grace]=45)
age[linus]=28
echo "${age[ada]}"
for key in "${!age[@]}"; do echo "$key=${age[$key]}"; done

Step 9: Functions

Define a function before you call it. Inside it, arguments arrive as $1, $2, and so on, and local variables keep names from leaking out. The return command sets a status from 0 to 255 for use in conditions. To give back text, print it and capture the output with command substitution, and send log messages to standard error so they are not captured as part of the result.

greet() {
  local who=${1:-world}
  echo "Hello, $who"
}
greet Ada
msg=$(greet Linus)

is_file() { [[ -f $1 ]]; }
if is_file notes.txt; then echo "exists"; fi

die() { echo "ERROR: $*" >&2; exit 1; }

Step 10: Redirection, pipes, and here documents

Every command has standard input (0), standard output (1), and standard error (2). Redirect them with the angle brackets, join them with 2>&1, and connect commands with the pipe. Put 2>&1 after the file redirection, not before it. A here document supplies several lines of input, and quoting the marker stops variable expansion inside it.

cmd > out.txt          # overwrite             cmd >> out.txt    # append
cmd 2> err.txt         # errors only           cmd < in.txt      # read from file
cmd > all.txt 2>&1     # both streams          cmd &> all.txt    # Bash shortcut
cmd > /dev/null 2>&1   # discard everything
echo "oops" >&2        # print to standard error

cmd1 | cmd2 | cmd3     # pipe output to the next command
cmd | tee out.txt      # show it and save it

cat > file.conf <<EOF
host=$HOSTNAME
EOF
cat <<'EOF'
$HOME stays literal here
EOF
grep -c a <<< "banana"   # here string

Step 11: Text processing one-liners

These tools cover most daily text work. Use grep to select lines, cut and awk to pick columns, sort and uniq to rank values, and sed to substitute. The uniq command only merges neighboring lines, so sort first. Editing in place with sed is written differently on macOS than on Linux, so test it where the script will run.

grep -rn "TODO" src/            # search recursively with line numbers
grep -E "err|warn" app.log      # alternatives    grep -v "debug"  # exclude
grep -q "x" f && echo yes       # test only, no output

cut -d, -f1,3 data.csv          # columns 1 and 3
sort -u names.txt               # unique sorted lines
sort file | uniq -c | sort -rn | head -5      # top five values
awk '{ print $1 }' access.log
awk -F, '$3 > 100 { print $1 }' data.csv
awk '{ s += $2 } END { print s }' sales.txt
sed 's/old/new/g' file.txt
sed -n '5,10p' file.txt
sed -i 's/a/b/g' file.txt       # GNU/Linux   (macOS: sed -i '' ...)
tr 'a-z' 'A-Z' <<< "shell"
wc -l file.txt

Step 12: find, xargs, and file operations

The find command walks a tree and filters by name, type, size, and age. Prefer find with print0 and xargs -0 when names may contain spaces. Look at the list of matches before you add delete or an action that changes files. Use the double dash before file names that might start with a hyphen, and quote variables in every path.

find . -name "*.log"
find . -type f -mtime +7                 # older than 7 days
find . -type f -size +100M
find . -name "*.tmp" -delete             # check the list first
find . -name "*.log" -exec gzip {} \;
find . -name "*.txt" -print0 | xargs -0 grep -l "error"

mkdir -p a/b/c       cp -r src dest       mv old new       rm -rf -- "$dir"
ln -s target link    chmod 640 file       chmod +x script.sh
tar -czf out.tar.gz dir/     tar -tzf out.tar.gz     tar -xzf out.tar.gz -C dest/

Step 13: Options, safety, and cleanup

The getopts command parses short options. Strict mode and a trap make scripts fail loudly and clean up after themselves. Use mktemp for temporary files, check that required programs exist before you need them, and run ShellCheck on every script. Note that set -e does not apply inside conditions or before the and and or operators, so check critical steps yourself.

while getopts ":vo:" opt; do
  case $opt in
    v) verbose=1 ;;
    o) out=$OPTARG ;;
    *) echo "usage: $0 [-v] [-o file]" >&2; exit 2 ;;
  esac
done
shift $(( OPTIND - 1 ))

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

command -v jq > /dev/null || { echo "jq required" >&2; exit 1; }
rm -rf -- "${dir:?dir not set}"/*
shellcheck script.sh

Step 14: Scheduling with cron

A crontab line has five time fields and then the command. Cron runs jobs with a minimal environment, so use absolute paths, set PATH at the top, and redirect output to a log file. Use flock to stop two copies of a long job from overlapping. Check that the exact command works in a plain shell before you schedule it.

# minute hour day-of-month month day-of-week  command
  30     2    *            *     *             /home/dev/bin/backup.sh >> /home/dev/backup.log 2>&1
  0      */6  *            *     *             /home/dev/bin/check.sh
  15     9    *            *     1-5           flock -n /tmp/report.lock /home/dev/bin/report.sh

crontab -e     # edit jobs         crontab -l    # list jobs
# fields: minute 0-59, hour 0-23, day 1-31, month 1-12, weekday 0-7 (0 and 7 are Sunday)
# symbols: * every   */5 every five   1-5 range   1,15 list

FAQs

What is the safest way to start a Bash script?
Begin with a bash shebang, enable set -euo pipefail, quote every variable expansion, validate arguments, and run ShellCheck. Put the logic in a main function and call it with "$@" at the bottom so that the whole file is read before anything runs.
When should I use sh instead of bash?
Use sh only when a script must run on minimal systems such as containers or embedded devices, and then avoid Bash-only features such as arrays, double brackets, and here strings. For everything else, Bash is more convenient and safer to write.
Where can I learn each of these topics in depth?
The Shell Scripting course in the Programming section walks through all of them with explanations and a backup script project, and the Linux command line cheat sheet covers files, permissions, and processes.

Summary

Start scripts with a shebang and strict mode, quote every expansion, test with double brackets, loop with for and while read, store lists in arrays, group code into functions, connect commands with pipes and redirection, process text with grep, sed, awk, sort, and uniq, and schedule with cron using absolute paths and logged output. Keep this page open while you write scripts, and use the full course when you need the reasoning behind a pattern.

Related tutorials

Comments

Log in or create a free account to join the discussion.

  • No comments yet. Be the first to comment.