CIS120 Linux Fundamentals by Scott Shaper

Bash Script Troubleshooting

Programmers are problem solvers at heart. When you code, you're constantly breaking down complex tasks into logical steps. Troubleshooting is simply an extension of this problem-solving mindset - identifying issues, diagnosing their causes, and implementing solutions.

Everyone's code breaks sometimes - even experienced programmers. This chapter will equip you with the problem-solving tools to diagnose issues, understand error messages, and fix your scripts. Think of troubleshooting as a puzzle to solve, where each error message is a clue that leads you closer to a working solution.

Quick Reference: Troubleshooting Techniques

Technique Description Common Use
Syntax Checking Identify missing quotes, brackets, etc. When script won't run at all
Tracing Watch script execution step-by-step When script runs but behaves unexpectedly
Echo Debugging Display variable values during execution When you need to see data at specific points
Defensive Programming Add safeguards to prevent errors When building robust, reliable scripts
Commenting Out Disable sections of code temporarily When isolating problem areas

Finding Syntax Errors

Syntax errors are like pieces that don't fit in your puzzle. The shell can't understand what you're asking it to do, so it refuses to run your script.

When to Look for Syntax Errors

  • When you see error messages about "unexpected tokens"
  • When your script refuses to run at all
  • After making changes to a previously working script
  • When learning new bash features

Common Syntax Errors

Error Type What Happens How to Prevent
Missing Quotes Shell keeps looking for closing quote Use syntax highlighting; count your quotes
Missing Tokens Commands like if/then are incomplete Use templates or snippets for complex structures
Empty Variables Commands receive incorrect arguments Quote your variables: "$variable"

Example: The Missing Quote Problem

Spot the error in this script:

# Buggy script
#!/bin/bash
echo "Hello, welcome to my script!
echo "Let's get started."

The error message might look like:

