mirror of
https://github.com/kvz/bash3boilerplate.git
synced 2024-12-24 08:36:37 +00:00
d383e8c098
moved entries in changelog from `unreleased` to 2.1.0 (even though technically it's too late for that since that version was already released). added new section for 2.2.0 and added some points on what changed. bumped version to 2.2.0 in main.sh, example.sh and package.json Closes #74
367 lines
13 KiB
Bash
Executable File
367 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# This file:
|
|
#
|
|
# - Demos BASH3 Boilerplate (change this for your script)
|
|
#
|
|
# Usage:
|
|
#
|
|
# LOG_LEVEL=7 ./main.sh -f /tmp/x -d (change this for your script)
|
|
#
|
|
# Based on a template by BASH3 Boilerplate v2.2.0
|
|
# http://bash3boilerplate.sh/#authors
|
|
#
|
|
# The MIT License (MIT)
|
|
# Copyright (c) 2013 Kevin van Zonneveld and contributors
|
|
# You are not obligated to bundle the LICENSE file with your b3bp projects as long
|
|
# as you leave these references intact in the header comments of your source files.
|
|
|
|
# Exit on error. Append "|| true" if you expect an error.
|
|
set -o errexit
|
|
# Exit on error inside any functions or subshells.
|
|
set -o errtrace
|
|
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR
|
|
set -o nounset
|
|
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip`
|
|
set -o pipefail
|
|
# Turn on traces, useful while debugging but commented out by default
|
|
# set -o xtrace
|
|
|
|
if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then
|
|
if [[ "${__usage+x}" ]]; then
|
|
__b3bp_external_usage="true"
|
|
__b3bp_tmp_source_idx=1
|
|
fi
|
|
else
|
|
[[ "${__usage+x}" ]] && unset -v __usage
|
|
[[ "${__helptext+x}" ]] && unset -v __helptext
|
|
fi
|
|
|
|
# Set magic variables for current file, directory, os, etc.
|
|
__dir="$(cd "$(dirname "${BASH_SOURCE[${__b3bp_tmp_source_idx:-0}]}")" && pwd)"
|
|
__file="${__dir}/$(basename "${BASH_SOURCE[${__b3bp_tmp_source_idx:-0}]}")"
|
|
__base="$(basename "${__file}" .sh)"
|
|
|
|
# Define the environment variables (and their defaults) that this script depends on
|
|
LOG_LEVEL="${LOG_LEVEL:-6}" # 7 = debug -> 0 = emergency
|
|
NO_COLOR="${NO_COLOR:-}" # true = disable color. otherwise autodetected
|
|
|
|
|
|
### Functions
|
|
##############################################################################
|
|
|
|
function __b3bp_log () {
|
|
local log_level="${1}"
|
|
shift
|
|
|
|
# shellcheck disable=SC2034
|
|
local color_debug="\x1b[35m"
|
|
# shellcheck disable=SC2034
|
|
local color_info="\x1b[32m"
|
|
# shellcheck disable=SC2034
|
|
local color_notice="\x1b[34m"
|
|
# shellcheck disable=SC2034
|
|
local color_warning="\x1b[33m"
|
|
# shellcheck disable=SC2034
|
|
local color_error="\x1b[31m"
|
|
# shellcheck disable=SC2034
|
|
local color_critical="\x1b[1;31m"
|
|
# shellcheck disable=SC2034
|
|
local color_alert="\x1b[1;33;41m"
|
|
# shellcheck disable=SC2034
|
|
local color_emergency="\x1b[1;4;5;33;41m"
|
|
|
|
local colorvar="color_${log_level}"
|
|
|
|
local color="${!colorvar:-${color_error}}"
|
|
local color_reset="\x1b[0m"
|
|
|
|
if [[ "${NO_COLOR:-}" = "true" ]] || [[ "${TERM:-}" != "xterm"* ]] || [[ ! -t 2 ]]; then
|
|
if [[ "${NO_COLOR:-}" != "false" ]]; then
|
|
# Don't use colors on pipes or non-recognized terminals
|
|
color=""; color_reset=""
|
|
fi
|
|
fi
|
|
|
|
# all remaining arguments are to be printed
|
|
local log_line=""
|
|
|
|
while IFS=$'\n' read -r log_line; do
|
|
echo -e "$(date -u +"%Y-%m-%d %H:%M:%S UTC") ${color}$(printf "[%9s]" "${log_level}")${color_reset} ${log_line}" 1>&2
|
|
done <<< "${@:-}"
|
|
}
|
|
|
|
function emergency () { __b3bp_log emergency "${@}"; exit 1; }
|
|
function alert () { [[ "${LOG_LEVEL:-0}" -ge 1 ]] && __b3bp_log alert "${@}"; true; }
|
|
function critical () { [[ "${LOG_LEVEL:-0}" -ge 2 ]] && __b3bp_log critical "${@}"; true; }
|
|
function error () { [[ "${LOG_LEVEL:-0}" -ge 3 ]] && __b3bp_log error "${@}"; true; }
|
|
function warning () { [[ "${LOG_LEVEL:-0}" -ge 4 ]] && __b3bp_log warning "${@}"; true; }
|
|
function notice () { [[ "${LOG_LEVEL:-0}" -ge 5 ]] && __b3bp_log notice "${@}"; true; }
|
|
function info () { [[ "${LOG_LEVEL:-0}" -ge 6 ]] && __b3bp_log info "${@}"; true; }
|
|
function debug () { [[ "${LOG_LEVEL:-0}" -ge 7 ]] && __b3bp_log debug "${@}"; true; }
|
|
|
|
function help () {
|
|
echo "" 1>&2
|
|
echo " ${*}" 1>&2
|
|
echo "" 1>&2
|
|
echo " ${__usage:-No usage available}" 1>&2
|
|
echo "" 1>&2
|
|
|
|
if [[ "${__helptext:-}" ]]; then
|
|
echo " ${__helptext}" 1>&2
|
|
echo "" 1>&2
|
|
fi
|
|
|
|
exit 1
|
|
}
|
|
|
|
|
|
### Parse commandline options
|
|
##############################################################################
|
|
|
|
# Commandline options. This defines the usage page, and is used to parse cli
|
|
# opts & defaults from. The parsing is unforgiving so be precise in your syntax
|
|
# - A short option must be preset for every long option; but every short option
|
|
# need not have a long option
|
|
# - `--` is respected as the separator between options and arguments
|
|
# - We do not bash-expand defaults, so setting '~/app' as a default will not resolve to ${HOME}.
|
|
# you can use bash variables to work around this (so use ${HOME} instead)
|
|
|
|
# shellcheck disable=SC2015
|
|
[[ "${__usage+x}" ]] || read -r -d '' __usage <<-'EOF' || true # exits non-zero when EOF encountered
|
|
-f --file [arg] Filename to process. Required.
|
|
-t --temp [arg] Location of tempfile. Default="/tmp/bar"
|
|
-v Enable verbose mode, print script as it is executed
|
|
-d --debug Enables debug mode
|
|
-h --help This page
|
|
-n --no-color Disable color output
|
|
-1 --one Do just one thing
|
|
EOF
|
|
|
|
# shellcheck disable=SC2015
|
|
[[ "${__helptext+x}" ]] || read -r -d '' __helptext <<-'EOF' || true # exits non-zero when EOF encountered
|
|
This is Bash3 Boilerplate's help text. Feel free to add any description of your
|
|
program or elaborate more on command-line arguments. This section is not
|
|
parsed and will be added as-is to the help.
|
|
EOF
|
|
|
|
# Translate usage string -> getopts arguments, and set $arg_<flag> defaults
|
|
while read -r __b3bp_tmp_line; do
|
|
if [[ "${__b3bp_tmp_line}" =~ ^- ]]; then
|
|
# fetch single character version of option string
|
|
__b3bp_tmp_opt="${__b3bp_tmp_line%% *}"
|
|
__b3bp_tmp_opt="${__b3bp_tmp_opt:1}"
|
|
|
|
# fetch long version if present
|
|
__b3bp_tmp_long_opt=""
|
|
|
|
if [[ "${__b3bp_tmp_line}" = *"--"* ]]; then
|
|
__b3bp_tmp_long_opt="${__b3bp_tmp_line#*--}"
|
|
__b3bp_tmp_long_opt="${__b3bp_tmp_long_opt%% *}"
|
|
fi
|
|
|
|
# map opt long name to+from opt short name
|
|
printf -v "__b3bp_tmp_opt_long2short_${__b3bp_tmp_long_opt//-/_}" '%s' "${__b3bp_tmp_opt}"
|
|
printf -v "__b3bp_tmp_opt_short2long_${__b3bp_tmp_opt}" '%s' "${__b3bp_tmp_long_opt//-/_}"
|
|
|
|
# check if option takes an argument
|
|
if [[ "${__b3bp_tmp_line}" =~ \[.*\] ]]; then
|
|
__b3bp_tmp_opt="${__b3bp_tmp_opt}:" # add : if opt has arg
|
|
__b3bp_tmp_init="" # it has an arg. init with ""
|
|
printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "1"
|
|
elif [[ "${__b3bp_tmp_line}" =~ \{.*\} ]]; then
|
|
__b3bp_tmp_opt="${__b3bp_tmp_opt}:" # add : if opt has arg
|
|
__b3bp_tmp_init="" # it has an arg. init with ""
|
|
# remember that this option requires an argument
|
|
printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "2"
|
|
else
|
|
__b3bp_tmp_init="0" # it's a flag. init with 0
|
|
printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "0"
|
|
fi
|
|
__b3bp_tmp_opts="${__b3bp_tmp_opts:-}${__b3bp_tmp_opt}"
|
|
fi
|
|
|
|
[[ "${__b3bp_tmp_opt:-}" ]] || continue
|
|
|
|
if [[ "${__b3bp_tmp_line}" =~ (^|\.\ *)Default= ]]; then
|
|
# ignore default value if option does not have an argument
|
|
__b3bp_tmp_varname="__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}"
|
|
|
|
if [[ "${!__b3bp_tmp_varname}" != "0" ]]; then
|
|
__b3bp_tmp_init="${__b3bp_tmp_line##*Default=}"
|
|
__b3bp_tmp_re='^"(.*)"$'
|
|
if [[ "${__b3bp_tmp_init}" =~ ${__b3bp_tmp_re} ]]; then
|
|
__b3bp_tmp_init="${BASH_REMATCH[1]}"
|
|
else
|
|
__b3bp_tmp_re="^'(.*)'$"
|
|
if [[ "${__b3bp_tmp_init}" =~ ${__b3bp_tmp_re} ]]; then
|
|
__b3bp_tmp_init="${BASH_REMATCH[1]}"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [[ "${__b3bp_tmp_line}" =~ (^|\.\ *)Required\. ]]; then
|
|
# remember that this option requires an argument
|
|
printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "2"
|
|
fi
|
|
|
|
printf -v "arg_${__b3bp_tmp_opt:0:1}" '%s' "${__b3bp_tmp_init}"
|
|
done <<< "${__usage:-}"
|
|
|
|
# run getopts only if options were specified in __usage
|
|
if [[ "${__b3bp_tmp_opts:-}" ]]; then
|
|
# Allow long options like --this
|
|
__b3bp_tmp_opts="${__b3bp_tmp_opts}-:"
|
|
|
|
# Reset in case getopts has been used previously in the shell.
|
|
OPTIND=1
|
|
|
|
# start parsing command line
|
|
set +o nounset # unexpected arguments will cause unbound variables
|
|
# to be dereferenced
|
|
# Overwrite $arg_<flag> defaults with the actual CLI options
|
|
while getopts "${__b3bp_tmp_opts}" __b3bp_tmp_opt; do
|
|
[[ "${__b3bp_tmp_opt}" = "?" ]] && help "Invalid use of script: ${*} "
|
|
|
|
if [[ "${__b3bp_tmp_opt}" = "-" ]]; then
|
|
# OPTARG is long-option-name or long-option=value
|
|
if [[ "${OPTARG}" =~ .*=.* ]]; then
|
|
# --key=value format
|
|
__b3bp_tmp_long_opt=${OPTARG/=*/}
|
|
# Set opt to the short option corresponding to the long option
|
|
__b3bp_tmp_varname="__b3bp_tmp_opt_long2short_${__b3bp_tmp_long_opt//-/_}"
|
|
printf -v "__b3bp_tmp_opt" '%s' "${!__b3bp_tmp_varname}"
|
|
OPTARG=${OPTARG#*=}
|
|
else
|
|
# --key value format
|
|
# Map long name to short version of option
|
|
__b3bp_tmp_varname="__b3bp_tmp_opt_long2short_${OPTARG//-/_}"
|
|
printf -v "__b3bp_tmp_opt" '%s' "${!__b3bp_tmp_varname}"
|
|
# Only assign OPTARG if option takes an argument
|
|
__b3bp_tmp_varname="__b3bp_tmp_has_arg_${__b3bp_tmp_opt}"
|
|
printf -v "OPTARG" '%s' "${@:OPTIND:${!__b3bp_tmp_varname}}"
|
|
# shift over the argument if argument is expected
|
|
((OPTIND+=__b3bp_tmp_has_arg_${__b3bp_tmp_opt}))
|
|
fi
|
|
# we have set opt/OPTARG to the short value and the argument as OPTARG if it exists
|
|
fi
|
|
__b3bp_tmp_varname="arg_${__b3bp_tmp_opt:0:1}"
|
|
__b3bp_tmp_default="${!__b3bp_tmp_varname}"
|
|
|
|
__b3bp_tmp_value="${OPTARG}"
|
|
if [[ -z "${OPTARG}" ]] && [[ "${__b3bp_tmp_default}" = "0" ]]; then
|
|
__b3bp_tmp_value="1"
|
|
fi
|
|
|
|
printf -v "${__b3bp_tmp_varname}" '%s' "${__b3bp_tmp_value}"
|
|
debug "cli arg ${__b3bp_tmp_varname} = (${__b3bp_tmp_default}) -> ${!__b3bp_tmp_varname}"
|
|
done
|
|
set -o nounset # no more unbound variable references expected
|
|
|
|
shift $((OPTIND-1))
|
|
|
|
[[ "${1:-}" = "--" ]] && shift
|
|
fi
|
|
|
|
|
|
### Automatic validation of required option arguments
|
|
##############################################################################
|
|
|
|
for __b3bp_tmp_varname in ${!__b3bp_tmp_has_arg_*}; do
|
|
# validate only options which required an argument
|
|
[[ "${!__b3bp_tmp_varname}" = "2" ]] || continue
|
|
|
|
__b3bp_tmp_opt_short="${__b3bp_tmp_varname##*_}"
|
|
__b3bp_tmp_varname="arg_${__b3bp_tmp_opt_short}"
|
|
[[ "${!__b3bp_tmp_varname}" ]] && continue
|
|
|
|
__b3bp_tmp_varname="__b3bp_tmp_opt_short2long_${__b3bp_tmp_opt_short}"
|
|
printf -v "__b3bp_tmp_opt_long" '%s' "${!__b3bp_tmp_varname}"
|
|
[[ "${__b3bp_tmp_opt_long:-}" ]] && __b3bp_tmp_opt_long=" (--${__b3bp_tmp_opt_long//_/-})"
|
|
|
|
help "Option -${__b3bp_tmp_opt_short}${__b3bp_tmp_opt_long:-} requires an argument"
|
|
done
|
|
|
|
|
|
### Cleanup Environment variables
|
|
##############################################################################
|
|
|
|
for __tmp_varname in ${!__b3bp_tmp_*}; do
|
|
unset -v "${__tmp_varname}"
|
|
done
|
|
|
|
unset -v __tmp_varname
|
|
|
|
|
|
### Externally supplied __usage. Nothing else to do here
|
|
##############################################################################
|
|
|
|
if [[ "${__b3bp_external_usage:-}" = "true" ]]; then
|
|
unset -v __b3bp_external_usage
|
|
return
|
|
fi
|
|
|
|
|
|
### Command-line argument switches (like -d for debugmode, -h for showing helppage)
|
|
##############################################################################
|
|
|
|
# debug mode
|
|
if [[ "${arg_d:?}" = "1" ]]; then
|
|
set -o xtrace
|
|
LOG_LEVEL="7"
|
|
fi
|
|
|
|
# verbose mode
|
|
if [[ "${arg_v:?}" = "1" ]]; then
|
|
set -o verbose
|
|
fi
|
|
|
|
# no color mode
|
|
if [[ "${arg_n:?}" = "1" ]]; then
|
|
NO_COLOR="true"
|
|
fi
|
|
|
|
# help mode
|
|
if [[ "${arg_h:?}" = "1" ]]; then
|
|
# Help exists with code 1
|
|
help "Help using ${0}"
|
|
fi
|
|
|
|
|
|
### Validation. Error out if the things required for your script are not present
|
|
##############################################################################
|
|
|
|
[[ "${arg_f:-}" ]] || help "Setting a filename with -f or --file is required"
|
|
[[ "${LOG_LEVEL:-}" ]] || emergency "Cannot continue without LOG_LEVEL. "
|
|
|
|
|
|
### Runtime
|
|
##############################################################################
|
|
|
|
function cleanup_before_exit () {
|
|
info "Cleaning up. Done"
|
|
}
|
|
trap cleanup_before_exit EXIT
|
|
|
|
info "__file: ${__file}"
|
|
info "__dir: ${__dir}"
|
|
info "__base: ${__base}"
|
|
info "OSTYPE: ${OSTYPE}"
|
|
|
|
info "arg_f: ${arg_f}"
|
|
info "arg_d: ${arg_d}"
|
|
info "arg_v: ${arg_v}"
|
|
info "arg_h: ${arg_h}"
|
|
|
|
info "$(echo -e "multiple lines example - line #1\nmultiple lines example - line #2\nimagine logging the output of 'ls -al /path/'")"
|
|
|
|
# All of these go to STDERR, so you can use STDOUT for piping machine readable information to other software
|
|
debug "Info useful to developers for debugging the application, not useful during operations."
|
|
info "Normal operational messages - may be harvested for reporting, measuring throughput, etc. - no action required."
|
|
notice "Events that are unusual but not error conditions - might be summarized in an email to developers or admins to spot potential problems - no immediate action required."
|
|
warning "Warning messages, not an error, but indication that an error will occur if action is not taken, e.g. file system 85% full - each item must be resolved within a given time. This is a debug message"
|
|
error "Non-urgent failures, these should be relayed to developers or admins; each item must be resolved within a given time."
|
|
critical "Should be corrected immediately, but indicates failure in a primary system, an example is a loss of a backup ISP connection."
|
|
alert "Should be corrected immediately, therefore notify staff who can fix the problem. An example would be the loss of a primary ISP connection."
|
|
emergency "A \"panic\" condition usually affecting multiple apps/servers/sites. At this level it would usually notify all tech staff on call."
|