#!/bin/bash # fail ERROR-MESSAGE # # Outputs parameters (ERROR-MESSAGE) passed to 'fail' to stderr # # If a function 'fail_cleanup' exists, execute it after outputting # error message. This allows scripts to perform any necessary script # specific cleanup on failure. # # If 'no_exit' is set, or 'script_name' is 'bash', # do not exit the process and return an error. This lets you chain # error handlers or run the 'irepo' functions from a bash command line # via: # . /opt/scripts/lib/irepo # declare -g fail_failed=0 declare -g fail_min_stack=1 declare -g fail_skip_continue=0 declare -g fail_skip_filepath="/home/user/.${script_name}.skip_continue_target" fail() { local tmp=$? if (( fail_failed )); then exit 1 fi fail_failed=${tmp} # On exit 1 from fail, trap is called again. We do not want to display # the stack frame for fail a second time, so set the fail_min_stack # as the limit. if (( ${#} != 0 )); then echo -e "$*" | while read -r line; do echo "FAIL: ${line}" >&2 done fail_min_stack=0 else echo -e "FAIL: ERR trap triggered: ${script_name} ${full_command}" >&2 fail_min_stack=1 fi if (( fail_skip_continue )); then echo ${BASH_LINENO[0]} > "${fail_skip_filepath}" fi # https://bashwizard.com/function-call-stack-and-backtraces/ declare CALLSTACK= if (( ${#FUNCNAME[*]} > fail_min_stack )); then CALLSTACK=$(local i ; for (( i=0; i<${#FUNCNAME[*]}; i++ )); do echo "FAIL: ${FUNCNAME[$i]}@${BASH_SOURCE[$i]}:${BASH_LINENO[$i-1]}" done) fi echo "${CALLSTACK}" >&2 if [[ "${FAILURE_TO}" != "" ]]; then echo "Attempting to email ${FAILURE_TO} of failure." >&2 declare _dir=$(pwd) _temp=$(mktemp -d) _filename="error-$(date +"%Y%m%d").log" { if [[ -e "${repositories}/.lock" ]]; then echo -e "Lockfile contents:\n" cat "${repositories}/.lock" else echo -e "No lockfile present during failure." fi echo "" echo -e "Callstack:\n" echo "${CALLSTACK}" echo "" echo -e "Environment:\n" export echo "" } >> "${_temp}/${_filename}" cd "${IREPO_HOME}/server" if [[ -e "${log_file}" ]]; then extra_attachments="--files=${log_file}" else extra_attachments="" fi export NODE_EXTRA_CA_CERTS="/usr/local/share/ca-certificates/IntelSHA256RootCA-base64.crt" npm run mailer -- \ --to="${FAILURE_TO}" \ --from="${FAILURE_FROM:-james.p.ketrenos@intel.com}" \ --type="failure" \ --files="${_temp}/${_filename}" \ "${extra_attachments}" cd "${_dir}" rm -rf "${_temp}" fi # If we are in an interactive shell, do not call exit or the shell will # terminate. if [[ "${-}" =~ .*i.* ]]; then kill -INT $$ fi # execute fail_cleanup if it is declared if [[ $(type -t fail_cleanup) == function ]]; then fail_cleanup fi exit 1 } trap "fail" ERR fail_on_exit() { if (( $# == 0 )); then fail "fail_on_exit must be provided a return code" fi local ret=$1 local message=$2 message="${message:-unspecified command}" if [[ ${ret} -ne 0 ]]; then fail "${message} failed: ${ret}" return ${ret} fi } int() { [ "$1" -eq "$1" ] || fail "$1 is not an integer." ; echo $1 ; } declare -g skip_continue_target=0 # skip_continue # # If called, skip_continue will enable line tracking in the fail ERR trap. # On failure, the file '/home/user/.${script_name}.skip_continue_target' # On exectuion, skip_continue will check the lineno contained in that # file and only return SUCCESS (0) if the current Bash line number # of the caller >= skip_continue_target. # # NOTE: This will not work with nested functions calling skip_continue # from functions not in the main script. skip_continue() { fail_skip_continue=1 if (( fail_failed )); then show_vals fail_failed exit ${fail_failed} fi if (( ! skip_continue_target )) && [[ -f "${fail_skip_filepath}" ]]; then skip_continue_target=$(cat "${fail_skip_filepath}") fi # Get the line of the skip_continue call local line_no=${BASH_LINENO[0]} line_no=$((line_no+1)) # Read two lines to see if the skip_continue has the command on # the actual line, or on the following line. Set "line" to that # line for display in the Running or Skipped output mapfile lines < <(head -n ${line_no} "${BASH_SOURCE[1]}"| tail -n 2) if [[ "${lines[0]}" =~ \&\&[[:space:]\\]*\#? ]]; then line="${lines[1]}" else line="${lines[0]%*skip_continue &&}" fi if (( line_no >= skip_continue_target )); then echo "Running (${line_no}): ${line%$'\n'}" return 0 else echo "Skipping (${line_no}): ${line%$'\n'}" return 1 fi } # array_to_csv ARRAY [DELIM] # array_to_csv() { declare -n _arr="${1}" local delim="${2}" local joined delim=${delim:-,} printf -v joined "%s${delim}" "${_arr[@]}" joined="${joined%"${delim}"}" joined="${joined#"${delim}"}" echo "${joined}" } # csv_to_array ARRAY CSV [DELIM] # csv_to_array() { declare -n _arr="${1}" _arr="${2}" declare _delim="${3:-,}" mapfile -d "${_delim}" -t _arr <<< "${_arr}" count=${#_arr[@]} _arr[count-1]="${_arr[count-1]%$'\n'}" } # pop TARGET ARRAY # # removes the first element from ARRAY and stores in TARGET pop() { declare -n _target="${1}" declare -n _arr="${2}" if (( ${#_arr[@]} == 0 )); then _target="" return 1 fi _target="${_arr[0]}" _arr=("${_arr[@]:1}") return 0 } # parse_arguments COMMAND-LINE-ARGUMENTS # # Converts 'arguments' variable to long and short parameter definitions # # Parses COMMAND-LINE-ARGUMENTS and sets 'opts' as the parsed parameters # to be used by caller in switch statement. # # NOTE: # Optional parameters must be passed to the command line as # '-param=VALUE' and not as '-param VALUE' or the optional parameter # value will not be parsed and will be moved to unparsed parameters. # # Correct: # app -has-optional=5 -other-param # # Incorrect: # app -has-optional 5 -other-param # # Non-optional parameters can use space, however for consistency it is # recommended that usage help show users to use '=' even for non-optional # parameters. # # See 'arg_example' later in this file for sample usage. # parse_arguments() { local arg_debug=0 if [[ ${#arguments[@]} == 0 ]]; then fail "No command line arguments options in '\$arguments'" fi local sopts local lopts local arg local _arguments local mandatory=() local valid=() for arg in "${arguments[@]}"; do arg="${arg%%\#*}" # Trim off any Help text local -a argument_set csv_to_array argument_set "${arg}" '|' # Convert to array for argument in "${argument_set[@]}"; do trimmed=${argument%:} # Trim off 'value required' indicator trimmed=${trimmed%:} # Trim off 'value optional' indicator if [[ "${argument:0:1}" == '*' ]]; then # Check if mandatory argument="${argument:1}" # Strip off '*' trimmed="${trimmed:1}" # Strip off '*' mandatory_arg="${argument_set[*]//:/ }" # Remove :+ from argument_set mandatory_arg="${mandatory_arg//$'*'}" # Remove '*'+ from argument_set mandatory+=("${mandatory_arg}") # Add argument_set to mandatory fi valid+=("${trimmed}") if [[ ${#trimmed} == 1 ]]; then sopts="$argument${sopts}" else if [[ "${lopts}" == "" ]]; then lopts=${argument} else lopts=${lopts},${argument} fi fi done done (( arg_debug )) && echo "lopts: ${lopts}" >&2 (( arg_debug )) && echo "sopts: ${sopts}" >&2 # Only accept exact parameters, do not let getopt try and "fit" for opt in "${@}"; do if [[ ! "${opt}" =~ ^- ]]; then continue fi if [[ "${opt}" == "--" ]]; then break fi opt=${opt#-} opt=${opt%%=*} if [[ ! " ${valid[*]} " =~ [[:space:]]${opt}[[:space:]] ]]; then echo "ERROR: '-${opt}' is not a valid parameter." >&2 return 1 fi done opts=$(getopt \ -a \ --longoptions "${lopts}" \ --name "$(basename "$0")" \ --options "${sopts}" \ -- "$@" ) if [[ $? -ne 0 ]]; then (( arg_debug )) && echo "opts: ${opts}" fail "\nSee '$(basename "${0}") -h' for valid options.\n" fi # Enforce mandatory options for arg in "${mandatory[@]}"; do provided=0 for opt in ${opts}; do if [[ "${opt}" == "--" ]]; then break fi for acceptable in ${arg}; do if [[ "${opt}" =~ ^-\-?"${acceptable}"$ ]]; then provided=1 break fi done done if (( ! provided )); then local last_arg=${arg% } last_arg=${last_arg##* } echo "ERROR: '-${last_arg}=' is a mandatory requirement and must be supplied." >&2 return 1 fi done return 0 } # Example function demonstrating argument declaration and parsing. # # You can copy this into your entry script, change the name, # and then call it in the start of your application passing in all # of the command line arguments, for example: # # copy arg_example => process_arguments # process_arguments ${@} # # And then adapt as fit for your application's parameters. # # To run the arg_example script, you can run irepo with # arg_example_show set to a value. For example: # # arg_example_show=1 irepo -h -lp -required-parameter=5 # arg_example() { arguments=( "h|help#This help text." "lp" "long-parameter" "rp:" "required-parameter:" "hop::" "has-optional-parameter:" ) parse_arguments "${@}" eval set -- "$opts" remaining=$# while (( $# > 0 )); do if [[ ${remaining} == 0 ]]; then fail "case statement is not shifting off values correctly." fi remaining=$((remaining-1)) # Uncomment to help debug case statement: # echo "Processing: \$1='$1' \$2='$2'" case "${1}" in # NOTE: Short options only have a single dash -h|--help) echo "...show help..." ;; # NOTE: Long options always have a double dash, even though the # application reads them from the command-line as a single dash --lp|--long-parameter) echo "...do work for long-parameter..." ;; --rp|--required-parameter) value=${2} shift # Remove the argument from the processing list echo "...do work for 'required-parameter=${value}'..." ;; --hop|--has-optional-parameter) value=default if [[ "${2}" != "" ]]; then value="${2}" fi shift # Remove the optional argument from the processing list echo "...do work for 'has-optional-parameter=${value}'..." ;; --) shift break ;; esac shift done } # show_vals VARIABLES # # Helper function for use in debugging. Typical usage: # # show_vals [OPTIONS] VARIABLE1 VARIABLE2 ... VARIABLEn # # Available options: -json -deshadow # -json Outputs as JSON # -deshadow Remove shadow_ prefix from any variable name # show_vals() { declare -i json=0 declare -i deshadow=0 # Eventually replace this with actual arg parsing, but since # only two options are possible, just check twice to parse both if [[ "${1}" == "-json" ]]; then json=1 shift fi if [[ "${1}" == "-deshadow" ]]; then deshadow=1 shift fi if [[ "${1}" == "-json" ]]; then json=1 shift fi if [[ "${1}" == "-deshadow" ]]; then deshadow=1 shift fi local var local value declare -i first=1 if (( json )); then echo "{" fi for var in "${@}"; do if (( deshadow )); then var_name="${var/shadow_}" else var_name="${var}" fi if (( first )); then first=0 else if ((json )); then echo "," fi fi if [[ "${!var}" == "" ]]; then if (( json )); then echo -n " \"${var_name}\": undefined" else echo "${var_name} = '' or unset" fi continue fi value=$(sed -nE "s#declare -a ${var}=\((.*)\)#\1#p" < \ <(declare -p "${var}")) if (( json )); then if [[ "${value}" == "" ]]; then echo -n " \"${var_name}\": \"${!var}\"" else eval declare -A values=(${value}) echo -n " \"${var_name}\": [" declare -i first_arr=1 for item in "${values[@]}"; do if (( first_arr )); then first_arr=0 else echo -n ", " fi echo -n "\"${item}\"" done echo -n "]" fi else if [[ "${value}" == "" ]]; then echo "${var_name} = ${!var}" else echo "${var_name}[] = ${value}" fi fi done if (( json )); then echo -e "\n}" fi } export yes # ask VAR OPTS PROMPT # # Will display PROMPT OPTS ? with OPTS split by | and default wrapped # in (). VAR is set to selected option. # # If OPTS contains a capital letter, that is used as the default if # ENTER is pressed. # # Usage: # ask ret yN "Are you sure" # if [[ "${ret}" == "n" ]]; then echo No; fi # if [[ "${ret}" == "y" ]]; then echo Yes; fi # # NOTE: If 'yes' is set in the enviornment, and 'y' is in the option # then 'y' value will be immediately returned after the prompt is shown. # # Will prompt: Are you sure y|(N)? # If neither y, n, or ENTER is pressed, the prompt will loop. # 'res' contains the character the user selected (lowercase) ask() { if [[ "${1}" == "-h" ]] || (( $# < 3 )); then fail "ask RETVAR OPTS PROMPT" fi declare -n _ret="${1}" local opts="${2}a" # Ny - N is default; ynC - C is default; Ynm - Y is default shift 2 local prompt="${*}" local index local opts_str local default # Loop through each character in opts and build opt_str # for display to read prompt. while [[ "${opts:${index}:1}" != "" ]]; do local c=${opts:${index}:1} index=$((index+1)) if [[ ${c} =~ [A-Z] ]]; then if [[ "${default}" != "" ]]; then fail "Only one character can be default: ${opts} ${default} ${c}" fi if [[ "${opts_str}" != "" ]]; then opts_str="${opts_str}|(${c})" else opts_str="(${c})" fi default=${c,,} else if [[ "${opts_str}" != "" ]]; then opts_str="${opts_str}|${c}" else opts_str="${c}" fi fi done if (( yes )) && [[ ${opts} =~ [yY] ]]; then # echo "${FUNCNAME[1]}@${BASH_SOURCE[1]}: ${prompt} ${opts_str}? y" echo "${prompt} ${opts_str}? y" _ret="y" return 0 fi local REPLY while read -n1 -r -p "${prompt} ${opts_str}? " REPLY; do case $REPLY in "a") fail "Abort" ;; "") if [[ "${default}" != "" ]]; then _ret="${default,,}" return 0 fi ;; *) echo "" if [[ ${REPLY,,} =~ [${opts,,}] ]]; then _ret="${REPLY,,}" return 0 fi ;; esac done } # default_usage # # Display default usage based on ${arguments} # # 'arguments' is an ARRAY of strings. # # Each string is of the form [*]NAME[:](|NAME2[:])#Help text. # If the leading '*' is present, this parameter is mandatory. # If a name is followed by a single ':", that parameter takes a value. # If name is followed by two colons ('::') the parameter is optional. # |NAME2[:] provides the ability to list both short and long parameter # options for a given parameter. # The help text is displayed right aligned. # If a variable NAME or shadow_NAME exists and is set, its value is # displayed in the Help text as the 'Default'. # default_generate_help_text() { for arg in "${@}"; do local _help local _default= local _help_text="${arg#*\#}" # Strip ^.*# from _help_text if [[ "${_help_text}" == "${arg}" ]]; then _help_text="" fi mapfile -t _help_text < <(fold -s -w 50 <<< "${_help_text}") if [[ "${arg:0:1}" == '*' ]]; then # Check if is_mandatory arg="${arg:1}" # Strip off '*' is_mandatory=1 else is_mandatory=0 fi arg="${arg%%\#*}" # Trim off any help text local -a argument_set csv_to_array argument_set "${arg}" '|' # Convert to array for argument in "${argument_set[@]}"; do if (( ${#argument_set[@]} > 1 )); then argument_value_name=${argument_set[-1]} else argument_value_name=${argument} fi if [[ "${argument}" == "${argument_set[0]}" ]]; then first_argument=1 else first_argument=0 fi if [[ "${argument}" == "${argument_set[-1]}" ]]; then last_argument=1 else last_argument=0 fi if (( ${#argument_set[@]} > 1 )); then more_than_one_argument=1 else more_than_one_argument=0 fi # If takes option, trim : off end of names if [[ "${argument:0-1}" == ":" ]]; then takes_value=1 argument_value_name=${argument_value_name:0:-1} argument="${argument%:}" # Trim off the trailing : else takes_value=0 fi # If option is optional, trim : off end of names if [[ "${argument:0-1}" == ":" ]]; then takes_value=0 takes_optional_value=1 argument_value_name=${argument_value_name:0:-1} argument="${argument%:}" # Trim off the trailing : else takes_optional_value=0 fi if (( is_mandatory )) && (( takes_optional_value )); then fail "'${argument}' Mandatory (*) requirements can not take optional values (::)" fi # If this is the first argument, initialize _help from first item # in _help_text. _default="". _default will be set later # if this argument takes parameters. if (( first_argument )); then pop _help _help_text _default="" showed_help=0 fi if (( takes_value )) || (( takes_optional_value )); then # If a shadow_ variant exists, use that as the default, # otherwise use the argument_value_name (with - changed to _) declare -n _shadow="shadow_${argument_value_name//-/_}" if [[ "${_shadow}" == "" ]]; then declare -n _shadow="${argument_value_name//-/_}" fi # Make the display name for a value for # foo-bar-rabbit into RABBIT argument_value_name=${argument_value_name^^} argument_value_name=${argument_value_name##*-} # First argument. Initializes _default. if (( first_argument )); then if (( is_mandatory )); then _default="Mandatory: ${argument_value_name} option must be provied." else if [[ "${_shadow}" != "" ]]; then _default="Default: $(array_to_csv _shadow)" else _default="Default: no default" fi fi fi # If this is the last argument in the set and the current help # text is empty and there is no more _help_text, set the # _help_text to default if (( last_argument )) && [[ "${_help}" == "" ]] && (( ${#_help_text[@]} == 0 )) ; then mapfile -t _help_text < <(fold -s -w 50 <<< "${_default}") _default="" pop _help _help_text fi if (( takes_optional_value )); then equals="[=${argument_value_name}]" else equals="=${argument_value_name}" fi if (( first_argument )); then # If there is more than one argument in the set, add a | to the # output and do not display _default if (( more_than_one_argument )); then printf " %-30s %s\n" \ "-${argument}${equals}|" \ "${_help}" showed_help=1 else printf " %-30s %s\n" \ "-${argument}${equals}" \ "${_help}" showed_help=1 fi else # (( ! first_argument )) # If not last optional argument, add a | to the output and do # not display _help if (( ! last_argument )); then printf " %-30s %s\n" \ "-${argument}${equals}|" \ "${_help}" showed_help=1 else printf " %-30s %s\n" \ "-${argument}${equals}" \ "${_help}" showed_help=1 fi fi else # (( ! takes_value )) && (( ! takes_optional_value )) if (( first_argument )); then option_string="" # If there is more than one argument in the set, add a | to the # output if (( more_than_one_argument )); then option_string="${option_string}-${argument}|" else # (( last_argument )) printf " %-30s %s\n" "-${argument}" "${_help}" showed_help=1 fi else # (( ! first_argument )) # If last non-optional argument, display help if (( last_argument )); then printf " %-30s %s\n" "${option_string}-${argument}" "${_help}" showed_help=1 else option_string="${option_string}-${argument}|" fi fi fi if (( showed_help )); then #show_vals argument argument_set first_argument last_argument more_than_one_argument _help _help_text _default is_mandatory takes_value takes_optional_value showed_help _shadow pop _help _help_text fi done # done for 'arg in "${argument_set[@]}"'' # If there is still a help string, display it. while [[ "$_help" != "" ]] || (( ${#_help_text[@]} > 0 )); do printf " %-30s %s\n" "" "${_help}" if ! pop _help _help_text; then if [[ "${_default}" == "" ]]; then break fi mapfile -t _help_text < <(fold -s -w 50 <<< "${_default}") _default="" pop _help _help_text fi done _help="" _default="" done } default_usage() { local _mandatory=() for arg in "${arguments[@]}"; do if [[ "${arg:0:1}" == '*' ]]; then # Check if is_mandatory arg="${arg%#*}" # Strip off help arg="${arg:1:-1}" # Strip off '*' csv_to_array arg "${arg}" '|' _mandatory+=("${arg[-1]}") fi done local _text="Usage: ${script_name} [OPTIONS] ${usage_extra}" for _arg in "${_mandatory[@]}"; do _text="${_text} -${_arg}=${_arg^^}" done mapfile -t _text < <(fold -s -w 72 <<< "${_text}") first_line=1 for _line in "${_text[@]}"; do if (( first_line )); then echo -e "\n${_line}" first_line=0 else echo " ${_line}" fi done echo -e "\nOptions:\n" default_generate_help_text "${arguments[@]}" echo "" }