|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +# |
| 4 | +# -e: Trap when any command returns nonzero (failed) and exit. |
| 5 | +# -u: Trap and exit with an error if any undefined variable is referenced |
| 6 | +# -E: Inherit traps within functions |
| 7 | +# |
| 8 | +# -o pipefail: If any part of a pipe fails, the pipe as a whole returns a |
| 9 | +# the failure code of the first failing part of the pipe. Without this |
| 10 | +# a pipe succeeds if the last command succeeds. |
| 11 | +# |
| 12 | +# |
| 13 | +# Scripting for -e mode |
| 14 | +# ---------------------- |
| 15 | +# When running with -e, make sure to handle any commands you expect to return |
| 16 | +# nonzero with one of these patterns: |
| 17 | +# |
| 18 | +# if command args |
| 19 | +# then |
| 20 | +# : # do nothing or on-success actions here |
| 21 | +# else |
| 22 | +# ecode=$? |
| 23 | +# ... handle error here ... |
| 24 | +# fi |
| 25 | +# |
| 26 | +# |
| 27 | +# if cmdoutput=$(command args) |
| 28 | +# then |
| 29 | +# ... do what to do if it worked ... |
| 30 | +# else |
| 31 | +# ecode=$? |
| 32 | +# ... do what to do if it failed... |
| 33 | +# fi |
| 34 | +# |
| 35 | +# |
| 36 | +# ecode=0 |
| 37 | +# command args || ecode=$? |
| 38 | +# if [[ "$ecode" -ne 0 ]] |
| 39 | +# then |
| 40 | +# ... handle error here ... |
| 41 | +# fi |
| 42 | +# |
| 43 | +# Scripting for -u mode |
| 44 | +# --------------------- |
| 45 | +# |
| 46 | +# If running with -u, you can't just use $1 if $1 may not be defined, etc. If a |
| 47 | +# var may not be defined you have to conditionally access it. |
| 48 | +# |
| 49 | +# Typical patterns: |
| 50 | +# |
| 51 | +# ${var:-"default here"} |
| 52 | +# |
| 53 | +# ${var:?"error message if undefined"} |
| 54 | +# |
| 55 | +# ${var:+"only emitted if $var is defined"} |
| 56 | +# |
| 57 | +# if [[ -n "${var:-}" ]] |
| 58 | +# then |
| 59 | +# ... $var defined ... |
| 60 | +# fi |
| 61 | +# |
| 62 | +# You should generally pre-declare any globals. |
| 63 | +# |
| 64 | +# Take particular care with arrays. You cannot "${arrayvar[@]}" a possibly-undefined |
| 65 | +# array. You have to use this icky spelling: |
| 66 | +# |
| 67 | +# "${#arrayvar[@]:+"${arrayvar[@]"}" |
| 68 | +# |
| 69 | +# |
| 70 | +# Function variable scopes |
| 71 | +# ------------------------ |
| 72 | +# |
| 73 | +# Make a habit of using "local" in your functions to protect variable scopes. E.g. in this |
| 74 | +# exmple, "$arg" won't be defined outside of function foo(): |
| 75 | +# |
| 76 | +# function foo() { |
| 77 | +# local arg="${1:-default}" |
| 78 | +# do_something_with $arg |
| 79 | +# } |
| 80 | +# foo "${global}" |
| 81 | +# |
| 82 | +# To return multiple values from functions and return efficiently, use output globals. |
| 83 | +# It's gross, but so is bash. E.g.: |
| 84 | +# |
| 85 | +# function bar() { |
| 86 | +# local x="${1:-}" |
| 87 | +# bar_out="$(( $x ++ ))" |
| 88 | +# } |
| 89 | +# bar 1 |
| 90 | +# echo ${bar_out} |
| 91 | +# |
| 92 | +# While we are here |
| 93 | +# ----------------- |
| 94 | +# |
| 95 | +# You can use 'printf' to set dynamic output variables. |
| 96 | +# |
| 97 | +# printf -v $VARNAME "whatever value" |
| 98 | +# |
| 99 | +# and "${!VARNAME}" to access them by name-reference. |
| 100 | +# |
| 101 | +# But it's better to use "declare -A" and associative arrays where possible, |
| 102 | +# since they're less awful than dynamic variable names. |
| 103 | +# |
| 104 | +# If you want to preserve spaces and quoting while building up a command |
| 105 | +# use an array and "${x[@]}" expansion, e.g. |
| 106 | +# |
| 107 | +# declare -a cmd_and_args=("cmd" "arg1" "arg 2 kept together") |
| 108 | +# cmd_and_args+=("arg 3 is not split by spaces") |
| 109 | +# # run it, preserving quoting and arg groupings |
| 110 | +# "${cmd_and_args[@]}" |
| 111 | +# |
| 112 | +# The 'read' command is great for simply splitting up input, and can |
| 113 | +# be used with a here-string "<<<". |
| 114 | +# |
| 115 | +# read -r VAR1 VAR2 VAR3 <<<"var1input var2input var3input" |
| 116 | +# |
| 117 | +# It respects IFS so you can use it to split on tabs etc. |
| 118 | +# |
| 119 | + |
| 120 | + |
| 121 | + |
| 122 | + |
| 123 | +set -e -u -E -o pipefail |
| 124 | + |
| 125 | +if [ -n "${DEBUG:-}" ] |
| 126 | +then |
| 127 | + set -x -o functrace |
| 128 | +fi |
| 129 | + |
| 130 | +# Backtrace an error and then exit with the the original exit code |
| 131 | +# |
| 132 | +function trap_err() { |
| 133 | + filename=${BASH_SOURCE[1]:+"${BASH_SOURCE[1]}"} |
| 134 | + fileline=${BASH_LINENO[$lineno]:+"${BASH_LINENO[$lineno]}"} |
| 135 | + echo 1>&2 "ERROR: unexpected exit code $ecode at $filename:$lineno" |
| 136 | + echo "Backtrace is:" |
| 137 | + i=0 |
| 138 | + while btline=$(caller $i) |
| 139 | + do |
| 140 | + if ! read -r line sub file <<<"$btline" |
| 141 | + then |
| 142 | + echo " $btline" |
| 143 | + elif [[ "$sub" == "main" || "${sub}" == "" ]] |
| 144 | + then |
| 145 | + echo " $file:$line" |
| 146 | + else |
| 147 | + echo " $file:$line $sub()" |
| 148 | + fi |
| 149 | + i=$((i+1)) |
| 150 | + done |
| 151 | + exit $ecode |
| 152 | +} |
| 153 | + |
| 154 | +# This trap runs on anything that would cause an exit due to -e |
| 155 | +# |
| 156 | +# It does not run on "exit n" for nonzero exit, or on -u traps |
| 157 | +# or other internal shell errors. |
| 158 | +# |
| 159 | +# If you want that, you can add another trap on EXIT. |
| 160 | +# |
| 161 | +trap 'ecode=$?;lineno=$LINENO;set +x;trap_err' ERR |
| 162 | + |
| 163 | +# Example of an exit trap. |
| 164 | +# |
| 165 | +# If you want to use this you must also put a "trap EXIT" before all your |
| 166 | +# intentional exits, e.g. |
| 167 | +# |
| 168 | +# trap EXIT |
| 169 | +# exit 1 |
| 170 | +# |
| 171 | +trap 'ecode=$?;lineno=$LINENO;set +x;trap EXIT;trap_err' EXIT |
| 172 | + |
| 173 | +# For testing. Source this file, then call foo() in the other file, maybe |
| 174 | +# within a few nested functions, or maybe via sourcing some other files. also |
| 175 | +# try ref'ing an undefined var |
| 176 | +# |
| 177 | +#function foo() { |
| 178 | +# false |
| 179 | +#} |
0 commit comments