(Home)

Bash Scripting Cheat Sheet

[Jul 15, 25]

The Basics

#!/bin/bash

# ^ Bash scripts should start with a shebang

# < Comments start with `#` symbol
# Comments are ignored by Bash

# Commands can be separated by new lines:
echo "First Command"
echo "Second Command"

# Or using semicolons in the same line:
echo "Third Command"; echo "Fourth Command"

# You can either run the bash script using the bash command:
bash ./script.sh

# Or by making it an executable:
chmod +x ./script.sh
./script.sh

Variables

# Variables are used to store data

# This is the syntax for declaring variables:
variable_name="value"

# No spaces around the equals sign:
var_one="value one"   # < Correct
var_two = "value two" # < Incorrect
# ^ That's because Bash interprets spaces as argument delimiters. This will throw a command-not-found error.

# You can access a variable by prefixing it with `$`:
name="John"
echo "Hello, $name"    # => Hello, John
echo "Hello, ${name}!" # => Hello, John!

# It's best practice to wrap variables in double quotes to prevent word splitting and glob expansion:
filename="My File.txt"
echo "$filename"    # Correct
echo $filename      # Risky: expands to `My` and `File.txt`, breaking commands like `rm $filename`

# Using single quotes won't expand the variable:
echo 'Hello, $name' # => Hello, $name

# Local variables are scoped to the block in which they are used:
greet() {
  local name="John"
  echo "Hello, ${name}!"
}
greet # => Hello, John!

# You can declare constants using `readonly`
# These variables can't be changed:
readonly version="0.0.1"
# or
declare -r pi=3.14

Data Types

name="Alice" # String
age=25 # Number

# Arrays:
friends=("John" "Bob")
friends[2]="Alex"
echo "${friends[0]}" # => John

# Associative Arrays:
# (Requires Bash 4.0+)
declare -A ages
ages[john]=23
ages[bob]=22
echo "John is ${ages[john]} years old" # => John is 23 years old

Conditional Flow

# if statement:
age=22
if [[ "$age" -gt 18 ]]; then
  echo "You are old enough to enter"
fi

# if/else statements:
age=16
if [[ "$age" -gt 18 ]]; then
  echo "You are old enough to enter"
else
  echo "You are not old enough to enter"
fi

# if/elif/else statements:
username="admin"
if [[ "$username" == "admin" ]]; then
  echo "You are the admin"
elif [[ "$username" == "contributor" ]]; then
  echo "You are a contributor"
else
  echo "Unknown user"
fi

# You can nest if statements:
username="admin"
password="secret-password"
if [[ "$username" == "admin" ]]; then
  if [[ "$password" == "secret-password" ]]; then
    echo "You are the admin"
  fi
fi

# You can also use || (or) and && (and):
username="admin"
password="secret-password"
if [[ "$username" == "admin" ]] && [[ "$password" == "secret-password" ]]; then
  echo "You are the admin"
else
  echo "You are not the admin"
fi

Conditional Operators

# String Comparison:
# [[ "$a" == "$b" ]]     # Equal
# [[ "$a" != "$b" ]]     # Not equal
# [[ -z "$a" ]]          # String is empty
# [[ -n "$a" ]]          # String is not empty

# Numeric Comparison:
# [[ $a -eq $b ]]        # Equal
# [[ $a -ne $b ]]        # Not equal
# [[ $a -lt $b ]]        # Less than
# [[ $a -le $b ]]        # Less than or equal
# [[ $a -gt $b ]]        # Greater than
# [[ $a -ge $b ]]        # Greater than or equal

# File Test Operators:
# [[ -e file.txt ]]      # File exists
# [[ -f file.txt ]]      # Is a regular file
# [[ -d dir ]]           # Is a directory
# [[ -r file.txt ]]      # File is readable
# [[ -w file.txt ]]      # File is writable
# [[ -x script.sh ]]     # File is executable
# [[ -s file.txt ]]      # File is not empty

# Logical Operators:
# [[ $a -gt 0 && $b -gt 0 ]]  # AND
# [[ $a -gt 0 || $b -gt 0 ]]  # OR
# [[ ! -e file.txt ]]         # NOT

Case Statement

# The `case` statement allows branching based on pattern matching

read -p "Enter a letter: " letter

case "$letter" in
  [a-z])
    echo "You entered a lowercase letter"
    ;;
  [A-Z])
    echo "You entered an uppercase letter"
    ;;
  [0-9])
    echo "You entered a digit"
    ;;
  *)
    echo "You entered something else"
    ;;
esac

# Another example with strings:
read -p "Enter a command (start/stop/restart): " cmd

case "$cmd" in
  start)
    echo "Starting service..."
    ;;
  stop)
    echo "Stopping service..."
    ;;
  restart)
    echo "Restarting service..."
    ;;
  *)
    echo "Unknown command: $cmd"
    ;;
esac

# Patterns can include wildcards:
filename="document.txt"
case "$filename" in
  *.txt) echo "Text file" ;;
  *.jpg|*.png) echo "Image file" ;;
  *) echo "Other file type" ;;
esac

Loops

# For loops allow you to go through a list or range:
for i in {10..15}; do
  echo "i: ${i}"
done

# Alternative syntax for portability:
for ((i=10; i<=15; i++)); do
  echo "i: ${i}"
done

# While loops allow you to loop as long as a condition is true:
i=0
while [[ "$i" -lt 5 ]]; do
  echo "i: ${i}"
  ((i++))
done