/home/student/script.sh: line 5: unexpected EOF while looking for matching `"'
/home/student/script.sh: line 6: syntax error: unexpected end of file

The problem solving:

  1. The error mentions a missing quote (")
  2. Notice the first echo doesn't have a closing quote
  3. The error appears at the end of the file (lines 5-6), not where the actual error is (line 3)

The fix:

# Fixed script
#!/bin/bash
echo "Hello, welcome to my script!"  # Added closing quote
echo "Let's get started."

Example: The Missing Semicolon Problem

# Buggy script
#!/bin/bash
number=1
if [ $number = 1 ] then
    echo "Number equals 1"
else
    echo "Number doesn't equal 1"
fi

The error message:

/home/student/script.sh: line 6: syntax error near unexpected token `else'
/home/student/script.sh: line 6: `else'

The problem solving:

  1. The error points to the else statement
  2. However, the actual problem is in the if statement
  3. The if condition needs either a semicolon (;) or a newline before then

The fix:

# Fixed script
#!/bin/bash
number=1
if [ $number = 1 ]; then  # Added semicolon
    echo "Number equals 1"
else
    echo "Number doesn't equal 1"
fi

Solving Logical Errors

Logical errors are trickier - your script runs without errors, but doesn't do what you expect. It's like having all the puzzle pieces connected, but forming the wrong picture.

When to Look for Logical Errors

  • When your script runs but produces incorrect results
  • When a script works with some inputs but fails with others
  • When loops don't iterate the expected number of times
  • When conditions don't evaluate as expected

Common Logical Errors

Error Type What Happens How to Find It
Off-by-one errors Loops run one time too many or too few Print counter values; check bounds
Incorrect conditions If/then logic fails or always takes one path Echo condition results before decisions
Unexpected data formats Script assumes data it receives has specific format Validate input; print received values

Example: The Always-True Condition Problem

# Buggy script - supposed to check if user input is valid
#!/bin/bash
read -p "Enter a number between 1 and 10: " user_input

# This condition has a logical error
if [ $user_input -ge 1 -o $user_input -le 10 ]; then
    echo "Valid input!"
else
    echo "Invalid input!"
fi

The problem:

This condition is always true! If user_input is 20, it's not >= 1 but it is <= 10. The condition uses OR (-o) when it should use AND (-a).

The fix:

# Fixed script
#!/bin/bash
read -p "Enter a number between 1 and 10: " user_input

# Fixed condition
if [ $user_input -ge 1 -a $user_input -le 10 ]; then
    echo "Valid input!"
else
    echo "Invalid input!"
fi

This example demonstrates how logical operators can be confused, leading to unexpected behavior without any syntax errors.

Defensive Programming

Defensive programming is like wearing a seatbelt when driving - it's not about preventing accidents, but surviving them when they happen. Good scripts anticipate problems before they occur.

When to Use Defensive Programming

  • When your script could receive unexpected input
  • When your script performs potentially dangerous operations (like deleting files)
  • When writing scripts that others will use
  • When your script depends on external resources (files, network, etc.)

Common Defensive Techniques

Technique What It Does Example
Check if files/directories exist Prevents operations on non-existent resources [[ -f "$filename" ]]
Validate command success Only continues if previous commands worked command1 && command2
Quote variables Prevents word splitting and globbing issues "$variable"
Validate input Ensures data meets expected format [[ $input =~ ^[0-9]+$ ]]

Example: The Case of the Dangerous Delete

This script is dangerous - it might delete files you didn't intend to delete:

# Dangerous script
#!/bin/bash
# Script to clean a directory

directory=$1
cd $directory
rm *  # DANGER! What if cd failed?

The problems:

  1. If $directory doesn't exist, cd will fail silently
  2. The script will then delete files in the current directory!
  3. No confirmation or validation occurs before deletion

The safer version:

# Safe script
#!/bin/bash
# Script to clean a directory

directory="$1"  # Quotes prevent word splitting

# Check if directory parameter was provided
if [[ -z "$directory" ]]; then
    echo "Error: No directory specified" >&2
    echo "Usage: $0 directory_name" >&2
    exit 1
fi

# Check if directory exists
if [[ ! -d "$directory" ]]; then
    echo "Error: Directory '$directory' does not exist" >&2
    exit 1
fi

# Try to change to the directory
if ! cd "$directory"; then
    echo "Error: Cannot change to directory '$directory'" >&2
    exit 1
fi

# Ask for confirmation before deleting
echo "About to delete all files in $(pwd)"
read -p "Are you sure? (y/n): " confirm

if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
    echo "Deleting files..."
    rm *
    echo "Done."
else
    echo "Operation cancelled."
fi

This safer version:

  • Quotes the directory parameter to handle spaces
  • Checks if the directory parameter was provided
  • Verifies the directory exists before trying to use it
  • Confirms the directory change was successful
  • Asks for confirmation before deleting
  • Shows where error messages come from by using the script name ($0)

Debugging Techniques

When your script isn't working correctly, you need tools to analyze the problem. These techniques help you observe what's happening inside your script to diagnose the issue.

When to Use Debugging

  • When you're not sure what part of your script is failing
  • When you need to see the values of variables during execution
  • When you want to follow the exact path your script is taking
  • When commands are producing unexpected results

Common Debugging Techniques

Technique What It Does When to Use It
Echo debugging Displays variable values at key points When specific variables have unexpected values
Tracing (set -x) Shows each command as it executes When you need to see the execution flow
Commenting out Temporarily disables code sections When isolating which section causes a problem
Test stubs Replace real commands with echo/test versions When testing potentially destructive operations

Example: The Mysterious Variable Problem

Let's debug a script with echo statements:

# Script with echo debugging
#!/bin/bash
# Calculate a discount

price=$1
discount_rate=$2

echo "DEBUG: price = $price, discount_rate = $discount_rate" # Debug line

discount=$(echo "$price * $discount_rate" | bc -l)
final_price=$(echo "$price - $discount" | bc -l)

echo "DEBUG: discount = $discount, final_price = $final_price" # Debug line

echo "Original price: $price"
echo "Discount: $discount"
echo "Final price: $final_price"

By adding the DEBUG echo statements, you can see exactly what values your variables have at each stage of calculation, helping identify where problems might occur.

Example: The Step-by-Step Analysis

Using trace mode to see every command as it executes:

# Script with tracing
#!/bin/bash
# Process a list of files

echo "Starting file processing..."

# Turn on tracing just for this section
set -x
for file in *.txt; do
    if [[ -f "$file" ]]; then
        lines=$(wc -l < "$file")
        echo "$file has $lines lines"
    fi
done
set +x  # Turn off tracing

echo "Processing complete!"

When run, this will show each command in the loop as it executes, along with variable substitutions. The output might look like:

Starting file processing...
+ for file in '*.txt'
+ [[ -f notes.txt ]]
+ wc -l
+ lines=42
+ echo 'notes.txt has 42 lines'
notes.txt has 42 lines
+ for file in '*.txt'
+ [[ -f report.txt ]]
+ wc -l
+ lines=157
+ echo 'report.txt has 157 lines'
report.txt has 157 lines
+ set +x
Processing complete!

This helps you see exactly which files are being processed, which conditions are being met, and what values are being assigned.

Testing Your Scripts

Testing is like a fire drill - it's better to find problems during practice than during a real emergency. Testing scripts helps catch problems before they cause real damage.

When to Test Scripts

  • After writing a new script
  • After making changes to an existing script
  • Before running a script that performs critical operations
  • When moving a script to a new environment

Testing Techniques

Technique What It Does Best For
Safe mode testing Replace destructive commands with echo Scripts that delete/modify files
Edge case testing Test extreme or unusual inputs Scripts that process user input
Test data Create sample files/data for testing Scripts that process files
Early exit Add exit commands to stop at specific points Testing specific sections of longer scripts

Example: Safe Testing Mode

# Script with safe testing mode
#!/bin/bash
# Script to archive old logs

log_dir="/var/log"
archive_dir="/backup/logs"
days_old=30

# Set to "true" for testing, "false" for real operation
TESTING="true"

find_old_logs() {
    find "$log_dir" -name "*.log" -type f -mtime +$days_old
}

if [[ "$TESTING" == "true" ]]; then
    echo "TEST MODE: Would process these files:"
    find_old_logs
    echo "TEST MODE: Would move files to $archive_dir"
    echo "TEST MODE: Would compress files after moving"
    exit 0 #This prevents the script from going to the end.
fi

# Real operations (only run if TESTING is false)
if [[ ! -d "$archive_dir" ]]; then
    mkdir -p "$archive_dir"
fi

for log_file in $(find_old_logs); do
    base_name=$(basename "$log_file")
    mv "$log_file" "$archive_dir/$base_name"
    gzip "$archive_dir/$base_name"
    echo "Archived: $base_name"
done

This script includes a testing mode that shows what would happen without actually moving or compressing files. By setting TESTING="true", you can safely see which files would be affected before running it for real.

How the test flag works:

  • When TESTING="true", the script enters the first if-block, shows what it would do, then exit 0 terminates the script before reaching the real operations
  • When TESTING="false", the if-condition evaluates to false, the script skips that block and continues to the actual file operations
  • This pattern (early exit) is common in bash for creating "dry run" modes that show what would happen without making actual changes

Tips for Success

  • Always test scripts with sample data before using real data
  • Start with small, working scripts and build up complexity gradually
  • Keep a backup of any files your script might modify
  • Use meaningful variable names that indicate what data they contain
  • Add comments explaining what complex sections of code do
  • Test your error handling by deliberately causing errors
  • Check exit status after critical commands with echo $?

Common Mistakes to Avoid

  • Forgetting to quote variables that might contain spaces
  • Assuming directories or files exist without checking
  • Using rm -rf without verifying what you're deleting
  • Confusing test operators like -eq (numeric) vs = (string)
  • Ignoring error messages and return codes
  • Writing complex one-liners that are hard to debug
  • Using hardcoded paths that might not exist on other systems

Best Practices

  • Add a descriptive comment header to every script explaining its purpose
  • Include usage information that prints when incorrect parameters are provided
  • Redirect error messages to stderr with >&2
  • Use meaningful exit codes (0 for success, non-zero for specific errors)
  • Handle unexpected input gracefully instead of crashing
  • Develop incrementally - write, test, then add more features
  • Use functions to organize code and avoid duplication

Becoming a Problem-Solving Programmer

Troubleshooting is a skill that improves with practice. Each bug you find and fix strengthens your problem-solving abilities and deepens your understanding of how scripts work. Don't get discouraged when your scripts break - even experienced programmers spend much of their time diagnosing and fixing code!

Remember that effective troubleshooting involves systematic analysis - narrowing down where things go wrong until you find the exact issue. With the techniques in this chapter, you'll be well-equipped to solve the programming puzzles in your own bash scripts.