The Ultimate Bash Scripting Tutorial: From Beginner to Advanced

Mohammad AmanMohammad Aman
10 min read

Bash scripting is a powerful tool for automating tasks, managing systems, and boosting your productivity as a developer. Whether you're a command-line newbie or a seasoned coder, this guide will take you from Bash basics to advanced scripting with practical examples, tips, and resources.

What is Bash?

Bash (Bourne Again SHell) is a Unix shell and command language. It's the default shell on:

  • Linux distributions

  • macOS (until Catalina, now Zsh)

  • Windows Subsystem for Linux (WSL)

  • Most Unix-like systems

Why Learn Bash Scripting?

  • ๐Ÿš€ Automation: Save hours by scripting repetitive tasks

  • ๐Ÿ›  System Administration: Essential for DevOps and sysadmins

  • ๐Ÿ’ป Developer Productivity: Combine tools and process data quickly

  • ๐Ÿ“Š Data Processing: Powerful text manipulation capabilities

Getting Started

Your First Bash Script

  1. Create a new file:

     touch hello.sh
    
  2. Add the following content:

     #!/bin/bash
     # This is a comment
     echo "Hello, World!"
    
  3. Make it executable:

     chmod +x hello.sh
    
  4. Run it:

     ./hello.sh
    

Key Components:

  • #!/bin/bash (Shebang line) tells the system to use Bash

  • # indicates comments

  • echo prints text to the terminal

Pro Tip: Always include the shebang line for portability, even though Bash is often the default.

Variables and Parameters

Basic Variables

name="Mohammad"  # No spaces around =
age=10
greeting="Good morning, $name!"

Accessing Variables:

echo "$name is $age years old."  # Mohammad is 10 years old.
echo "Message: $greeting"       # Good morning, Mohammad!

Command Substitution

Store command output in a variable:

current_date=$(date)
echo "Today is $current_date"

# Alternative syntax
files=`ls`
echo "Files: $files"

Positional Parameters

Access command-line arguments:

#!/bin/bash
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "Number of arguments: $#"

Run with:

./script.sh arg1 arg2 arg3

Special Variables

VariableDescription
$0Script name
$1-$9Positional arguments
$#Number of arguments
$@All arguments as separate strings
$*All arguments as single string
$?Exit status of last command
$$Process ID (PID) of script
$!PID of last background command

Using getopts for Flags

Handle command-line options professionally:

#!/bin/bash

while getopts "f:d:v" opt; do
  case $opt in
    f) file="$OPTARG" ;;
    d) dir="$OPTARG" ;;
    v) verbose=true ;;
    \?) echo "Invalid option: -$OPTARG" >&2
        exit 1 ;;
  esac
done

echo "File: ${file:-not specified}"
echo "Directory: ${dir:-not specified}"
echo "Verbose: ${verbose:-false}"

Usage:

./script.sh -f data.txt -d /home/user -v

Best Practices:

  • Always quote variables: "$var"

  • Use ${var:-default} for default values

  • Prefer $@ over $* to preserve argument separation

Control Structures

Conditionals (if/elif/else)

Basic Syntax:

if [[ condition ]]; then
  # commands
elif [[ another_condition ]]; then
  # commands
else
  # commands
fi

Common Test Operators:

OperatorDescriptionExample
-eqEqual[[ "$a" -eq "$b" ]]
-neNot equal[[ "$a" -ne "$b" ]]
-ltLess than[[ "$a" -lt "$b" ]]
-gtGreater than[[ "$a" -gt "$b" ]]
-zString is empty[[ -z "$str" ]]
-nString is not empty[[ -n "$str" ]]
\==String equality[[ "$str1" == "$str2" ]]
\=~Regex match[[ "$str" =~ ^[0-9]+$ ]]
-eFile exists[[ -e "file.txt" ]]
-dDirectory exists[[ -d "/path" ]]

Example:

#!/bin/bash

read -p "Enter temperature (ยฐC): " temp

if [[ "$temp" -gt 30 ]]; then
  echo "It's hot outside! ๐Ÿฅต"
elif [[ "$temp" -lt 10 ]]; then
  echo "It's cold outside! โ„๏ธ"
else
  echo "It's pleasant weather. ๐ŸŒž"
fi

Case Statements

#!/bin/bash

read -p "Enter your favorite fruit: " fruit

case "$fruit" in
  apple)
    echo "Apples are crunchy!"
    ;;
  banana)
    echo "Bananas are rich in potassium!"
    ;;
  orange|lemon)
    echo "Citrus fruits are vitamin C rich!"
    ;;
  *)
    echo "I don't know about $fruit"
    ;;