# Until loops run until the condition becomes true (opposite of while):
i=5
until [[ "$i" -eq 0 ]]; do
  echo "i: ${i}"
  ((i--))
done

# `break` exits the entire loop early:
i=5
while true; do
  echo "i: ${i}"
  ((i--))

  if [[ "$i" -eq 0 ]]; then
    break
  fi
done

# `continue` skips the rest of the current iteration and continues with the next:
for i in {1..20}; do
  if [[ $(($i % 2)) -eq 1 ]]; then
    continue
  fi

  echo "i: ${i}"
done

# You can also nest loops:
for i in {1..5}; do
  for j in {5..10}; do
    echo "i: ${i}; j: ${j}"
  done
done

Arrays

# Arrays are used to store multiple values in a single variable

# Declare an indexed array:
fruits=("apple" "banana" "cherry")

# Access individual elements (zero-based indexing):
echo "${fruits[0]}"  # => apple
echo "${fruits[1]}"  # => banana

# Add or update elements:
fruits[3]="date"

# Print all elements:
echo "${fruits[@]}"    # => apple banana cherry date
echo "${fruits[*]}"    # => apple banana cherry date

# Get array length:
echo "${#fruits[@]}"   # => 4

# Loop through an array:
for fruit in "${fruits[@]}"; do
  echo "Fruit: $fruit"
done

# Get all indices:
echo "${!fruits[@]}"   # => 0 1 2 3

# Remove an element:
unset 'fruits[1]'      # removes "banana"
echo "${fruits[@]}"    # => apple cherry date

# Declare an associative array (Bash 4.0+):
declare -A capitals
capitals[France]="Paris"
capitals[Germany]="Berlin"

# Access associative array elements:
echo "${capitals[France]}" # => Paris

# Loop through keys:
for country in "${!capitals[@]}"; do
  echo "$country: ${capitals[$country]}"
done

# Print all values:
echo "${capitals[@]}"

Functions

# Functions let you group reusable blocks of code
say_hello() {
  echo "Hello!"
}

say_hello # => Hello!

# A different syntax:
function say_hi() {
  echo "Hi!"
}

say_hi # => "Hi!"

# Functions can accept arguments (as $1, $2, ...):
greet() {
  echo "Hello, ${1}!"
}

greet "Alice" # => Hello, Alice!


# You can check for the number of arguments:
print_info() {
  echo "Script called with $# arguments"
}

print_info one two # => Script called with 2 arguments

# Local variables:
example() {
  local msg="This is local"
  echo "$msg"
}

example # => This is local

# Returning a value using echo and capturing it:
add() {
  echo $(($1 + $2))
}

sum=$(add 3 5)
echo "Sum: $sum" # => Sum: 8

# Returning an exit status (0 = success, non-zero = failure):
is_even() {
  if (( $1 % 2 == 0 )); then
    return 0  # true
  else
    return 1  # false
  fi
}

is_even 4
if [[ $? -eq 0 ]]; then
  echo "Number is even"
else
  echo "Number is odd"
fi

Special Variables

# $0   – The name of the script
# $1   – First argument
# $2   – Second argument, etc.
# $#   – Number of arguments
# $@   – All arguments as separate words
# $*   – All arguments as a single word
# $?   – Exit status of the last command
# $$   – Process ID of the current script
# $!   – PID of the last background command

Input

# The `read` command is used to take user input

# Basic usage:
read name
echo "Hello, $name"

# Prompting the user with -p:
read -p "Enter your name: " name
echo "Hello, $name"

# Reading multiple variables in one line:
read first last
echo "First: $first, Last: $last"

# Using -s to hide input (e.g., for passwords):
read -s -p "Enter password: " password
echo
echo "Password received"

# Using -n to limit number of characters:
read -n 1 -p "Continue? (y/n): " choice
echo "You entered: $choice"

# With a timeout using -t (in seconds):
read -t 5 -p "Enter something in 5 seconds: " input
echo "You typed: $input"

Error Handling

# Check if a command succeeded using if:
if cp file.txt backup.txt; then
  echo "Backup succeeded"
else
  echo "Backup failed"
fi

# Exit the script immediately if a command fails:
set -e

# You can disable `set -e` using:
set +e

# You can also set a custom exit code:
if [[ ! -f "config.yaml" ]]; then
  echo "Missing config file"
  exit 1
fi

# Catch failures using ||:
mkdir new_folder || echo "Failed to create folder"

# Exit script on undefined variable usage (good for strict mode):
set -u
echo "$undefined_var" # Will cause an error and exit

Command Substitution

# Command substitution lets you capture the output of a command into a variable

# Using $(command)
current_date=$(date)
echo "Today is: $current_date"

# Nesting substitutions:
echo "Files in current directory: $(ls | wc -l)"

# Capturing multiline output:
list=$(ls -1)
echo "File list:"
echo "$list"

# Backticks are also supported but not recommended:
hostname=`hostname`  # Use $(hostname) instead

Debugging Tips

# Use `set -x` to print commands as they are executed:
set -x
echo "Debugging this line"
set +x

# Use `trap` to handle script exits and errors:
trap 'echo "An error occurred on line $LINENO"' ERR

# Display line number on error:
trap 'echo "Error on line $LINENO"; exit 1' ERR

# Print current line number manually:
echo "Current line: $LINENO"

# Use `bash -x` to debug a script when running it:
bash -x script.sh

# Dry-run with `-n` (no exec):
bash -n script.sh  # Check for syntax errors without running the script