Exit status codes are a critical technique for indicating successful execution or failures in Bash scripts. Understanding the meaning and proper usage of exit codes like exit 0 and exit 1 is foundational knowledge for productive Bash scripting.
This comprehensive guide covers everything from exit code basics and conventions to real-world examples and best practices for leveraging script exit statuses effectively. Follow along to gain expert-level mastery of this essential Bash scripting tool.
What Are Exit Codes and Status Values in Bash?
Exit codes (also called exit statuses) allow Bash scripts to communicate results and errors to surrounding code. They provide information about whether execution succeeded or a failure occurred.
Some key characteristics:
- Integer values: Exit statuses are represented by integer numbers from 0 up to 255.
- Returned after execution: The exit code is returned after a script or command finishes running.
- Built-in variable: The special
$?
variable contains the exit value of the last executed command. - Convention meanings: Specific exit code values denote different outcomes by convention.
Well-designed Bash scripts leverage exit codes to indicate success, failure modes, runtime errors, and other meaningful execution details. This allows superior orchestration, debugging, and automation around shell scripting.
According to the Linux Foundation‘s 2021 Open Source Jobs Report, Bash and shell scripting skill are the 2nd most desired coding capability that companies hire for, with 67% of hiring managers looking for Bash competency. Mastering exit code practices is an essential capability for professional Bash coders.
Purpose and Usage of Exit Codes
Exit statuses serve vital purposes like controlling program flow, error checking, signaling issues, and more:
Error Checking: Scripts can easily check a command‘s exit code to see if it succeeded or failed using the $?
variable. This facilitates robust error validation.
Modular Coding: Functions and scripts can return exit codes to cleanly propagate status results up to calling routines. This enables building reusable code components.
Debugging: Exit status values provide diagnostics to help pinpoint exactly where and how execution failures occur. This allows faster debugging and troubleshooting.
Automation: Shell script exit codes can drive automated decisions and logic in orchestration systems. Success versus failure status facilitates rich automation schemes.
Portability: Well-known exit code conventions provide common ground across different languages, scripts, programs, and operating systems.
Using exit codes appropriately is vital for production-ready scripts where reliability, scalability, and automated handling are important.
Exit Code Zero Details and Examples
The exit status 0 communicates successful execution without any major errors. According to Bash standards:
- Meaning: Exit code 0 means success – the command or script ran to completion as intended.
- Success indicator: This code signals normal, desired execution results to calling processes.
- No outright failure: 0 means nothing explicitly flagged an error during the run. It does not guarantee additional checks like data validation passed.
Here is an example showcasing exit 0 in a script:
#!/bin/bash
# Demo script for exit 0
echo "Sample script running"
# Normal code path
echo "Done"
exit 0
This script runs without issue and exits with a 0 status:
$ ./myscript.sh
Sample script running
Done
$ echo $?
0
The 0 return indicates success to the parent shell or calling code.
Let‘s explore a more realistic example where standard output and exit 0 denote a normal run:
#!/bin/bash
# Script to validate input
if [ -z "$1" ]; then
echo "Usage: ${0} <input_file>"
exit 1
fi
input_file=$1
# Check if input file exists
if [ ! -f ${input_file} ]; then
echo "Input file ${input_file} not found"
exit 1
fi
# Run processing on input
process_input ${input_file}
echo "Processed file successfully"
exit 0
This script tests preconditions like input arguments and dependencies. If everything checks out, it runs the desired operation and reports success to the caller via output and exit 0 status.
Exit Code 1 Signals and Examples
While exit code 0 denotes success, exit code 1 communicates a failure or error condition according to convention. For example:
- Meaning: Exit 1 means failure – the script or command was unable to complete normally.
- Error indicator: An exit 1 alerts calling processes that something went wrong.
- Abnormal termination: It means execution exited prematurely or abnormally.
Some example situations where a script might return exit 1:
- Invalid arguments or input values
- Failed validation checks
- Missing dependencies or prerequisites
- Runtime errors like divide by zero
- Inability to find required resources
- Hardware faults or out of memory errors
- Invalid environment or configuration
Here is a sample script demonstrating exit 1 usage:
#!/bin/bash
# Search script to find users
if [ $# -ne 1 ]; then
echo "Usage: ${0} <username>"
exit 1
fi
user=$1
# Check if user exists on system
if ! id ${user} > /dev/null 2>&1; then
echo "User ${user} not found"
exit 1
fi
getent passwd ${user}
In this case, exit 1 is employed both for invalid input and the user not existing – two distinct failure modes. The script explicitly returns 1 in exception cases vs 0 for the happy path.
In another example, exit 1 might be used for a runtime issue:
#!/bin/bash
read -p "Enter a number: " value
result=$((value + 5))
if [[ ${result} -eq 0 ]]; then
echo "Error computing result"
exit 1
fi
echo ${result}
Here if the computation fails, the script alerts the user and exits 1.
So in general, exit 1 signals that the script failed due to an error, invalid input, or unexpected runtime issue.
Key Differences Between Exit 0 and Exit 1
The major differences between exit code 0 and exit code 1 are:
Exit 0 | Exit 1 |
---|---|
Indicates success – no errors during execution | Denotes a failure or abnormal exit |
Normal, expected result | Unexpected program termination |
All major processing succeeded | Error preventing normal completion |
Valid usage and results | Invalid input/conditions or runtime issue |
Continue calling process logic | Handle failure in caller per policy |
Exit 0 tells surrounding code that the script ran fine with no detectable issues. Exit 1 means a failure occurred – something went wrong causing abnormal termination.
These two standardized codes connect Bash script outcomes with Unix process conventions spanning languages and OS‘s. They allow interoperation and rich programming logic around shell script results.
Common Non-Zero Exit Codes
In Bash, while exit 1 denotes a generic execution failure, other integer codes often signify specific kinds of errors:
Exit Code | Meaning |
---|---|
2 | Misuse of shell builtins, like missing keywords or permissions issues |
126 | Invoked command is not executable |
127 | "Command not found" |
128 | Invalid exit argument (outside 0-255 range) |
128+n | Fatal error signal "n" |
130 | Script terminated by Control-C |
255 | Exit status out of range |
These codes allow providing more targeted diagnostic information compared to a generic exit 1 error. Scripts leveraging them can propagate the most accurate status to calling processes.
For example, this script handles different kinds of input failures uniquely vs a catch-all exit 1:
if [ -z "$1" ]; then
echo "Missing input filename"
exit 2
elif [ ! -f "$1" ]; then
echo "File $1 not found"
exit 127
else
echo "Invalid file $1"
exit 1
fi
# Rest of script...
Utilizing exit codes this way improves debugging and handling of failures.
Employing Exit Codes for Control Flow
Beyond signaling errors, exit status values are commonly used to drive control flow in Bash scripts based on results.
For instance, this simple pattern checks a command‘s exit code using the $?
variable to decide whether to continue processing or abort:
generate_report ${inputs}
if [ $? -ne 0 ]; then
echo "Failed to generate report"
exit 1
fi
# Carry on with report processing
The script tests the result of generate_report
to ensure it succeeded before continuing.
We can extend this with additional logic:
rm -f ${target}
if [ $? -ne 0 ]; then
echo "Failed to delete ${target}"
if [[ "$CONTINUE_ON_ERROR" == "false" ]]; then
exit 1
fi
# Otherwise continue
fi
echo "Continuing processing..."
Here the script deletes a target destination, checks the result code, and either exits or continues depending on an environment variable flag.
This demonstrates a common pattern – branching script execution based on exit codes.
Besides controlling script flow, exit statuses may also be used to dynamically execute code blocks handling different exit cases. For example:
#!/bin/bash
# Run backup job
backup_data
# Take action based on status
case $? in
0)
echo "Backup succeeded";;
1)
echo "Backup failed";;
send_alert_email;;
2)
verify_environment;;
*)
echo "Unknown error"
exit 1;;
esac
This allows tailored handling of different exit cases like backup succeeding, a runtime failure, config issue, etc.
So leveraging exit codes to alter control flow is an important Bash scripting technique for robust automation.
Employing Exit Codes for Modular Code
Another common usage is employing exit codes to build reusable, modular script components.
By returning exit status values, functions and scripts signal results to upstream calling logic. This enables clean decomposition into reusable units with well-defined interfaces.
For example:
#!/bin/bash
# Validate input arg counts
validate_args() {
if [ $# -ne 2 ]; then
echo "Usage: ${0} <src> <dest>"
exit 1
fi
}
# Standardized error handler
err_handler() {
echo "Failed with exit code ${1}: ${2}"
exit ${1}
}
# Application logic
validate_args $@
cp "${1}" "${2}"
result=$?
if [ ${result} -ne 0 ]; then
err_handler ${result} "Copy failed"
fi
echo "Done"
This script demonstrates some best practices:
- Functions validate inputs and return exit 1 for errors
- Application code checks result codes after steps
- Central error handler propagates statuses
Such reusable logic and consistency sets the stage for solid automation and integration.
Debugging Using Exit Codes
Exit status values serve a vital role for tracing errors and diagnosing failures in Bash scripts.
By logging codes at key points, the exact line of failure can be identified. For example:
set -eo pipefail
echo "Start"
...
backup_file ./mydata.txt
echo "Backed up file, status: $?"
...
restore_file ./mydata.txt
echo "Restored file, status: $?"
...
verify_integrity ./mydata.txt
echo "Verified file, status: $?"
This logs diagnostic points including the exit status throughout the script. If issues occur, the developer knows the exact failing statement.
Certain practices maximize debugging ability:
- Strategically logging exit codes at function/logic boundaries
- Trapping and examining errors with
trap ERR
- Logging output to disambiguate root causes
- Asserting on expectations with status checks
- Enabling debugging output/modes (
-x
flag)
Parsing logged outputs around deviations makes diagnosis straightforward.
For example, seeing status 127 after a restoration would strongly suggest the restore program was not available in $PATH
. This quickly points to environment misconfiguration as the issue without needing deep debugging.
Exit Code Integration and Automation
Exit statuses are central to Bash script integration and workflow automation:
- Scripts should log codes to enable debugging in downstream systems
- Calling programs should check exit values to manage failures and retries
- Production scripts should validate exit codes of called commands
- Integrations should route, alert, or handle different failure codes separately
- Automation systems must parse exit statuses to progress/sequence tasks
Without this information propagation, retries, awareness, and scriptability around failures suffer greatly.
Best practice is handling exit codes at each layer:
Further, exit values may drive automated handling for resilience:
retry_count=0
max_retries=5
until [ ${retry_count} -eq ${max_retries} ]; do
# Run sync
rsync ...
# Check status
if [ $? -eq 0 ]; then
echo "Succeeded"
break
else
((retry_count++))
echo "Failed, retrying"
fi
done
# Check if succeeded or not
if [ ${retry_count} -eq ${max_retries} ]; then
echo "Hit max retries, alerting"
send_error_event
fi
This Bash automation loop leverages exit codes for robust sync retries. The script takes action based on multi-step status flows.
So exit codes are integral for reliable scripting, orchestration, and automation.
Comparison to Other Status Communication Methods
Exit status codes provide simple yet universal status conveyance between programs, scripts, commands, and operating system layers.
However, many alternatives exist:
- Return values – Functions communicate errors via return values. More localized than exit codes.
- Exceptions – Some languages use try/catch exceptions for errors. More information rich.
- STDOUT/STDERR – Directly logging and parsing outputs. Useful but less standardized.
- Event/metrics systems – Emit script run events to monitoring tools. Requires integration.
- Exit messages – Print error messages on failures. Human readable on terminals.
- Shared files – Write status outputs to shared files or databases. Loosely coupled.
Each approach has tradeoffs – exit codes optimize for simplicity, conventions, ubiquity, and easy script integration. For distributing statuses across processes, components, scripts, and systems, they are unparalleled.
However, augmenting exit code checking with other mechanisms can provide additional monitoring, debugging, and handling benefits at the expense of more integration. Certain techniques may suit particular script use cases better.
Putting It All Together – Best Practices
Learning exit code mechanics is easy but mastering best practices takes experience. Key guidelines include:
Use Codes Consistently
- Employ exit 0 for success, exit 1 for general errors
- Leverage other standardized codes appropriately
- Avoid custom non-zero codes lacking meaning
Validate Exit Status Strategically
- Assert critical command status after execution
- Check status codes from called functions and scripts
- Review steps logs to confirm codes match expectations
Handle Codes Comprehensively
- Manage failure codes appropriately in calling routines
- Distinguish retry scenarios from errors needing alerts
- Support automated handling and integration where feasible
Log Codes for Debugging
- Print codes at function boundaries during development
- Capture error trap codes to diagnose runtime issues
- Journal codes to aid production debugging
Follow Code Modularity Practices
- Centralize common error handling routines
- Return codes from functions for caller evaluation
- Hide implementation details when useful
Mastering these exit best practices accelerates Bash script maturity for mission-critical applications.
Key Takeaways and Next Steps
Robust exit code hygiene is critical for managing errors, control flow, automation, modularity, and debugging in Bash scripting. Key lessons include:
- Exit status 0 indicates successful execution while exit 1 signals general failures
- Purposeful exit code checking enables layered handling of different errors
- Propagating exit codes drives modular design and integrated automation
- Trapping codes aids diagnosing runtime issues and unexpected termination
- Supporting exit code conventions improves portability and tool chain interoperation
With this comprehensive guide under your belt, you are ready to employ exit status codes effectively in your Bash programming. Next level up error handling, validation checks, automation, and monitoring in your scripts leveraging these best practices!