Linux CLI 58 🐧 shell functions part 2
#!/bin/bash
set -e
set -u
check_success() {
local result=$1
if [ $result -eq 0 ]; then
echo "Command succeeded."
return 0
else
echo "Command failed."
return 1
fi
}
check_exit() {
local result=$1
if [ $result -eq 0 ]; then
echo "Exiting with success status."
exit 0
else
echo "Exiting with failure status."
exit 1
fi
}
echo "Starting the script..."
some_command || check_success $?
if [ $? -ne 0 ]; then
echo "Command failed with exit code: $?"
fi
# Trap errors and handle them manually.
trap 'echo "An error occurred on line ${LINENO}: ${BASH_COMMAND}" >&2; exit 1' ERR
check_exit 0 && echo "Exited successfully." || echo "Exited unsuccessfully."
Part 1 covered how to define and call functions. Part 2 is about making them robust — how functions report success or failure, how to stop a script when something breaks, and how to catch errors before they cascade. These techniques separate a fragile script from one you can trust in production.
Key point: In shell scripting, success is 0 and failure is anything else. Functions use return to report status, exit to terminate the whole script, and set -e/trap to handle errors automatically. local keeps variables from leaking between functions and the global scope.
a – shell functions return codes
Function return codes are used to indicate the success or failure of a function’s execution. They’re also known as exit statuses.
The rule:
| Return code | Meaning |
|---|---|
0 | Success |
1 – 255 | Failure (or a specific error) |
Inside a function, use return:
check_success() {
local result=$1
if [ $result -eq 0 ]; then
echo "Command succeeded."
return 0
else
echo "Command failed."
return 1
fi
}
return 0→ signals successful executionreturn 1→ signals non-successful execution
Capturing the return code with $?:
$ check_success 0
[ Command succeeded. ]
$ echo $?
[ 0 ]
$ check_success 1
[ Command failed. ]
$ echo $?
[ 1 ]
$? always holds the exit status of the last command — including a function call.
Using the return code in an if:
if check_success 0; then
echo "All good"
else
echo "Something failed"
fi
[ Command succeeded. ]
[ All good ]
The if tests the function’s exit status directly — no need to check $? explicitly.
Returning specific error codes:
validate_file() {
if [ ! -e "$1" ]; then
return 1 # not found
fi
if [ ! -r "$1" ]; then
return 2 # not readable
fi
if [ ! -s "$1" ]; then
return 3 # empty
fi
return 0 # ok
}
validate_file /etc/passwd
echo "Status: $?"
[ Status: 0 ]
validate_file /nonexistent
echo "Status: $?"
[ Status: 1 ]
Standard exit codes:
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Misuse of shell builtin |
| 126 | Command found but not executable |
| 127 | Command not found |
| 128+N | Killed by signal N |
| 130 | Interrupted by Ctrl+C |
Chaining with && and ||:
$ check_success 0 && echo "OK" || echo "FAIL"
[ Command succeeded. ]
[ OK ]
$ check_success 1 && echo "OK" || echo "FAIL"
[ Command failed. ]
[ FAIL ]
⚠️ Warning:
returnonly accepts values 0–255.return 300wraps around to 44. For larger values, useechoand capture with$(...)instead.
Return code vs output — don’t confuse them:
add() {
local sum=$(( $1 + $2 ))
echo $sum # this is OUTPUT
return 0 # this is STATUS
}
result=$(add 5 3) # captures OUTPUT ("8")
status=$? # captures STATUS (0)
echo "Result: $result, Status: $status"
[ Result: 8, Status: 0 ]
| Mechanism | Purpose | How to capture |
|---|---|---|
echo | Return a value | result=$(fn) |
return | Return a status | $? or if fn |
b – shell function exit command and local variables
The exit command:
exit terminates script execution based on some condition. Unlike return, which only exits the function, exit stops the entire script.
| Command | Effect |
|---|---|
return N | Exit the function with status N |
exit N | Exit the script with status N |
Examples:
check_exit() {
local result=$1
if [ $result -eq 0 ]; then
echo "Exiting with success status."
exit 0
else
echo "Exiting with failure status."
exit 1
fi
}
check_exit 0
echo "This line never runs"
[ Exiting with success status. ]
# script ends here
$ ./script.sh
[ Exiting with success status. ]
$ echo $?
[ 0 ]
When to use return vs exit:
| Situation | Use |
|---|---|
| Function reports status to its caller | return |
| Function wants to stop the whole script | exit |
| Validation fails — abort everything | exit 1 |
| Helper function returns a value | return 0 |
| Signal handler or cleanup | exit |
A common pattern — die:
die() {
echo "FATAL: $1" >&2
exit 1
}
if [ ! -f config.txt ]; then
die "config.txt not found"
fi
exit in a subshell doesn’t exit the parent:
$ (exit 1); echo "still here"
[ still here ]
$ exit 1
# shell exits
Note:
exitinside$(...)or a pipe only exits the subshell, not the main script.
Local variables:
Local variables are used to define variables within a specific scope — inside a function. They are not accessible outside the function, and they don’t interfere with global variables of the same name.
Declaring a local variable:
local result=$1
Example — without local:
count=0
increment() {
count=$(( count + 1 )) # modifies GLOBAL count
echo "Inside: $count"
}
increment
increment
echo "Outside: $count"
[ Inside: 1 ]
[ Inside: 2 ]
[ Outside: 2 ]
The function changed the global count. That might be intentional — or a bug.
Example — with local:
count=0
increment() {
local count=0 # LOCAL copy
count=$(( count + 1 ))
echo "Inside: $count"
}
increment
increment
echo "Outside: $count"
[ Inside: 1 ]
[ Inside: 1 ]
[ Outside: 0 ]
Each call gets its own count. The global stays untouched.
Why local matters:
| Scenario | Without local | With local |
|---|---|---|
| Same-named global | Overwritten | Protected |
| Recursive function | Variables clash | Each call separate |
| Nested functions | Shared scope | Isolated |
| Debugging | Hard to trace | Predictable |
Example — a function that uses a temp variable:
process_file() {
local file="$1"
local tmp
tmp=$(mktemp)
cp "$file" "$tmp"
# ... process ...
rm -f "$tmp"
}
Without local, file and tmp would leak into the global scope and might clobber other variables.
local with arrays:
myfunc() {
local -a items=("a" "b" "c")
local -A map=([key]="value")
echo "${items[0]}"
echo "${map[key]}"
}
Reading a global from inside a function:
GLOBAL="hello"
myfunc() {
local local_var="world"
echo "$GLOBAL $local_var"
}
myfunc
[ hello world ]
You can read globals. You just shouldn’t write to them unless intended.
Common patterns:
# Preserve a global by making a local copy
myfunc() {
local PATH="$PATH:/opt/bin" # doesn't change global PATH
command_that_needs_opt_bin
}
# Isolate a loop counter
count_items() {
local i
for i in "$@"; do
echo "$i"
done
}
Best practice: Declare every variable inside a function as
localunless you specifically want to modify the global scope. This prevents subtle bugs.
c – shell functions error handling
Error handling is an essential aspect of writing robust shell scripts. The goal is simple: when something fails, don’t keep going as if nothing happened. Here are the main strategies.
Strategy 1 — Using return codes:
The simplest approach. Check the status of every command and act on it.
#!/bin/bash
backup() {
cp "$1" "$2" || return 1
return 0
}
if backup /etc/passwd /tmp/; then
echo "Backup succeeded"
else
echo "Backup failed" >&2
exit 1
fi
Strategy 2 — set -e — exit on error:
#!/bin/bash
set -e
echo "Starting"
false # command fails
echo "Never reached"
[ Starting ]
# script exits with status 1
With set -e, the script exits immediately if any command returns non-zero. This catches errors early — no more “kept running after failure.”
Caveats of set -e:
| Situation | Behavior |
|---|---|
| Simple command fails | Script exits |
Command in if | Does not exit |
Command in && / || | Does not exit |
Command in a pipe (unless pipefail) | Only last command checked |
Command in $() | Exits if the substitution fails |
set -e
false && echo "no" # doesn't exit — false is part of &&
if false; then :; fi # doesn't exit — false is in if
false # exits
Tip: Use
set -ecarefully. It’s helpful but has edge cases. Pair it withset -o pipefailto catch failures in the middle of a pipe.
set -e
set -o pipefail
cat missing.txt | grep foo # exits — pipefail catches cat
Strategy 3 — set -u — fail on undefined variables:
#!/bin/bash
set -u
echo "$UNDEFINED"
[ line 4: UNDEFINED: unbound variable ]
With set -u, referencing an unset variable is an error. This catches typos and missing values.
Use defaults with -u:
set -u
echo "${NAME:-Guest}" # OK — has default
echo "${NAME:-}" # OK — empty default
echo "$NAME" # ❌ error if NAME unset
Strategy 4 — trap — catch errors and clean up:
trap runs a command when a signal or error occurs. The most common use is trap ... ERR and trap ... EXIT.
#!/bin/bash
trap 'echo "An error occurred on line ${LINENO}: ${BASH_COMMAND}" >&2; exit 1' ERR
ERRfires when a command returns non-zero${LINENO}is the line number${BASH_COMMAND}is the command that failed
Example — error with cleanup:
#!/bin/bash
set -e
TMPFILE=$(mktemp)
cleanup() {
echo "Cleaning up $TMPFILE"
rm -f "$TMPFILE"
}
trap cleanup EXIT
echo "Working..."
echo "data" > "$TMPFILE"
# ... do work ...
# cleanup runs automatically on exit (normal or error)
Common trap signals:
| Signal | Fires when |
|---|---|
EXIT | Script exits (any reason) |
ERR | A command returns non-zero |
INT | Ctrl+C |
TERM | kill |
HUP | Terminal closed |
DEBUG | Before each command |
Example — trap on Ctrl+C:
#!/bin/bash
trap 'echo "Interrupted! Cleaning up..."; exit 130' INT
echo "Press Ctrl+C to stop"
while true; do
sleep 1
done
$ ./script.sh
Press Ctrl+C to stop
^C
[ Interrupted! Cleaning up... ]
$ echo $?
[ 130 ]
Combining strategies:
#!/bin/bash
set -euo pipefail
trap 'echo "Error on line ${LINENO}: ${BASH_COMMAND}" >&2' ERR
cleanup() {
rm -f "$TMP"
}
trap cleanup EXIT
TMP=$(mktemp)
main() {
echo "Starting"
false # triggers ERR trap and exits due to set -e
}
main "$@"
Breaking down set -euo pipefail:
| Flag | Effect |
|---|---|
-e | Exit on error |
-u | Error on unset variable |
-o pipefail | Pipe fails if any command fails |
This combination is often called “bash strict mode” — it makes scripts fail loudly and early instead of silently continuing.
When to use strict mode:
| Situation | Recommendation |
|---|---|
| Production scripts | ✅ Use strict mode |
| Scripts that modify data | ✅ Use strict mode |
| One-off scripts | ⚠️ Optional |
| Scripts with expected failures | ⚠️ Handle carefully |
| Interactive helpers | ⚠️ May be too strict |
Graceful degradation — combining set -e with explicit error handling:
#!/bin/bash
set -e
# This failure is OK — don't exit
if ! grep -q "pattern" file.txt; then
echo "Pattern not found (not an error)"
fi
# This failure SHOULD exit
critical_command
A complete error-handling template:
#!/bin/bash
set -euo pipefail
TMP_FILES=()
cleanup() {
for f in "${TMP_FILES[@]}"; do
rm -f "$f"
done
}
trap cleanup EXIT
error_handler() {
local exit_code=$?
local line_no=$1
echo "Error at line $line_no (exit code $exit_code)" >&2
exit "$exit_code"
}
trap 'error_handler $LINENO' ERR
log() {
echo "[$(date +%T)] $*"
}
main() {
log "Starting"
local tmp
tmp=$(mktemp)
TMP_FILES+=("$tmp")
log "Using temp file: $tmp"
# ... real work ...
log "Done"
}
main "$@"
This template covers:
- Strict mode — fail fast
- Cleanup on exit
- Error reporting with line numbers
- Logging with timestamps
Complete Example Session
# ============================================
# PART 1: RETURN CODES
# ============================================
$ check_success() {
> local result=$1
> if [ $result -eq 0 ]; then
> echo "Command succeeded."
> return 0
> else
> echo "Command failed."
> return 1
> fi
> }
$ check_success 0
[ Command succeeded. ]
$ echo $?
[ 0 ]
$ check_success 1
[ Command failed. ]
$ echo $?
[ 1 ]
# ============================================
# PART 2: USING IN IF
# ============================================
$ if check_success 0; then echo "OK"; fi
[ Command succeeded. ]
[ OK ]
$ if check_success 1; then echo "OK"; else echo "FAIL"; fi
[ Command failed. ]
[ FAIL ]
# ============================================
# PART 3: EXIT COMMAND
# ============================================
$ check_exit() {
> local result=$1
> if [ $result -eq 0 ]; then
> echo "Exiting with success status."
> exit 0
> else
> echo "Exiting with failure status."
> exit 1
> fi
> }
$ check_exit 0 && echo "Exited successfully." || echo "Exited unsuccessfully."
[ Exiting with success status. ]
[ Exited successfully. ]
$ echo $?
[ 0 ]
# ============================================
# PART 4: LOCAL VS GLOBAL
# ============================================
$ count=0
$ increment() {
> count=$(( count + 1 ))
> echo "Inside: $count"
> }
$ increment
$ increment
$ echo "Outside: $count"
[ Inside: 1 ]
[ Inside: 2 ]
[ Outside: 2 ]
$ count=0
$ increment() {
> local count=0
> count=$(( count + 1 ))
> echo "Inside: $count"
> }
$ increment
$ increment
$ echo "Outside: $count"
[ Inside: 1 ]
[ Inside: 1 ]
[ Outside: 0 ]
# ============================================
# PART 5: SET -E
# ============================================
$ cat > test-e.sh << 'EOF'
#!/bin/bash
set -e
echo "Starting"
false
echo "Never reached"
EOF
$ chmod +x test-e.sh
$ ./test-e.sh
[ Starting ]
$ echo $?
[ 1 ]
# ============================================
# PART 6: SET -U
# ============================================
$ cat > test-u.sh << 'EOF'
#!/bin/bash
set -u
echo "Value: $UNDEFINED"
echo "Never reached"
EOF
$ chmod +x test-u.sh
$ ./test-u.sh
[ test-u.sh: line 3: UNDEFINED: unbound variable ]
$ echo $?
[ 1 ]
# ============================================
# PART 7: SET -E DOESN'T TRIGGER IN IF
# ============================================
$ cat > test-if.sh << 'EOF'
#!/bin/bash
set -e
if false; then
echo "won't run"
fi
echo "Still here"
EOF
$ chmod +x test-if.sh
$ ./test-if.sh
[ Still here ]
# ============================================
# PART 8: TRAP ERR
# ============================================
$ cat > test-trap.sh << 'EOF'
#!/bin/bash
trap 'echo "Error on line ${LINENO}: ${BASH_COMMAND}" >&2; exit 1' ERR
echo "Starting"
false
echo "Never reached"
EOF
$ chmod +x test-trap.sh
$ ./test-trap.sh
[ Starting ]
[ Error on line 4: false ]
$ echo $?
[ 1 ]
# ============================================
# PART 9: TRAP EXIT FOR CLEANUP
# ============================================
$ cat > test-cleanup.sh << 'EOF'
#!/bin/bash
set -e
TMP=$(mktemp)
cleanup() {
echo "Cleaning up $TMP"
rm -f "$TMP"
}
trap cleanup EXIT
echo "Working with $TMP"
echo "data" > "$TMP"
# cleanup runs on exit
EOF
$ chmod +x test-cleanup.sh
$ ./test-cleanup.sh
[ Working with /tmp/tmp.aB3xYz ]
[ Cleaning up /tmp/tmp.aB3xYz ]
# ============================================
# PART 10: STRICT MODE
# ============================================
$ cat > strict.sh << 'EOF'
#!/bin/bash
set -euo pipefail
trap 'echo "Error on line ${LINENO}: ${BASH_COMMAND}" >&2' ERR
main() {
echo "Starting"
false
echo "Never reached"
}
main "$@"
EOF
$ chmod +x strict.sh
$ ./strict.sh
[ Starting ]
[ Error on line 9: false ]
$ echo $?
[ 1 ]
# ============================================
# PART 11: FULL ERROR TEMPLATE
# ============================================
$ cat > template.sh << 'EOF'
#!/bin/bash
set -euo pipefail
TMP_FILES=()
cleanup() {
for f in "${TMP_FILES[@]}"; do
rm -f "$f"
done
}
trap cleanup EXIT
error_handler() {
local exit_code=$?
local line_no=$1
echo "Error at line $line_no (exit code $exit_code)" >&2
exit "$exit_code"
}
trap 'error_handler $LINENO' ERR
log() {
echo "[$(date +%T)] $*"
}
main() {
log "Starting"
local tmp
tmp=$(mktemp)
TMP_FILES+=("$tmp")
log "Using temp file: $tmp"
log "Done"
}
main "$@"
EOF
$ chmod +x template.sh
$ ./template.sh
[ [10:00:00] Starting ]
[ [10:00:00] Using temp file: /tmp/tmp.aB3xYz ]
[ [10:00:00] Done ]
Quick Reference
Return Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Misuse |
| 126 | Not executable |
| 127 | Command not found |
| 128+N | Killed by signal N |
| 130 | Ctrl+C |
return vs exit
| Command | Scope | Purpose |
|---|---|---|
return N | Function | Exit function with status N |
exit N | Script | Exit entire script with status N |
Capturing Status
| Method | Purpose |
|---|---|
$? | Status of last command |
if fn; then ... | Test function status |
fn && echo ok | Run on success |
fn || echo fail | Run on failure |
set Options
| Option | Effect |
|---|---|
set -e | Exit on error |
set -u | Error on unset variable |
set -o pipefail | Pipe fails if any command fails |
set -x | Print commands before running |
set +e | Disable -e temporarily |
Strict Mode
| Command | Meaning |
|---|---|
set -euo pipefail | Fail fast, fail loud |
trap Signals
| Signal | Fires when |
|---|---|
EXIT | Script exits |
ERR | Command returns non-zero |
INT | Ctrl+C |
TERM | Kill |
HUP | Terminal closed |
DEBUG | Before each command |
Special Variables in Traps
| Variable | Meaning |
|---|---|
$LINENO | Current line number |
$BASH_COMMAND | Command being executed |
$? | Exit status |
local
| Command | Effect |
|---|---|
local var=val | Function-local variable |
local -a arr | Local array |
local -A map | Local associative array |
Best Practices
✅ Do This:
# Use local for all internal variables
myfunc() {
local tmp="$1"
} # ✅
# Use return for status
check() {
[ -f "$1" ] && return 0 || return 1
} # ✅
# Use echo + $() for values
add() {
echo $(( $1 + $2 ))
}
result=$(add 5 3) # ✅
# Use strict mode in production scripts
set -euo pipefail # ✅
# Trap EXIT for cleanup
trap cleanup EXIT # ✅
# Trap ERR for diagnostics
trap 'echo "Error at line $LINENO" >&2' ERR # ✅
# Use defaults with -u
echo "${NAME:-Guest}" # ✅
# Provide usage on bad args
if [ $# -lt 2 ]; then
echo "Usage: $0 <a> <b>" >&2
exit 1
fi # ✅
# Use a die function
die() { echo "FATAL: $1" >&2; exit 1; } # ✅
❌ Don’t Do This:
# Don't forget local
myfunc() {
tmp="$1" # ❌ leaks
}
# Don't return values > 255
return 300 # ❌ wraps to 44
# Don't use exit inside a function you might want to reuse
helper() {
exit 1 # ❌ kills the whole script
} # ✅ use return instead
# Don't set -e without understanding the caveats
set -e
if false; then :; fi # doesn't exit
false && true # doesn't exit
# Don't pipe without pipefail
cat missing | grep foo # ⚠️ only grep's status checked
set -o pipefail # ✅ catches cat's failure
# Don't use unset variables
echo "$UNDEFINED" # ⚠️ silent with no -u
set -u # ✅ makes it an error
# Don't skip cleanup on error
# Always trap EXIT # ✅
# Don't ignore the return code
myfunc # ⚠️ did it fail?
if ! myfunc; then echo "failed"; fi # ✅ check it
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
Forgot local | Variable leaks | Use local |
return 300 | Wraps to 44 | Use echo for values |
exit in a helper | Kills the script | Use return |
set -e in if | Doesn’t exit | Expected behavior |
| Pipe failure missed | Only last command checked | set -o pipefail |
| Unset variable | Silent bug | set -u |
| No cleanup on error | Temp files left | trap ... EXIT |
| Error message lost | No line number | Use $LINENO |
| Trap overwritten | Earlier trap lost | Combine or chain |
| Subshell exit | Doesn’t exit parent | Avoid exit in $() |
Real-World Examples
1. Check Function with Return
#!/bin/bash
check_success() {
local result=$1
if [ $result -eq 0 ]; then
echo "Command succeeded."
return 0
else
echo "Command failed."
return 1
fi
}
check_success 0
[ Command succeeded. ]
2. Exit on Failure
#!/bin/bash
check_exit() {
local result=$1
if [ $result -eq 0 ]; then
echo "Exiting with success status."
exit 0
else
echo "Exiting with failure status."
exit 1
fi
}
check_exit 0 && echo "Exited successfully."
[ Exiting with success status. ]
[ Exited successfully. ]
3. Local Variable Isolation
#!/bin/bash
name="global"
show() {
local name="local"
echo "Inside: $name"
}
show
echo "Outside: $name"
[ Inside: local ]
[ Outside: global ]
4. Strict Mode
#!/bin/bash
set -euo pipefail
echo "Running safely"
ls /etc/passwd
[ Running safely ]
[ /etc/passwd ]
5. Error Trap
#!/bin/bash
trap 'echo "Error on line ${LINENO}: ${BASH_COMMAND}" >&2; exit 1' ERR
echo "Starting"
false
echo "Never reached"
[ Starting ]
[ Error on line 4: false ]
6. Cleanup Trap
#!/bin/bash
set -e
TMP=$(mktemp)
cleanup() {
echo "Cleaning up $TMP"
rm -f "$TMP"
}
trap cleanup EXIT
echo "data" > "$TMP"
echo "Done"
[ Done ]
[ Cleaning up /tmp/tmp.aB3xYz ]
7. Ctrl+C Handler
#!/bin/bash
trap 'echo "Interrupted"; exit 130' INT
echo "Press Ctrl+C"
sleep 100
[ Press Ctrl+C ]
^C
[ Interrupted ]
8. Die Function
#!/bin/bash
die() {
echo "FATAL: $1" >&2
exit 1
}
if [ ! -f config.txt ]; then
die "config.txt not found"
fi
[ FATAL: config.txt not found ]
9. Validate Arguments
#!/bin/bash
usage() {
echo "Usage: $0 <src> <dst>" >&2
exit 1
}
if [ $# -ne 2 ]; then
usage
fi
10. Validate File
#!/bin/bash
validate_file() {
if [ ! -e "$1" ]; then return 1; fi
if [ ! -r "$1" ]; then return 2; fi
if [ ! -s "$1" ]; then return 3; fi
return 0
}
if ! validate_file "$1"; then
case $? in
1) echo "File not found" ;;
2) echo "File not readable" ;;
3) echo "File is empty" ;;
esac
fi
11. Retry with Backoff
#!/bin/bash
retry() {
local max=$1
local delay=$2
shift 2
local i
for ((i=1; i<=max; i++)); do
if "$@"; then
return 0
fi
echo "Attempt $i failed, retrying in ${delay}s..."
sleep "$delay"
delay=$(( delay * 2 ))
done
return 1
}
retry 3 1 curl -s https://example.com
12. Full Error Template
#!/bin/bash
set -euo pipefail
TMP_FILES=()
cleanup() {
for f in "${TMP_FILES[@]}"; do
rm -f "$f"
done
}
trap cleanup EXIT
error_handler() {
echo "Error at line $1 (exit $?)" >&2
exit 1
}
trap 'error_handler $LINENO' ERR
log() { echo "[$(date +%T)] $*"; }
main() {
log "Starting"
local tmp
tmp=$(mktemp)
TMP_FILES+=("$tmp")
log "Using $tmp"
}
main "$@"
[ [10:00:00] Starting ]
[ [10:00:00] Using /tmp/tmp.aB3xYz ]
13. Graceful Degradation
#!/bin/bash
set -e
# Optional command — ignore failure
if ! grep -q pattern file.txt; then
echo "Pattern not found (ok)"
fi
# Required command — fail on error
critical_command
14. Log Errors Without Exiting
#!/bin/bash
log_error() {
echo "[ERROR] $*" >&2
}
process() {
if ! cp "$1" "$2"; then
log_error "Failed to copy $1 to $2"
return 1
fi
}
process file.txt /tmp/ || log_error "Processing failed"
15. Subshell vs Main Shell
#!/bin/bash
(exit 1)
echo "Still here"
[ Still here ]
# But:
exit 1
# never reached
16. Trap Multiple Signals
#!/bin/bash
cleanup() {
echo "Cleaning up"
exit 0
}
trap cleanup INT TERM HUP
echo "Running..."
sleep 100
17. Using set -x for Debugging
#!/bin/bash
set -x
name="Alice"
echo "Hello, $name"
[ + name=Alice ]
[ + echo 'Hello, Alice' ]
[ Hello, Alice ]
18. Conditional Strict Mode
#!/bin/bash
if [ "${DEBUG:-0}" = "1" ]; then
set -euo pipefail
set -x
fi
echo "Running"
19. Function with Timeout
#!/bin/bash
with_timeout() {
local seconds=$1
shift
timeout "$seconds" "$@"
}
with_timeout 5 sleep 10
echo "Status: $?"
[ Status: 124 ]
20. Preserve Global PATH
#!/bin/bash
add_to_path() {
local PATH="$PATH:$1"
export PATH
echo "Local PATH: $PATH"
}
add_to_path /opt/bin
echo "Global PATH: $PATH"
[ Local PATH: ...:/opt/bin ]
[ Global PATH: ... ] # unchanged
Visual: Error Handling Flow
┌──────────────────────────────────────────────┐
│ Without error handling │
│ │
│ cmd1 ✓ │
│ cmd2 ✗ ← fails, but script continues │
│ cmd3 ✓ │
│ cmd4 ✓ │
│ "Done" ← misleading! │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ With set -e │
│ │
│ cmd1 ✓ │
│ cmd2 ✗ ← script exits immediately │
│ (never runs) │
│ │
│ Exit code = 1 │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ With trap ERR + EXIT │
│ │
│ cmd1 ✓ │
│ cmd2 ✗ → ERR trap fires │
│ → logs line number + command │
│ → EXIT trap fires │
│ → cleanup runs │
│ Exit code = 1 │
│ │
└──────────────────────────────────────────────┘
Visual: return vs exit
┌──────────────────────────────────────────────┐
│ Script: │
│ │
│ helper() { │
│ return 1 ← exits FUNCTION only │
│ } │
│ ││ main() { │
│ helper │
│ echo "Still here" ← still runs │
│ exit 1 ← exits SCRIPT │
│ echo "Never" ← never runs │
│ } │
│ │
│ main │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Purpose | Example |
|---|---|---|
return 0 | Success status | return 0 |
return 1 | Failure status | return 1 |
$? | Last status | echo $? |
if fn; then | Test status | if check; then ... |
exit 0 | Exit script success | exit 0 |
exit 1 | Exit script failure | exit 1 |
local var | Function scope | local tmp="$1" |
set -e | Exit on error | set -e |
set -u | Error on unset var | set -u |
set -o pipefail | Pipe fails on any error | set -o pipefail |
set -euo pipefail | Strict mode | set -euo pipefail |
trap ... ERR | Catch errors | trap 'echo $LINENO' ERR |
trap ... EXIT | Run on exit | trap cleanup EXIT |
trap ... INT | Handle Ctrl+C | trap 'exit 130' INT |
$LINENO | Line number | echo $LINENO |
$BASH_COMMAND | Failing command | echo $BASH_COMMAND |
Key takeaways:
- Functions return a status (0 = success, 1–255 = failure), not a value
- Use
returnto exit a function,exitto exit the entire script - Capture output with
$(...); capture status with$?orif fn; then localkeeps variables inside the function — use it for every internal variableset -eexits on error;set -uerrors on unset variables;pipefailcatches pipe failuresset -euo pipefailis bash strict mode — the standard for production scriptstrap ... ERRcatches errors with line numbers;trap ... EXITruns cleanup- Use
$LINENOand$BASH_COMMANDfor useful error messages trap ... INThandles Ctrl+C gracefullyreturnonly accepts 0–255 — for larger values, useecho+$(...)- Use
die,usage, andloghelper functions for consistent scripts - Always check return codes — a script that ignores failure is a script that lies
Remember: Error handling is what separates a script from a reliable script. Use local for every internal variable. Use return for status and echo for values. Use set -euo pipefail in production. Use trap ... EXIT to clean up, and trap ... ERR to report. Check every return code — if fn; then and || die "message" are your friends. Master these, and your scripts will fail loudly, cleanly, and predictably instead of silently corrupting data.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!