esac

Loops

For Loop:

# Basic for loop
for i in {1..5}; do
  echo "Number: $i"
done

# C-style for loop
for ((i=0; i<5; i++)); do
  echo "Count: $i"
done

# Iterate over files
for file in *.txt; do
  echo "Processing $file"
done

While Loop:

count=1
while [[ $count -le 5 ]]; do
  echo "Count: $count"
  ((count++))
done

# Reading file line by line
while IFS= read -r line; do
  echo "Line: $line"
done < "file.txt"

Until Loop:

attempt=1
until ping -c1 example.com &>/dev/null; do
  echo "Attempt $attempt: Waiting for connection..."
  ((attempt++))
  sleep 1
done
echo "Connection established!"

Loop Control:

  • break: Exit the loop

  • continue: Skip to next iteration

for i in {1..10}; do
  if [[ $i -eq 5 ]]; then
    continue
  fi
  if [[ $i -eq 8 ]]; then
    break
  fi
  echo "Number: $i"
done

Functions

Basic Function

greet() {
  local name="$1"  # Local variable
  echo "Hello, $name!"
}

greet "Alice"  # Output: Hello, Alice!
greet "Bob"    # Output: Hello, Bob!

Returning Values

add() {
  local sum=$(( $1 + $2 ))
  echo "$sum"  # Output the result
}

result=$(add 3 5)
echo "3 + 5 = $result"  # 3 + 5 = 8

Function Parameters

create_user() {
  local username="$1"
  local shell="${2:-/bin/bash}"  # Default value

  echo "Creating user $username with shell $shell"
  # useradd "$username" -s "$shell"
}

create_user "Mohammad"
create_user "Aman" "/bin/zsh"

Best Practices:

  • Use local for function variables to avoid side effects

  • Return status codes (0 for success) with return

  • Use echo to "return" data

  • Document functions with comments

Error Handling

Exit Codes

Every command returns an exit code (0 = success, non-zero = error):

mkdir /non_existent_dir
if [[ $? -ne 0 ]]; then
  echo "Failed to create directory!" >&2
  exit 1
fi

The set Command

Control script behavior:

set -e      # Exit immediately if a command fails
set -u      # Treat unset variables as an error
set -o pipefail  # Fail if any command in a pipeline fails
set -x      # Print commands before execution (debugging)

Trapping Signals

cleanup() {
  echo "Cleaning up..."
  rm -f tempfile.txt
}

trap cleanup EXIT INT TERM  # Run on exit or interrupt

Logging

log() {
  local message="$1"
  local level="${2:-INFO}"
  local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
  echo "[$timestamp] [$level] $message" >> script.log
}

log "Starting script execution"
log "Error encountered" "ERROR"

Advanced Topics

Arrays

