Exit Codes and Debugging Scripts: A Beginner’s Guide to Linux

When you’re writing scripts in Linux, things don’t always go as planned. Sometimes your script runs perfectly, other times it fails mysteriously. This is where understanding exit codes becomes crucial for debugging and creating reliable scripts.

What Are Exit Codes?

Every command and script in Linux returns an exit code when it finishes running. Think of it as the script’s way of saying “I’m done, and here’s how it went.” Exit codes are numbers that tell you whether a command succeeded or failed, and if it failed, they can give you clues about what went wrong.

The most important exit codes to remember:

  • 0: Success! Everything worked perfectly
  • 1-255: Something went wrong (different numbers indicate different types of errors)

Checking Exit Codes

Let’s start with a simple example. Open your terminal and run a command:

1
2
ls /home
echo $?

The echo $? command shows you the exit code of the last command that ran. If the ls command worked, you’ll see 0. Now try:

1
2
ls /nonexistent-directory
echo $?

This time you’ll see a non-zero number (probably 2), indicating the command failed.

Your First Script with Exit Codes

Let’s create a simple script to understand how this works. Create a file called check_file.sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/bin/bash

# Check if a file exists
if [ -f "$1" ]; then
    echo "File $1 exists!"
    exit 0  # Success
else
    echo "File $1 does not exist!"
    exit 1  # Failure
fi

Make it executable and test it:

1
2
3
4
5
6
chmod +x check_file.sh
./check_file.sh /etc/passwd
echo "Exit code: $?"

./check_file.sh /nonexistent-file
echo "Exit code: $?"

Common Exit Codes and Their Meanings

Different exit codes mean different things. Here are the most common ones:

  • 0: Success
  • 1: General error
  • 2: Misuse of shell command
  • 126: Command found but not executable
  • 127: Command not found
  • 128: Invalid exit argument
  • 130: Script terminated by Ctrl+C

Building Better Scripts with Exit Codes

Now let’s create a more practical script that demonstrates proper error handling. This script will backup a directory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/bash

# Simple backup script
SOURCE_DIR="$1"
BACKUP_DIR="$2"

# Check if both arguments are provided
if [ $# -ne 2 ]; then
    echo "Usage: $0 <source_directory> <backup_directory>"
    exit 1
fi

# Check if source directory exists
if [ ! -d "$SOURCE_DIR" ]; then
    echo "Error: Source directory '$SOURCE_DIR' does not exist"
    exit 2
fi

# Create backup directory if it doesn't exist
if [ ! -d "$BACKUP_DIR" ]; then
    mkdir -p "$BACKUP_DIR"
    if [ $? -ne 0 ]; then
        echo "Error: Cannot create backup directory '$BACKUP_DIR'"
        exit 3
    fi
fi

# Perform the backup
cp -r "$SOURCE_DIR"/* "$BACKUP_DIR"
if [ $? -eq 0 ]; then
    echo "Backup completed successfully!"
    exit 0
else
    echo "Backup failed!"
    exit 4
fi

Debugging Techniques

When your scripts don’t work as expected, here are powerful debugging techniques:

1. Use set -e for Strict Error Handling

Add this at the beginning of your script to make it exit immediately when any command fails:

1
2
3
4
5
6
#!/bin/bash
set -e  # Exit on any error

echo "Starting script..."
ls /nonexistent-directory  # This will cause the script to exit
echo "This line will never be reached"

2. Use set -x for Tracing

This shows you exactly what your script is doing:

1
2
3
4
5
#!/bin/bash
set -x  # Enable tracing

NAME="John"
echo "Hello, $NAME!"

3. Combine Both for Maximum Debugging

1
2
3
4
5
6
#!/bin/bash
set -ex  # Exit on error AND show what's happening

echo "Checking system..."
whoami
pwd

Creating a Robust Script Template

Here’s a template that incorporates good practices for error handling:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/bin/bash

# Enable strict error handling
set -euo pipefail

# Function to display usage
usage() {
    echo "Usage: $0 [options] <required_argument>"
    echo "Options:"
    echo "  -h, --help    Show this help message"
    echo "  -v, --verbose Enable verbose output"
    exit 1
}

# Function to log messages
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

# Function to handle errors
error_exit() {
    log "ERROR: $1"
    exit 1
}

# Parse command line arguments
VERBOSE=false
while [[ $# -gt 0 ]]; do
    case $1 in
        -h|--help)
            usage
            ;;
        -v|--verbose)
            VERBOSE=true
            shift
            ;;
        *)
            REQUIRED_ARG="$1"
            shift
            ;;
    esac
done

# Check if required argument is provided
if [ -z "${REQUIRED_ARG:-}" ]; then
    error_exit "Required argument is missing"
fi

# Main script logic
log "Starting script with argument: $REQUIRED_ARG"

# Your script logic here...
if $VERBOSE; then
    log "Verbose mode enabled"
fi

log "Script completed successfully"
exit 0

Testing Your Scripts

Always test your scripts with different scenarios:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash

# Test script for various conditions
test_script() {
    local test_name="$1"
    local command="$2"
    local expected_exit_code="$3"
    
    echo "Testing: $test_name"
    $command
    actual_exit_code=$?
    
    if [ $actual_exit_code -eq $expected_exit_code ]; then
        echo "✓ PASS: Expected $expected_exit_code, got $actual_exit_code"
    else
        echo "✗ FAIL: Expected $expected_exit_code, got $actual_exit_code"
    fi
    echo
}

# Run tests
test_script "Valid file check" "./check_file.sh /etc/passwd" 0
test_script "Invalid file check" "./check_file.sh /nonexistent" 1

Common Debugging Scenarios

Script Exits Unexpectedly

  • Check if set -e is causing premature exits
  • Add echo statements to see where the script stops
  • Use set -x to trace execution

Variables Not Working

  • Check for typos in variable names
  • Ensure proper quoting: "$variable" instead of $variable
  • Use set -u to catch undefined variables

Permission Issues

  • Check file permissions with ls -l
  • Ensure your script is executable: chmod +x script.sh
  • Verify you have permission to read/write files the script accesses

Best Practices Summary

  1. Always use meaningful exit codes in your scripts
  2. Check the exit codes of important commands
  3. Use set -e to catch errors early
  4. Use set -x when debugging
  5. Provide helpful error messages
  6. Test your scripts with various inputs
  7. Use functions for error handling
  8. Log important actions and errors

Conclusion

Understanding exit codes is fundamental to writing reliable Linux scripts. They help you catch errors, debug problems, and create scripts that behave predictably. Start with simple scripts, add proper error handling, and gradually build more complex automation.

Remember: a good script doesn’t just work when everything goes right—it fails gracefully and tells you exactly what went wrong when things don’t go as planned. With exit codes and proper debugging techniques, you’ll be able to create robust scripts that you can trust in production environments.