Handling Script Exit Codes in Shell Scripting
Exit codes are fundamental components of shell scripting and system administration that provide a standardized way for programs and scripts to communicate their execution status to the calling process. Understanding and properly implementing exit codes is crucial for creating robust, maintainable, and professional shell scripts.
What are Exit Codes
Exit codes, also known as return codes or status codes, are numeric values returned by programs, commands, or scripts when they terminate. These codes serve as a communication mechanism between the executed program and its parent process, indicating whether the execution was successful or if an error occurred.
Every command or script in Unix-like systems returns an exit code upon completion. By convention, an exit code of 0 indicates successful execution, while any non-zero value indicates an error or abnormal termination. The exit code range is typically 0-255, though the specific meaning of each code can vary depending on the program or script.
Standard Exit Codes
The following table outlines the most commonly used standard exit codes:
| Exit Code | Meaning | Description | Usage Example | |-----------|---------|-------------|---------------| | 0 | Success | Command executed successfully | Normal program completion | | 1 | General Error | Catchall for general errors | Permission denied, file not found | | 2 | Misuse of Shell Builtins | Incorrect usage of shell commands | Missing keyword or command | | 3-125 | Custom Error Codes | Application-specific errors | User-defined error conditions | | 126 | Command Not Executable | Permission problem or command not executable | File exists but cannot be executed | | 127 | Command Not Found | Command not found in PATH | Typo in command name or missing binary | | 128 | Invalid Argument to Exit | Exit command received invalid argument | exit with non-numeric argument | | 128+n | Fatal Error Signal n | Script terminated by signal n | 130 = Ctrl+C (SIGINT) | | 255 | Exit Status Out of Range | Exit code outside 0-255 range | Wrapped around due to overflow |
Checking Exit Codes
Using the $? Variable
The $? special variable contains the exit code of the last executed command. This variable is automatically updated after each command execution.
`bash
#!/bin/bash
Example 1: Basic exit code checking
ls /existing/directory echo "Exit code of ls command: $?"ls /nonexistent/directory echo "Exit code of failed ls command: $?"
Example 2: Immediate checking after command
if [ $? -eq 0 ]; then echo "Previous command succeeded" else echo "Previous command failed" fi`Note: The $? variable is overwritten after each command, so you must check it immediately after the command you want to test.
Storing Exit Codes
`bash
#!/bin/bash
Store exit code for later use
command_to_test exit_status=$?Now you can use exit_status multiple times
echo "Command exit status: $exit_status"if [ $exit_status -eq 0 ]; then
echo "Command executed successfully"
else
echo "Command failed with exit code: $exit_status"
fi
`
Setting Exit Codes in Scripts
Using the exit Command
The exit command terminates a script and returns a specified exit code to the calling process.
`bash
#!/bin/bash
Function to validate input
validate_input() { local input="$1" if [ -z "$input" ]; then echo "Error: No input provided" >&2 exit 1 fi if [ ${#input} -lt 3 ]; then echo "Error: Input too short" >&2 exit 2 fi echo "Input validation successful" return 0 }Main script logic
if [ $# -eq 0 ]; then echo "Usage: $0validate_input "$1"
echo "Processing input: $1"
exit 0 # Explicit success exit
`
Implicit Exit Codes
If a script doesn't explicitly call exit, it will return the exit code of the last executed command.
`bash
#!/bin/bash
This script's exit code will be the exit code of the last command
echo "Starting process..." mkdir /tmp/test_directory ls -la /tmp/test_directoryScript exits with the exit code of 'ls' command
`Conditional Execution Based on Exit Codes
Using Logical Operators
Shell provides logical operators that work directly with exit codes:
`bash
#!/bin/bash
AND operator (&&) - execute second command only if first succeeds
mkdir /tmp/backup && cp important_file.txt /tmp/backup/OR operator (||) - execute second command only if first fails
cp important_file.txt /backup/ || echo "Backup failed, using local copy"Chaining multiple commands
mkdir /tmp/work && cd /tmp/work && touch newfile.txt || { echo "Failed to create working environment" exit 1 }`Using if Statements
`bash
#!/bin/bash
Direct command testing in if statement
if grep "pattern" file.txt; then echo "Pattern found in file" else echo "Pattern not found" fiTesting specific exit codes
if command_that_might_fail; then echo "Command succeeded" elif [ $? -eq 2 ]; then echo "Command failed with specific error" else echo "Command failed with unexpected error" fi`Advanced Exit Code Handling
Creating Custom Exit Code Functions
`bash
#!/bin/bash
Define custom exit codes
readonly EXIT_SUCCESS=0 readonly EXIT_GENERAL_ERROR=1 readonly EXIT_FILE_NOT_FOUND=2 readonly EXIT_PERMISSION_DENIED=3 readonly EXIT_INVALID_ARGUMENT=4Function to handle different types of errors
handle_file_operation() { local file="$1" local operation="$2" if [ ! -e "$file" ]; then echo "Error: File '$file' not found" >&2 return $EXIT_FILE_NOT_FOUND fi if [ ! -r "$file" ]; then echo "Error: Permission denied for file '$file'" >&2 return $EXIT_PERMISSION_DENIED fi case "$operation" in "read") cat "$file" return $EXIT_SUCCESS ;; "size") wc -l "$file" return $EXIT_SUCCESS ;; *) echo "Error: Invalid operation '$operation'" >&2 return $EXIT_INVALID_ARGUMENT ;; esac }Usage example
handle_file_operation "/etc/passwd" "read" case $? in $EXIT_SUCCESS) echo "Operation completed successfully" ;; $EXIT_FILE_NOT_FOUND) echo "File operation failed: File not found" exit $EXIT_FILE_NOT_FOUND ;; $EXIT_PERMISSION_DENIED) echo "File operation failed: Permission denied" exit $EXIT_PERMISSION_DENIED ;; $EXIT_INVALID_ARGUMENT) echo "File operation failed: Invalid argument" exit $EXIT_INVALID_ARGUMENT ;; esac`Trap Handlers for Exit Codes
The trap command allows you to define cleanup actions that execute when a script exits:
`bash
#!/bin/bash
Global variables to track script state
TEMP_DIR="" LOG_FILE="" EXIT_CODE=0Cleanup function
cleanup() { local exit_code=$? echo "Cleaning up resources..." if [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ]; then rm -rf "$TEMP_DIR" echo "Removed temporary directory: $TEMP_DIR" fi if [ -n "$LOG_FILE" ] && [ -f "$LOG_FILE" ]; then echo "Script exited with code: $exit_code" >> "$LOG_FILE" fi exit $exit_code }Set trap to call cleanup on script exit
trap cleanup EXITMain script logic
TEMP_DIR=$(mktemp -d) LOG_FILE="/tmp/script.log"echo "Script started at $(date)" > "$LOG_FILE"
Simulate some work that might fail
if ! some_critical_operation; then echo "Critical operation failed" exit 1 fiecho "Script completed successfully"
exit 0
`
Exit Codes in Functions
Functions in shell scripts can return exit codes using the return statement:
`bash
#!/bin/bash
Function that validates and processes data
process_data() { local data="$1" local output_file="$2" # Validate input parameters if [ $# -ne 2 ]; then echo "Usage: process_dataUsing the function with exit code handling
if process_data "sample data" "/tmp/output.txt"; then echo "Data processing completed" else case $? in 1) echo "Function called with wrong number of arguments" ;; 2) echo "Empty data provided" ;; 3) echo "Output directory does not exist" ;; 4) echo "Failed to write output file" ;; *) echo "Unknown error occurred" ;; esac exit 1 fi`Best Practices for Exit Code Handling
Comprehensive Error Handling Strategy
`bash
#!/bin/bash
Enable strict error handling
set -euo pipefailDefine exit codes
readonly E_SUCCESS=0 readonly E_GENERAL=1 readonly E_USAGE=64 readonly E_DATAERR=65 readonly E_NOINPUT=66 readonly E_NOUSER=67 readonly E_NOHOST=68 readonly E_UNAVAILABLE=69 readonly E_SOFTWARE=70 readonly E_OSERR=71 readonly E_OSFILE=72 readonly E_CANTCREAT=73 readonly E_IOERR=74 readonly E_TEMPFAIL=75 readonly E_PROTOCOL=76 readonly E_NOPERM=77 readonly E_CONFIG=78Error reporting function
error_exit() { local message="$1" local exit_code="${2:-$E_GENERAL}" echo "ERROR: $message" >&2 echo "Script: $0" >&2 echo "Line: ${BASH_LINENO[1]}" >&2 echo "Exit Code: $exit_code" >&2 exit "$exit_code" }Usage validation
if [ $# -lt 1 ]; then error_exit "Insufficient arguments provided" $E_USAGE fiFile operations with proper error handling
process_file() { local file="$1" [ -f "$file" ] || error_exit "File not found: $file" $E_NOINPUT [ -r "$file" ] || error_exit "Cannot read file: $file" $E_NOPERM # Process file content if ! grep -q "required_pattern" "$file"; then error_exit "Required pattern not found in file" $E_DATAERR fi return $E_SUCCESS }Main execution
main() { local input_file="$1" echo "Processing file: $input_file" if process_file "$input_file"; then echo "File processed successfully" return $E_SUCCESS else error_exit "File processing failed" $E_SOFTWARE fi }Execute main function
main "$@" exit $E_SUCCESS`Exit Code Testing and Debugging
Creating Test Scripts
`bash
#!/bin/bash
Test script for exit code validation
test_exit_codes() { local test_script="$1" local expected_codes="$2" echo "Testing script: $test_script" echo "Expected exit codes: $expected_codes" echo "----------------------------------------" # Test various scenarios local test_cases=( "valid_input.txt" "nonexistent_file.txt" "" "readonly_file.txt" ) for test_case in "${test_cases[@]}"; do echo "Testing with: '$test_case'" "$test_script" "$test_case" local actual_exit_code=$? echo "Exit code: $actual_exit_code" if [[ "$expected_codes" =~ $actual_exit_code ]]; then echo "✓ PASS" else echo "✗ FAIL - Unexpected exit code" fi echo "----------------------------------------" done }Usage example
test_exit_codes "./my_script.sh" "0 1 2 3"`Debugging Exit Codes
`bash
#!/bin/bash
Debug mode for exit code tracking
DEBUG=${DEBUG:-0}debug_log() { if [ "$DEBUG" -eq 1 ]; then echo "[DEBUG] $*" >&2 fi }
Wrapper function to log exit codes
run_command() { local cmd="$*" debug_log "Executing: $cmd" eval "$cmd" local exit_code=$? debug_log "Exit code: $exit_code" return $exit_code }Example usage with debugging
DEBUG=1 run_command "ls /tmp" DEBUG=1 run_command "ls /nonexistent"`Integration with System Tools
Exit Codes in Cron Jobs
`bash
#!/bin/bash
Script designed for cron execution
LOGFILE="/var/log/backup_script.log" LOCKFILE="/var/run/backup_script.lock"Function to log messages with timestamps
log_message() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOGFILE" }Check if script is already running
if [ -f "$LOCKFILE" ]; then log_message "Script already running, exiting" exit 1 fiCreate lock file
echo $ > "$LOCKFILE"Cleanup function
cleanup() { rm -f "$LOCKFILE" exit $1 }Set trap for cleanup
trap 'cleanup $?' EXITMain backup logic
perform_backup() { local source_dir="/important/data" local backup_dir="/backup/location" if [ ! -d "$source_dir" ]; then log_message "ERROR: Source directory not found" return 2 fi if ! rsync -av "$source_dir/" "$backup_dir/"; then log_message "ERROR: Backup failed" return 3 fi log_message "Backup completed successfully" return 0 }Execute backup
log_message "Starting backup process"if perform_backup; then
log_message "Backup process completed successfully"
cleanup 0
else
backup_exit_code=$?
log_message "Backup process failed with exit code: $backup_exit_code"
cleanup $backup_exit_code
fi
`
Exit Codes in Service Scripts
`bash
#!/bin/bash
Service control script with proper exit codes
SERVICE_NAME="myservice" PID_FILE="/var/run/$SERVICE_NAME.pid" LOG_FILE="/var/log/$SERVICE_NAME.log"Standard service exit codes
readonly EXIT_SUCCESS=0 readonly EXIT_DEAD_PID_EXISTS=1 readonly EXIT_DEAD_LOCK_EXISTS=2 readonly EXIT_NOT_RUNNING=3 readonly EXIT_UNKNOWN_STATUS=4get_pid() { if [ -f "$PID_FILE" ]; then cat "$PID_FILE" fi }
is_running() { local pid=$(get_pid) if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then return 0 else return 1 fi }
start_service() { if is_running; then echo "$SERVICE_NAME is already running" return $EXIT_SUCCESS fi echo "Starting $SERVICE_NAME..." # Start the actual service (replace with your service command) nohup /usr/bin/myservice > "$LOG_FILE" 2>&1 & local service_pid=$! echo "$service_pid" > "$PID_FILE" # Verify service started successfully sleep 2 if is_running; then echo "$SERVICE_NAME started successfully" return $EXIT_SUCCESS else echo "Failed to start $SERVICE_NAME" rm -f "$PID_FILE" return $EXIT_UNKNOWN_STATUS fi }
stop_service() { local pid=$(get_pid) if [ -z "$pid" ]; then echo "$SERVICE_NAME is not running" return $EXIT_NOT_RUNNING fi echo "Stopping $SERVICE_NAME..." if kill "$pid" 2>/dev/null; then # Wait for process to terminate local count=0 while kill -0 "$pid" 2>/dev/null && [ $count -lt 10 ]; do sleep 1 count=$((count + 1)) done if kill -0 "$pid" 2>/dev/null; then echo "Force killing $SERVICE_NAME..." kill -9 "$pid" fi rm -f "$PID_FILE" echo "$SERVICE_NAME stopped" return $EXIT_SUCCESS else echo "Failed to stop $SERVICE_NAME" return $EXIT_UNKNOWN_STATUS fi }
status_service() { if is_running; then echo "$SERVICE_NAME is running (PID: $(get_pid))" return $EXIT_SUCCESS else if [ -f "$PID_FILE" ]; then echo "$SERVICE_NAME is dead but PID file exists" return $EXIT_DEAD_PID_EXISTS else echo "$SERVICE_NAME is not running" return $EXIT_NOT_RUNNING fi fi }
Main command processing
case "$1" in start) start_service exit $? ;; stop) stop_service exit $? ;; status) status_service exit $? ;; restart) stop_service start_service exit $? ;; *) echo "Usage: $0 {start|stop|status|restart}" exit $EXIT_UNKNOWN_STATUS ;; esac`Summary
Exit codes are essential for creating robust and maintainable shell scripts. They provide a standardized way to communicate execution status between processes and enable proper error handling and flow control. Key points to remember:
1. Always use exit code 0 for success and non-zero codes for errors
2. Check exit codes immediately after command execution using $?
3. Use meaningful and consistent exit codes throughout your scripts
4. Implement proper cleanup procedures using trap handlers
5. Document your custom exit codes for future maintenance
6. Test your scripts thoroughly to ensure proper exit code behavior
7. Use exit codes to enable conditional execution and error recovery
By following these practices and understanding the concepts outlined in this guide, you can create professional-quality shell scripts that integrate seamlessly with system tools and provide clear feedback about their execution status.