# Indexed arrays
fruits=("apple" "banana" "cherry")
echo ${fruits[0]}  # apple
echo ${fruits[@]}  # all elements
echo ${#fruits[@]} # array length

# Adding elements
fruits+=("date")

# Associative arrays (Bash 4+)
declare -A user
user["name"]="Sakir"
user["age"]=20
echo "${user["name"]} is ${user["age"]} years old."

String Manipulation

str="Hello World"

# Substrings
echo ${str:0:5}    # Hello
echo ${str:6}      # World

# Replacement
echo ${str/World/Everyone}  # Hello Everyone
echo ${str//l/L}   # HeLLo WorLd (global replacement)

# Case conversion
echo ${str^^}      # HELLO WORLD
echo ${str,,}      # hello world

Arithmetic Operations

a=5
b=3

# Using $(( ))
echo $((a + b))    # 8
echo $((a * b))    # 15
echo $((a++))      # 5 (then a becomes 6)

# Using let
let "sum = a + b"
echo $sum          # 9 (if a is now 6)

# Using expr (older syntax)
echo $(expr $a + $b)

Process Substitution

# Compare outputs
diff <(ls dir1) <(ls dir2)

# Process multiple files
paste <(cut -f1 file1) <(cut -f2 file2) > output.txt

Here Documents

cat <<EOF
This is a multi-line
text block that preserves
formatting and variables like $HOME
EOF

# With variable expansion disabled
cat <<'EOF'
No variable expansion here $HOME
EOF

Performance Tips

  • Avoid unnecessary subshells:

      # Slow
      count=$(ls | wc -l)
    
      # Faster
      files=(*)
      count=${#files[@]}
    
  • Use built-in string operations instead of external commands:

      # Instead of:
      uppercase=$(echo "$str" | tr '[:lower:]' '[:upper:]')
    
      # Use:
      uppercase="${str^^}"
    
  • Batch file operations:

      # Instead of:
      for file in *.txt; do
        process "$file"
      done
    
      # Consider:
      find . -name "*.txt" -exec process {} \;
    
  • Use printf instead of echo for complex output:

      printf "Name: %-20s Age: %3d\n" "$name" "$age"
    

Security Best Practices

  • Always quote variables:

      rm "$file"  # Safe
      rm $file    # Dangerous (filename with spaces becomes multiple arguments)
    
  • Validate input:

      if [[ ! "$input" =~ ^[a-zA-Z0-9_]+$ ]]; then
        echo "Invalid input" >&2
        exit 1
      fi
    
  • Use mktemp for temporary files:

      tempfile=$(mktemp /tmp/script.XXXXXX)
      echo "Data" > "$tempfile"
    
  • Avoid eval - it can execute arbitrary code:

      # Dangerous!
      eval "$user_input"
    
  • Run with least privileges:

      # Use sudo only when necessary
      sudo chown root:root /important/file
    

Real-World Script Examples

System Backup Script

#!/bin/bash
# backup.sh - Automated system backup

set -euo pipefail

BACKUP_DIR="/backups/$(date +%Y-%m-%d_%H-%M-%S)"
LOG_FILE="/var/log/backup.log"

mkdir -p "$BACKUP_DIR"

log() {
  echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log "Starting backup to $BACKUP_DIR"

# Backup home directories
for user in /home/*; do
  if [[ -d "$user" ]]; then
    username=$(basename "$user")
    log "Backing up $username"
    tar -czf "$BACKUP_DIR/$username.tar.gz" "$user"
  fi
done

# Backup system configurations
log "Backing up system configurations"
tar -czf "$BACKUP_DIR/etc.tar.gz" /etc

log "Backup completed successfully"

Log Analyzer

#!/bin/bash
# log_analyzer.sh - Analyze log files

if [[ $# -eq 0 ]]; then
  echo "Usage: $0 <logfile> [error_level]"
  exit 1
fi

LOG_FILE="$1"
ERROR_LEVEL="${2:-ERROR}"

if [[ ! -f "$LOG_FILE" ]]; then
  echo "Error: File $LOG_FILE not found" >&2
  exit 1
fi

echo "Analyzing $LOG_FILE for $ERROR_LEVEL messages"

declare -A error_counts

while IFS= read -r line; do
  if [[ "$line" =~ \[([^]]+)\] ]]; then
    timestamp="${BASH_REMATCH[1]}"
    date_part="${timestamp%% *}"

    if [[ "$line" =~ $ERROR_LEVEL ]]; then
      ((error_counts["$date_part"]++))
    fi
  fi
done < "$LOG_FILE"

echo -e "\nError Count by Date:"
for date in "${!error_counts[@]}"; do
  printf "%s: %d\n" "$date" "${error_counts[$date]}"
done | sort

Cheat Sheet

Variable Manipulation

SyntaxDescription
${var}Variable value
${var:-default}Use default if var is unset
${var:=default}Set default if var is unset
${#var}Length of string
${var:position:length}Substring
${var/pattern/repl}Replace first match
${var//pattern/repl}Replace all matches

Test Operators

OperatorDescription
-eFile exists
-fRegular file
-dDirectory
-rReadable
-wWritable
-xExecutable
-zString is empty
-nString is not empty

Arithmetic

OperatorDescription
+Addition
-Subtraction
*Multiplication
/Division
%Modulus
**Exponentiation

Resources

Documentation

Tools

Books

Communities

Conclusion

Bash scripting is a must-have skill for developers and system administrators. Start with simple automation tasks and gradually incorporate advanced features as you gain confidence. Key takeaways:

  • Test thoroughly: Especially for scripts that modify files or systems

  • Add comments: Document your code for future you

  • Follow conventions: Make your scripts readable and maintainable

  • Share your work: Contribute to open source or publish your utilities

Next Steps:

  • Explore awk and sed for advanced text processing

  • Learn about cron for scheduling scripts

  • Dive into shell scripting frameworks like Bash Infinity

Happy scripting! ๐Ÿš๐Ÿ’ป

Discussion Prompt: What's your favorite Bash scripting trick? Share in the comments below! ๐Ÿ‘‡

0
Subscribe to my newsletter

Read articles from Mohammad Aman directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mohammad Aman
Mohammad Aman

Full-stack developer with a good foundation in frontend, now specializing in backend development. Passionate about building efficient, scalable systems and continuously sharpening my problem-solving skills. Always learning, always evolving.