Shell Script Exit Codes: Complete Guide for Developers

Master exit codes in shell scripting. Learn standard codes, implementation techniques, and best practices for robust script development.

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: $0 " >&2 exit 64 # EX_USAGE from sysexits.h fi

validate_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_directory

Script 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" fi

Testing 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=4

Function 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=0

Cleanup 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 EXIT

Main 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 fi

echo "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_data " >&2 return 1 fi # Check if data is not empty if [ -z "$data" ]; then echo "Error: Data cannot be empty" >&2 return 2 fi # Check if output directory exists local output_dir=$(dirname "$output_file") if [ ! -d "$output_dir" ]; then echo "Error: Output directory does not exist: $output_dir" >&2 return 3 fi # Process the data echo "Processing: $data" > "$output_file" if [ $? -eq 0 ]; then echo "Data processed successfully" return 0 else echo "Error: Failed to write output file" >&2 return 4 fi }

Using 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 pipefail

Define 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=78

Error 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 fi

File 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 fi

Create lock file

echo $ > "$LOCKFILE"

Cleanup function

cleanup() { rm -f "$LOCKFILE" exit $1 }

Set trap for cleanup

trap 'cleanup $?' EXIT

Main 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=4

get_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.

Tags

  • Unix
  • bash
  • exit-codes
  • shell-scripting
  • system-administration

Related Articles

Popular Technical Articles & Tutorials

Explore our comprehensive collection of technical articles, programming tutorials, and IT guides written by industry experts:

Browse all 8+ technical articles | Read our IT blog

Shell Script Exit Codes: Complete Guide for Developers