Overview
The exit command is a shell built-in that terminates the current shell session or script and returns control to the parent process. It's one of the most fundamental commands in shell scripting, used to end scripts gracefully and communicate success or failure through exit status codes.
Primary Functions:
- Exit the current shell or terminal session
- Terminate a running script
- Return an exit status code to the parent process
- Signal success (0) or failure (non-zero) of operations
- Control script flow and error handling
- Enable conditional execution in pipelines
Exit Status Codes:
- 0: Success - command completed successfully
- 1-255: Various error conditions
- 126: Command found but not executable
- 127: Command not found
- 128+n: Fatal signal n (e.g., 130 = Ctrl+C, signal 2)
- 255: Exit status out of range
Exit is a shell built-in command, not an external program. This means it's part of the shell itself (bash, sh, zsh, etc.) and can directly affect the current shell process. When you type exit in a terminal, that terminal session ends. When used in a script, the script terminates but the terminal remains open.
Detailed Examples
Basic Exit from Interactive Shell
The simplest use of exit - terminating your current shell session.
# You're working in a terminal
$ whoami
craig
$ pwd
/home/craig
# Exit the shell
$ exit
exit in an interactive shell, it terminates that shell session. In a terminal window, this typically closes the window. In an SSH session, it logs you out and closes the connection. The default exit status is the status of the last command executed before exit.
Ctrl+D (End-of-File), which has the same effect as typing exit. The logout command also works in login shells.
Exit with Explicit Status Code
Specifying an exit status to communicate success or failure.
#!/bin/bash
# script1.sh - Exit with success
echo "Processing data..."
# Do some work...
echo "Operation completed successfully!"
# Exit with status 0 (success)
exit 0
#!/bin/bash
# script2.sh - Exit with failure
echo "Checking configuration..."
if [ ! -f "/etc/myapp.conf" ]; then
echo "Error: Configuration file not found!"
exit 1 # Exit with status 1 (failure)
fi
echo "Configuration OK"
exit 0
# Run the scripts and check their exit status
$ ./script1.sh
Processing data...
Operation completed successfully!
$ echo $?
0
$ ./script2.sh
Checking configuration...
Error: Configuration file not found!
$ echo $?
1
exit n command terminates the script and returns status code n to the parent process. By convention, 0 means success and non-zero means failure. The special variable $? holds the exit status of the last command executed.
exit 0 for successful completion and non-zero values (typically 1-255) for errors. This allows other scripts and commands to determine if your script succeeded.
Using Different Exit Codes for Different Errors
Implement meaningful exit codes to distinguish between different error conditions.
#!/bin/bash
# backup-script.sh - Backup with meaningful exit codes
SOURCE_DIR="/home/craig/documents"
BACKUP_DIR="/backup"
BACKUP_FILE="backup-$(date +%Y%m%d).tar.gz"
# Exit code definitions
EXIT_SUCCESS=0
EXIT_SOURCE_NOT_FOUND=1
EXIT_BACKUP_DIR_NOT_FOUND=2
EXIT_NO_SPACE=3
EXIT_COMPRESSION_FAILED=4
EXIT_PERMISSION_DENIED=5
# Check if source directory exists
if [ ! -d "$SOURCE_DIR" ]; then
echo "Error: Source directory not found: $SOURCE_DIR"
exit $EXIT_SOURCE_NOT_FOUND
fi
# Check if backup directory exists
if [ ! -d "$BACKUP_DIR" ]; then
echo "Error: Backup directory not found: $BACKUP_DIR"
exit $EXIT_BACKUP_DIR_NOT_FOUND
fi
# Check if we have write permission
if [ ! -w "$BACKUP_DIR" ]; then
echo "Error: No write permission for $BACKUP_DIR"
exit $EXIT_PERMISSION_DENIED
fi
# Check available space (simplified)
AVAILABLE=$(df -k "$BACKUP_DIR" | tail -1 | awk '{print $4}')
if [ "$AVAILABLE" -lt 1048576 ]; then # Less than 1GB
echo "Error: Insufficient disk space in $BACKUP_DIR"
exit $EXIT_NO_SPACE
fi
# Perform backup
echo "Creating backup: $BACKUP_FILE"
tar -czf "$BACKUP_DIR/$BACKUP_FILE" "$SOURCE_DIR" 2>/dev/null
if [ $? -ne 0 ]; then
echo "Error: Backup compression failed"
exit $EXIT_COMPRESSION_FAILED
fi
echo "Backup completed successfully: $BACKUP_DIR/$BACKUP_FILE"
exit $EXIT_SUCCESS
# Calling script that checks exit codes
#!/bin/bash
# run-backup.sh
./backup-script.sh
case $? in
0)
echo "✓ Backup successful"
;;
1)
echo "✗ Source directory not found"
# Maybe try alternate source
;;
2)
echo "✗ Backup directory not found"
# Maybe create it
;;
3)
echo "✗ Insufficient disk space"
# Send alert to admin
;;
4)
echo "✗ Compression failed"
# Log error details
;;
5)
echo "✗ Permission denied"
# Run with sudo?
;;
*)
echo "✗ Unknown error"
;;
esac
Early Exit on Error
Exit immediately when an error is detected to prevent cascading failures.
#!/bin/bash
# deployment-script.sh - Exit early on any error
set -e # Exit immediately if any command fails
echo "Starting deployment..."
# This will exit if directory doesn't exist
cd /opt/application || exit 1
echo "Stopping service..."
systemctl stop myapp || exit 2
echo "Backing up current version..."
cp -r current current.backup || exit 3
echo "Deploying new version..."
tar -xzf /tmp/newversion.tar.gz || exit 4
echo "Starting service..."
systemctl start myapp || exit 5
echo "Verifying service..."
sleep 5
systemctl is-active myapp || exit 6
echo "Deployment completed successfully!"
exit 0
|| exit N pattern checks if the previous command failed (non-zero exit status) and immediately exits with a specific error code. The set -e option at the top makes the script exit automatically on any command failure, providing a safety net.
set -e can have unexpected consequences in complex scripts. Commands in conditionals, pipelines, or with ||/&& may not trigger automatic exit. Always test thoroughly!
set -e, explicitly check important commands:
if ! systemctl stop myapp; then
echo "Failed to stop service"
exit 2
fi
Exit from Functions
Using return vs exit in shell functions, and how they differ.
#!/bin/bash
# function-exit-demo.sh
# Function using return (correct for functions)
check_file_exists() {
local file="$1"
if [ -f "$file" ]; then
echo "File exists: $file"
return 0 # Success - continues script
else
echo "File not found: $file"
return 1 # Failure - continues script
fi
}
# Function using exit (wrong - exits entire script!)
check_file_exists_wrong() {
local file="$1"
if [ -f "$file" ]; then
echo "File exists: $file"
exit 0 # Exits the entire script!
else
echo "File not found: $file"
exit 1 # Exits the entire script!
fi
}
echo "Testing with return (correct):"
check_file_exists "/etc/passwd"
echo "Still running after function..."
check_file_exists "/nonexistent"
echo "Still running even after error..."
echo ""
echo "Testing with exit (wrong):"
check_file_exists_wrong "/etc/passwd"
echo "You won't see this line!" # Never executed
return to exit the function and return control to the calling code. Use exit only when you want to terminate the entire script. Using exit in a function will end the entire script, not just the function!
- return: Exits the function, returns status to caller, script continues
- exit: Exits the entire script (or shell), returns status to parent process
- Both accept status codes:
return 0,exit 1 - Return is only valid inside functions
- Exit works anywhere in script or interactive shell
Exit in Subshells and Pipelines
Understanding how exit behaves in subshells, which is crucial for complex scripts.
#!/bin/bash
# subshell-exit-demo.sh
echo "=== Exit in Subshell (Parentheses) ==="
echo "Main script running..."
(
echo "Inside subshell"
exit 1 # Only exits the subshell!
echo "This won't print"
)
echo "Main script still running! Subshell exited."
echo "Subshell exit status: $?"
echo ""
echo "=== Exit in Command Substitution ==="
result=$(
echo "Processing..."
exit 5 # Only exits this command substitution
echo "Never reached"
)
echo "After command substitution"
echo "Command substitution exit status: $?"
echo ""
echo "=== Exit in Pipeline ==="
cat /etc/passwd | (
while read line; do
if [[ "$line" == *"root"* ]]; then
echo "Found root!"
exit 0 # Only exits this part of pipeline
fi
done
exit 1
)
echo "After pipeline"
echo "Pipeline exit status: $?"
echo ""
echo "Main script ending with exit 0"
exit 0
exit in a subshell (created by parentheses, command substitution, or pipelines), it only exits that subshell, not the main script. The main script continues executing. This is important for understanding script flow and avoiding unexpected behavior.
(command; exit 1)
if [ $? -ne 0 ]; then
exit 1
fi
Trap and Exit - Cleanup on Exit
Using trap to ensure cleanup code runs regardless of how the script exits.
#!/bin/bash
# cleanup-on-exit.sh - Proper cleanup using trap
TEMP_DIR=$(mktemp -d)
LOCKFILE="/var/run/myscript.lock"
# Cleanup function
cleanup() {
local exit_code=$?
echo ""
echo "Cleaning up..."
# Remove temporary directory
if [ -d "$TEMP_DIR" ]; then
rm -rf "$TEMP_DIR"
echo " Removed temp directory: $TEMP_DIR"
fi
# Remove lock file
if [ -f "$LOCKFILE" ]; then
rm -f "$LOCKFILE"
echo " Removed lock file: $LOCKFILE"
fi
echo "Cleanup complete. Exiting with code: $exit_code"
exit $exit_code
}
# Set trap to call cleanup on EXIT signal
trap cleanup EXIT
# Also trap for specific signals
trap 'echo "Caught SIGINT (Ctrl+C)"; exit 130' INT
trap 'echo "Caught SIGTERM"; exit 143' TERM
echo "Script starting..."
echo "Creating temporary files in: $TEMP_DIR"
# Create lock file
touch "$LOCKFILE"
echo "Created lock file: $LOCKFILE"
# Simulate some work
echo "Processing..."
sleep 2
# Create some temp files
touch "$TEMP_DIR/file1.tmp"
touch "$TEMP_DIR/file2.tmp"
echo "Created temporary files"
# Simulate different exit scenarios
read -p "Choose exit method (1=normal, 2=error, 3=Ctrl+C): " choice
case $choice in
1)
echo "Exiting normally..."
exit 0
;;
2)
echo "Simulating error..."
exit 1
;;
3)
echo "Waiting for Ctrl+C..."
sleep 30
;;
esac
trap command sets up a function to run when the script exits, regardless of whether it's a normal exit, an error exit, or termination by signal (Ctrl+C, kill, etc.). This ensures cleanup code always runs, preventing resource leaks and leaving the system in a clean state.
- Temporary files and directories
- Lock files
- Database connections
- Mounted filesystems
- Background processes
- Network connections
Exit Status in Conditional Chains
How exit status affects && and || operators in command chains.
#!/bin/bash
# conditional-chains.sh
echo "=== AND Operator (&&) ==="
echo "Only continues if previous command succeeds (exit 0)"
# All succeed - all execute
true && echo "Step 1" && echo "Step 2" && echo "Step 3"
echo ""
# Failure stops the chain
true && echo "Step 1" && false && echo "Never reached"
echo ""
echo "=== OR Operator (||) ==="
echo "Only continues if previous command fails (exit non-zero)"
# First succeeds - stops
true || echo "Not executed" || echo "Not executed either"
echo ""
# Keep trying until one succeeds
false || false || echo "Finally succeeded!" || echo "Not needed"
echo ""
echo "=== Combined && and || ==="
# Common pattern: command || exit
mkdir /tmp/testdir || exit 1
echo "Directory created"
# Check if file exists, else exit
[ -f "/etc/passwd" ] || { echo "File not found!"; exit 1; }
echo "File check passed"
# Success notification
./important-script.sh && echo "SUCCESS" || { echo "FAILED"; exit 1; }
echo ""
echo "=== Practical Examples ==="
# Deployment pattern
cd /opt/application || exit 1
systemctl stop myapp || exit 2
git pull || exit 3
systemctl start myapp || exit 4
echo "Deployment successful"
# Backup pattern
tar -czf backup.tar.gz data/ || { echo "Backup failed"; exit 1; }
scp backup.tar.gz remote:/backups/ || { echo "Upload failed"; exit 2; }
rm backup.tar.gz || { echo "Cleanup warning"; } # Non-fatal
echo "Backup completed"
&& operator only executes the next command if the previous one succeeded (exit 0). The || operator only executes if the previous command failed (non-zero exit). These operators provide concise error handling and conditional execution based on exit status.
cmd1 && cmd2- Run cmd2 only if cmd1 succeedscmd1 || cmd2- Run cmd2 only if cmd1 failscmd1 && cmd2 || cmd3- Run cmd2 if cmd1 succeeds, else run cmd3cmd || exit 1- Exit if cmd fails (common pattern)
Exit Without Specifying Code - Inheriting Status
When you don't specify an exit code, the script exits with the status of the last command.
#!/bin/bash
# inherit-status.sh
echo "=== Script 1: Exit without code after success ==="
cat <<'EOF' > script1.sh
#!/bin/bash
ls /etc/passwd
exit # No code specified - inherits 0 from successful ls
EOF
chmod +x script1.sh
./script1.sh
echo "Script 1 exit status: $?"
echo ""
echo "=== Script 2: Exit without code after failure ==="
cat <<'EOF' > script2.sh
#!/bin/bash
ls /nonexistent/file 2>/dev/null
exit # No code specified - inherits non-zero from failed ls
EOF
chmod +x script2.sh
./script2.sh
echo "Script 2 exit status: $?"
echo ""
echo "=== Script 3: No explicit exit ==="
cat <<'EOF' > script3.sh
#!/bin/bash
echo "First command"
echo "Second command"
false # Last command fails
# No exit statement - script ends with status of last command
EOF
chmod +x script3.sh
./script3.sh
echo "Script 3 exit status: $?"
echo ""
echo "=== Script 4: Proper explicit exit ==="
cat <<'EOF' > script4.sh
#!/bin/bash
echo "Processing..."
false # This fails
echo "More processing..."
exit 0 # Explicitly exit with success despite earlier failure
EOF
chmod +x script4.sh
./script4.sh
echo "Script 4 exit status: $?"
# Cleanup
rm -f script{1,2,3,4}.sh
exit without specifying a code, or if your script ends without an explicit exit statement, the exit status will be the status of the last command executed. This can lead to unexpected results if you're not careful.
exit 0 for success or exit N for errors. Don't rely on inheriting the last command's status - it makes your script's behavior unclear and error-prone.
Complete Script with Comprehensive Exit Handling
A production-quality script demonstrating all exit best practices.
#!/bin/bash
# database-backup.sh - Production database backup script
# Demonstrates comprehensive exit handling
# Exit codes
readonly EXIT_SUCCESS=0
readonly EXIT_INVALID_ARGS=1
readonly EXIT_DEPENDENCY_MISSING=2
readonly EXIT_DB_CONNECTION_FAILED=3
readonly EXIT_BACKUP_FAILED=4
readonly EXIT_UPLOAD_FAILED=5
readonly EXIT_CLEANUP_FAILED=6
# Configuration
readonly SCRIPT_NAME=$(basename "$0")
readonly BACKUP_DIR="/var/backups/database"
readonly TEMP_DIR=$(mktemp -d)
readonly LOG_FILE="/var/log/database-backup.log"
# Logging function
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# Error handling function
error_exit() {
local message="$1"
local exit_code="${2:-1}"
log "ERROR: $message"
exit "$exit_code"
}
# Cleanup function
cleanup() {
local exit_code=$?
log "Performing cleanup..."
if [ -d "$TEMP_DIR" ]; then
rm -rf "$TEMP_DIR" || log "WARNING: Failed to remove temp directory"
fi
# Log final status
if [ $exit_code -eq 0 ]; then
log "Backup completed successfully"
else
log "Backup failed with exit code: $exit_code"
fi
exit $exit_code
}
# Set trap for cleanup
trap cleanup EXIT INT TERM
# Validate arguments
if [ $# -ne 1 ]; then
echo "Usage: $SCRIPT_NAME <database_name>"
echo "Example: $SCRIPT_NAME production_db"
exit $EXIT_INVALID_ARGS
fi
DB_NAME="$1"
# Start script
log "========================================="
log "Starting backup for database: $DB_NAME"
log "========================================="
# Check dependencies
log "Checking dependencies..."
for cmd in mysqldump gzip aws; do
if ! command -v "$cmd" &>/dev/null; then
error_exit "Required command not found: $cmd" $EXIT_DEPENDENCY_MISSING
fi
done
log "All dependencies found"
# Create backup directory if needed
if [ ! -d "$BACKUP_DIR" ]; then
log "Creating backup directory: $BACKUP_DIR"
mkdir -p "$BACKUP_DIR" || error_exit "Failed to create backup directory" $EXIT_BACKUP_FAILED
fi
# Test database connection
log "Testing database connection..."
if ! mysql -e "SELECT 1" "$DB_NAME" &>/dev/null; then
error_exit "Cannot connect to database: $DB_NAME" $EXIT_DB_CONNECTION_FAILED
fi
log "Database connection successful"
# Perform backup
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$TEMP_DIR/${DB_NAME}_${TIMESTAMP}.sql"
COMPRESSED_FILE="${BACKUP_FILE}.gz"
log "Creating database dump..."
if ! mysqldump "$DB_NAME" > "$BACKUP_FILE" 2>>"$LOG_FILE"; then
error_exit "Database dump failed" $EXIT_BACKUP_FAILED
fi
# Verify backup file was created and has content
if [ ! -s "$BACKUP_FILE" ]; then
error_exit "Backup file is empty" $EXIT_BACKUP_FAILED
fi
FILE_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
log "Backup created: $BACKUP_FILE (Size: $FILE_SIZE)"
# Compress backup
log "Compressing backup..."
if ! gzip "$BACKUP_FILE" 2>>"$LOG_FILE"; then
error_exit "Compression failed" $EXIT_BACKUP_FAILED
fi
log "Backup compressed: $COMPRESSED_FILE"
# Move to backup directory
FINAL_FILE="$BACKUP_DIR/$(basename "$COMPRESSED_FILE")"
if ! mv "$COMPRESSED_FILE" "$FINAL_FILE"; then
error_exit "Failed to move backup to final location" $EXIT_BACKUP_FAILED
fi
log "Backup saved to: $FINAL_FILE"
# Upload to S3 (if configured)
if [ -n "${AWS_S3_BUCKET:-}" ]; then
log "Uploading to S3: s3://$AWS_S3_BUCKET/backups/"
if ! aws s3 cp "$FINAL_FILE" "s3://$AWS_S3_BUCKET/backups/" 2>>"$LOG_FILE"; then
error_exit "S3 upload failed" $EXIT_UPLOAD_FAILED
fi
log "Upload completed successfully"
fi
# Clean old backups (keep last 7 days)
log "Cleaning old backups (keeping 7 days)..."
find "$BACKUP_DIR" -name "${DB_NAME}_*.sql.gz" -mtime +7 -delete 2>>"$LOG_FILE"
# Success!
log "Backup process completed successfully!"
exit $EXIT_SUCCESS
- Defined exit codes with meaningful names
- Consistent error handling with descriptive messages
- Trap-based cleanup that always runs
- Comprehensive logging
- Dependency checking with appropriate exit codes
- Clear success path with exit 0
- Distinct exit codes for different failure scenarios
- Detect specific types of failures
- Send targeted alerts to appropriate teams
- Trigger automatic remediation for known issues
- Generate accurate reports on backup success rates
Standard Exit Codes
| Code | Meaning | Common Use |
|---|---|---|
| 0 | Success | Command completed successfully |
| 1 | General error | Catchall for general errors |
| 2 | Misuse of shell command | Missing keyword, command, or permission problem |
| 126 | Command cannot execute | Permission problem or command is not executable |
| 127 | Command not found | Possible typo or PATH issue |
| 128 | Invalid exit argument | Exit takes only integer args in range 0-255 |
| 128+n | Fatal error signal "n" | 130 (128+2) = terminated by Ctrl+C (SIGINT) |
| 130 | Script terminated by Ctrl+C | SIGINT (signal 2) |
| 137 | Script terminated by SIGKILL | kill -9 (signal 9) |
| 143 | Script terminated by SIGTERM | kill (signal 15, default) |
| 255 | Exit status out of range | Exit takes only integer args in range 0-255 |
Best Practices
# Bad - uses same code for different errors
if [ ! -f "$CONFIG" ]; then
exit 1
fi
if [ ! -w "$LOGDIR" ]; then
exit 1
fi
# Good - distinct codes for different errors
if [ ! -f "$CONFIG" ]; then
echo "Config file not found"
exit 2
fi
if [ ! -w "$LOGDIR" ]; then
echo "Log directory not writable"
exit 3
fi
# Always clean up resources
trap 'rm -rf "$TEMP_DIR"' EXIT
# Your script code here
TEMP_DIR=$(mktemp -d)
# ... work ...
# Bad - ignores errors
mysqldump database > backup.sql
echo "Backup complete"
# Good - checks status
if ! mysqldump database > backup.sql; then
echo "Backup failed"
exit 1
fi
echo "Backup complete"
# Bad - exits entire script
function check_file() {
[ -f "$1" ] || exit 1
}
# Good - returns status
function check_file() {
[ -f "$1" ] || return 1
}
# Caller can decide whether to exit
if ! check_file "$CONFIG"; then
echo "Config not found"
exit 1
fi
- Using exit in functions when you mean return
- Not checking exit status of critical commands
- Using same exit code for different errors
- Forgetting cleanup code (use trap!)
- Using exit codes outside 0-255 range
- Relying on implicit exit status (always be explicit)
Quick Reference
| Command | Description |
|---|---|
exit |
Exit with status of last command |
exit 0 |
Exit with success |
exit 1 |
Exit with general error |
exit $? |
Exit with status of last command (explicit) |
echo $? |
Show exit status of last command |
command || exit 1 |
Exit if command fails |
trap cleanup EXIT |
Run cleanup function on exit |
set -e |
Exit on any error (use carefully!) |
return N |
Exit function with status N |
Ctrl+D |
Exit interactive shell (EOF) |