|

Linux CLI 58 🐧 shell functions part 2

⏱️ Estimated read: 18 min | |

#!/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 codeMeaning
0Success
1 – 255Failure (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 execution
  • return 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:

CodeMeaning
0Success
1General error
2Misuse of shell builtin
126Command found but not executable
127Command not found
128+NKilled by signal N
130Interrupted 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: return only accepts values 0–255. return 300 wraps around to 44. For larger values, use echo and 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 ]
MechanismPurposeHow to capture
echoReturn a valueresult=$(fn)
returnReturn 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.

CommandEffect
return NExit the function with status N
exit NExit 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:

SituationUse
Function reports status to its callerreturn
Function wants to stop the whole scriptexit
Validation fails — abort everythingexit 1
Helper function returns a valuereturn 0
Signal handler or cleanupexit

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: exit inside $(...) 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:

ScenarioWithout localWith local
Same-named globalOverwrittenProtected
Recursive functionVariables clashEach call separate
Nested functionsShared scopeIsolated
DebuggingHard to tracePredictable

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 local unless 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:

SituationBehavior
Simple command failsScript exits
Command in ifDoes 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 -e carefully. It’s helpful but has edge cases. Pair it with set -o pipefail to 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
  • ERR fires 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:

SignalFires when
EXITScript exits (any reason)
ERRA command returns non-zero
INTCtrl+C
TERMkill
HUPTerminal closed
DEBUGBefore 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:

FlagEffect
-eExit on error
-uError on unset variable
-o pipefailPipe 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:

SituationRecommendation
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

CodeMeaning
0Success
1General error
2Misuse
126Not executable
127Command not found
128+NKilled by signal N
130Ctrl+C

return vs exit

CommandScopePurpose
return NFunctionExit function with status N
exit NScriptExit entire script with status N

Capturing Status

MethodPurpose
$?Status of last command
if fn; then ...Test function status
fn && echo okRun on success
fn || echo failRun on failure

set Options

OptionEffect
set -eExit on error
set -uError on unset variable
set -o pipefailPipe fails if any command fails
set -xPrint commands before running
set +eDisable -e temporarily

Strict Mode

CommandMeaning
set -euo pipefailFail fast, fail loud

trap Signals

SignalFires when
EXITScript exits
ERRCommand returns non-zero
INTCtrl+C
TERMKill
HUPTerminal closed
DEBUGBefore each command

Special Variables in Traps

VariableMeaning
$LINENOCurrent line number
$BASH_COMMANDCommand being executed
$?Exit status

local

CommandEffect
local var=valFunction-local variable
local -a arrLocal array
local -A mapLocal 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

PitfallProblemSolution
Forgot localVariable leaksUse local
return 300Wraps to 44Use echo for values
exit in a helperKills the scriptUse return
set -e in ifDoesn’t exitExpected behavior
Pipe failure missedOnly last command checkedset -o pipefail
Unset variableSilent bugset -u
No cleanup on errorTemp files lefttrap ... EXIT
Error message lostNo line numberUse $LINENO
Trap overwrittenEarlier trap lostCombine or chain
Subshell exitDoesn’t exit parentAvoid 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

ConceptPurposeExample
return 0Success statusreturn 0
return 1Failure statusreturn 1
$?Last statusecho $?
if fn; thenTest statusif check; then ...
exit 0Exit script successexit 0
exit 1Exit script failureexit 1
local varFunction scopelocal tmp="$1"
set -eExit on errorset -e
set -uError on unset varset -u
set -o pipefailPipe fails on any errorset -o pipefail
set -euo pipefailStrict modeset -euo pipefail
trap ... ERRCatch errorstrap 'echo $LINENO' ERR
trap ... EXITRun on exittrap cleanup EXIT
trap ... INTHandle Ctrl+Ctrap 'exit 130' INT
$LINENOLine numberecho $LINENO
$BASH_COMMANDFailing commandecho $BASH_COMMAND

Key takeaways:

  • Functions return a status (0 = success, 1–255 = failure), not a value
  • Use return to exit a function, exit to exit the entire script
  • Capture output with $(...); capture status with $? or if fn; then
  • local keeps variables inside the function — use it for every internal variable
  • set -e exits on error; set -u errors on unset variables; pipefail catches pipe failures
  • set -euo pipefail is bash strict mode — the standard for production scripts
  • trap ... ERR catches errors with line numbers; trap ... EXIT runs cleanup
  • Use $LINENO and $BASH_COMMAND for useful error messages
  • trap ... INT handles Ctrl+C gracefully
  • return only accepts 0–255 — for larger values, use echo + $(...)
  • Use die, usage, and log helper 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.

Was this helpful?


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!