Files
shell_sequencer/sequencer.sh

1710 lines
45 KiB
Bash
Executable File

#!/usr/bin/env bash
# shellcheck disable=SC2034 # variable not used
# shellcheck disable=SC1090 # follow non constant source
enableErrorCheck() {
# Do not allow use of unbound variables.
# Use ${VAR:-} if possibly unbound
set -o nounset
}
disableErrorCheck() {
# Do allow the use of unbound vars.
set +o nounset
}
# Exit on error. Append "|| true" if you expect an error.
set -o errexit
# Exit on error inside any functions or subshells.
set -o errtrace
enableErrorCheck
# 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
## Globals
{
readonly sqr_version=16
readonly sqr_versionMajor=0
readonly sqr_versionMinor=0
readonly sqr_versionString="${sqr_version}.${sqr_versionMajor}.${sqr_versionMinor}"
## Seq
readonly seq_name="${_sqn_alias:-${0##*/}}"
readonly seq_dir="$(cd -- "$(dirname -- "${0}")" && pwd)"
readonly seq_origin="$(cd -- "$(dirname -- "$(readlink -f -- "${0}")")" && pwd)"
readonly seq_file="$(basename -- "${0}")"
readonly seq_fileName="${seq_file%%.*}"
readonly seq_self="${seq_origin}/${seq_file}"
# shellcheck disable=SC2015 # && || is not if else
readonly seq_invocation="$(printf '%q' "${0}")$( (($#)) && printf ' %q' "$@" || true)"
readonly seq_template="seqTemplate.sh"
readonly _seq_configDirName=".seqs"
_seq_configEdit=0
_seq_profileList=
_seq_stepReturn=255
seq_args=
seq_configFile= # Filled by initSeqConfig
seq_profileName=
seq_configRoot="${HOME}/${_seq_configDirName}"
# May be overwritten by seq
seq_configName="${seq_fileName}.cfg"
seq_configTemplate="${0%.*}.cfg.example"
## Sequencer
readonly sqr_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly sqr_file="$(basename -- "${BASH_SOURCE[0]}")"
readonly sqr_name="${sqr_file%%.*}"
readonly sqr_origin="$(cd -- "$(dirname -- \
"$(readlink -f -- "${BASH_SOURCE[0]}")")" && pwd)"
sqr_args=
sqr_missingConf=missingConf.log
_sqr_contextHelp=0
_sqr_contextExe=0
_sqr_debug=0
_sqr_dry=0
_sqr_editor=
_sqr_errno=0
_sqr_interactive=1
_sqr_single=0
readonly _sqr_stepMax=512
_sqr_verbose=0
# Part of a pipe or output to terminal
_sqr_term=0 && [ -t 1 ] && _sqr_term=1
readonly _sqr_term
_sqr_colorAlways=0
## Terminal position
_sqr_savePosAlias= ;[ -t 1 ] && _sqr_savePosAlias='\033[1A\033[1C\033[s\033[1B\033[1C'
_sqr_savePosExe= ;[ -t 1 ] && _sqr_savePosExe='\033[s'
_sqr_savePos= ;[ -t 1 ] && _sqr_savePos='\033[3D\033[s\033[3C'
_sqr_restorePos= ;[ -t 1 ] && _sqr_restorePos='\033[u'
## Terminal colors
col_black='\033[0;30m'
col_darkgrey='\033[1;30m'
col_red='\033[0;31m'
col_lightred='\033[1;31m'
col_green='\033[0;32m'
col_lightgreen='\033[1;32m'
col_orange='\033[0;33m'
col_yellow='\033[1;33m'
col_blue='\033[0;34m'
col_lightblue='\033[1;34m'
col_purple='\033[0;35m'
col_lightpurple='\033[1;35m'
col_cyan='\033[0;36m'
col_lightcyan='\033[1;36m'
col_lightgray='\033[0;37m'
col_white='\033[1;37m'
## No Color
col_off='\033[0m'
col() {
local colVar="col_${1:-"off"}"
((_sqr_term)) || ((_sqr_colorAlways)) && echo "${!colVar}"
true
}
}
helpSequencer() {
cat <<USAGE_EOF
Usage: ${seq_name} [OPTIONS] [STEP NUMBER(s) or ALIAS] [STEP ARGUMENTS]
[OPTIONS]
--all, -a : Run all steps regardless of continuity
--color : Output color codes also within pipe
--config, -c : Open sequence configuration file (also sets -qq)
--dry-run, -d : Only print to console what would be done
! Attention - Sequence must support this
--help, -h : Display help
--helpapi, -ha : Display help about build-in supporting functions
(e.g. exe,addconf,echerr,...)
--liststeps, -ls : List all step numbers and alias
--profile, -p : Sequence configuration profile name (default: "default")
(if supported by sequence)
-pl : List available profiles
--quiet, -q : Don't ask for permission to execute steps
If called without starting step number, only this help is shown
--silent, -qq : Same as --quiet but suppresses regular sequencer.sh output
--single, -s : Execute only one step
If more than one step is requested, only the first will be executed
--verbose, -v : Verbose output (use exe() function to call shell commands in seqs)
( e.g.: exe apt update )
--version : Display version of sequencer and revision of sequence
-- : End options marker
++ : Quick start step 1 (-qq) and skipping [STEP NUMBER(s) or ALIAS]
[STEP NUMBER"(s)" 1-${_sqr_stepMax} or ALIAS]
No STEP or ALIAS : assume 1 as starting point
Single STEP or ALIAS : starting point of sequential process
Multiple STEPS or ALIAS : execute only given steps
(e.g. $0 "2 4 12")
multiple steps need to be given as string
[STEP ARGUMENTS]
* : Arguments will be passed to selected steps and step infos as:
\$2 ...
\$1 is always the step number
USAGE_EOF
}
helpApi(){
cat <<USAGE_API
sequencer.sh API
The sequencer.sh build-in functions are available in all sequence functions:
- seq_config
If optional step_config is defined in the sequence, it will be called once before execution of steps.
- step_[1-${_sqr_stepMax}]_info
- step_[1-${_sqr_stepMax}]_alias
- step_[1-${_sqr_stepMax}]
sequencer.sh global variables:
\${LOG_LEVEL}
Control log level directly
4 - debug -> 0 - silent
\${LOG_TIME}
1 - Show time stamps before log outputs
\${sqr_args}
String of all given options
\${seq_configRoot}
Path to user specific seq configuration directory
\${seq_configFile}
Path to user specific seq configuration file
Will be empty if unused
\${seq_profileName}
Profile string selected with -p argument
sequencer.sh build-in functions:
USAGE_API
echo -e "$(col green) root$(col off)"
cat <<USAGE_API
Returns true if current user is root.
e.g. root || echo "Not root"
USAGE_API
echo -e "$(col green) running$(col off)"
cat <<USAGE_API
Returns true if same script already runs.
e.g. running && exit 1
USAGE_API
echo -e "$(col green) exists [OPTIONS] [ELEMENT]$(col off)"
cat <<USAGE_API
[ELEMENT]
: either a variable- or a funtion name (-f)
[OPTIONS]
-f : a function
-- : end of options
USAGE_API
echo -e "$(col green) quiet$(col off)"
cat <<USAGE_API
Returns true if seq runs non-interactive (-q|-qq).
USAGE_API
echo -e "$(col green) silent$(col off)"
cat <<USAGE_API
Returns true if seq runs interactive.
USAGE_API
echo -e "$(col green) dry$(col off)"
cat <<USAGE_API
Returns true if seq runs in dry-run mode (-d)
USAGE_API
echo -e "$(col green) verbose$(col off)"
cat <<USAGE_API
Returns true if seq runs in verbose mode (-v)
USAGE_API
echo -e "$(col green) contextHelp$(col off)"
cat <<USAGE_API
Returns true if current output is only for help purposes.
USAGE_API
echo -e "$(col green) contextExe$(col off)"
cat <<USAGE_API
Returns true if current output is in execution mode.
USAGE_API
echo -e "$(col green) confirm [OPTIONS] [QUESTION]$(col off)"
cat <<USAGE_API
Default (empty character) = no
Invalid character trigger the default
[OPTIONS]
-f : interactive even if quiet
-n : no input help
-y : default = yes
-- : end of options
USAGE_API
echo -e "$(col green) ask [OPTION] [QUESTION] [DEFAULT]$(col off)"
cat <<USAGE_API
Will ask for input even if quiet, when [DEFAULT] is empty
[OPTION]
-e : allow empty input
Ignored if [DEFAULT] is available
-s : ask for secret (don't print input)
Does not add a newline. Usage:
pass=\$(ask -s "Password"); echo
-- : End of options
USAGE_API
echo -e "$(col green) exe [COMMANDLINE]$(col off)"
cat <<USAGE_API
Execute command line without pipes or redirects (>,<,|).
Supporting: dry-run (-d): only print command without execution
verbose (-v): print command before execution
USAGE_API
echo -e "$(col green) exep \"[COMMAND STRING(s)]\"$(col off)"
cat <<USAGE_API
See exe, but support for pipes or redirects.
e.g.: exep echo hello world \\> \\'out put.log\\'
exep echo hello world \\> out\\\\ put.log
exep "echo hello world > 'out put.log'"
exep "echo hello world > out\\ put.log"
Important:
- Shell commands cd, read, ... won't work because [COMMAND STRING(s)] is started in a new shell.
- All apostrophes need to be esacped since the command line is given as string.
USAGE_API
echo -e "$(col green) escpath <PATH>$(col off)"
cat <<USAGE_API
Escaping non-printable characters with the proposed POSIX $'' syntax
e.g. \$(escpath /my own/ho me/path) = $(escpath /my own/ho me/path)
USAGE_API
echo -e "$(col green) initSeqConfig [OPTION] <NAME> [TEMPLATE]$(col off)"
cat <<USAGE_API
Create a configuration file in ${seq_configRoot}/ and source it if already existent.
[OPTION]
-p : Use profiles
-t : Source config also if created from template
-e : Create empty configuration if no template is found
Returns
0 : sourced configuration or
(-t) : created and sourced configuration from template
1 : created configuration from template but not sourced
2 : created empty configuration
3 : No configuration created
USAGE_API
echo -e "$(col green) addConf <OPTIONS> [SOURCE TYPE] <SOURCE> <DESTINATION FILE>$(col off)"
cat <<USAGE_API
Trying to write or append text or a file (<SOURCE>) to a destination file.
If the CONFIGFILE exists, a backup (name_%Y%m%d-%H%M%S.bck) is saved at the same location.
If -s fails or -m, "$(realpath "${sqr_missingConf}")" is created with the conflicts
to be resolved by the user.
<OPTIONS>
-c : create a new file
-a : append to existing file
-s : skip if CONFIGFILE exists (no backup and entry in missing conf)
-m : only add content to missing conf and warn user
[SOURCE TYPE]
-f : <SOURCE> is a file
<SOURCE>
Text or file (-f) to create or added to <DESTINATION FILE>
<DESTINATION FILE>
Target file to be created or modified.
USAGE_API
echo -e "$(col green) step <STEP NUMBER OR ALIAS>$(col off)"
cat <<USAGE_API
Executes a single step also by alias. Useful if step numbers get reorganized.
dry-run is not applied in this function! The executed step is responsible.
USAGE_API
echo -e "$(col green) color [FOREGROUND COLOR] [BACKGROUND COLOR]$(col off)"
cat <<USAGE_API
Set output color permanently until reset.
No argument or unknown foreground color restores shell default (reset).
Color reset happens after every step and step_info function call.
[COLOR]: black, red, green, yellow, blue, magenta, cyan, white
USAGE_API
echo -e "$(col green) info|warning|error|debug [OPTIONS] [STRING]$(col off)"
cat <<USAGE_API
Output [STRING] (can be multiline) to stdout according to log level.
[OPTIONS]
-a : Append [STRING] skipping leading " [ ] " prefix
-d : No leading " [ ] " prefix
-e : Output to stderr
-n : No newline
USAGE_API
echo -e "$(col green) echoinfo [...]$(col off)"
cat <<USAGE_API
echo additional correctly indented line to step info
[...] : all parameter are forwared to echo
USAGE_API
echo -e "$(col green) echoinfoArgs [...]$(col off)"
cat <<USAGE_API
echo argument description after step number or alias.
This must be called first in the step info function.
Does not add a newline at the end.
[...] : no parameter are forwared to echo
USAGE_API
echo -e "$(col green) endIfEmpty <VARIABLENAME> [DESCRIPTION]$(col off)"
cat <<USAGE_API
exit 6 if variable is empty
<VARIABLENAME> : Name without \$
[DESCRIPTION] : Additional text for error output
USAGE_API
echo -e "$(col green) saveReturn [ERRORCODE]$(col off)"
cat <<USAGE_API
Save ERRORCODE if it is != 0 for later use with endReturn
USAGE_API
echo -e "$(col green) getReturn$(col off)"
cat <<USAGE_API
Return last saved error code
USAGE_API
echo -e "$(col green) endReturn [OPTIONS] [MESSAGE]$(col off)"
cat <<USAGE_API
Notifys user that there was an error (previously saved by saveReturn,
or -o [ERRORCODE]) and asks to continue or end the sequence.
Always exits with evaluated error code.
[OPTIONS]
-f : force exit without user input, if error code is not 0
-o ERRORCODE : override stored error code and check ERRORCODE
[MESSAGE]
String which is displayed in the error output
USAGE_API
}
## Logging
{
readonly log_fatal=0
readonly log_error=1
readonly log_warning=2
readonly log_info=3
readonly log_debug=4
LOG_LEVEL="${LOG_LEVEL:-"${log_info}"}" # 4 = debug -> 0 = fatal (stop)
LOG_TIME="${LOG_TIME:-}" # 1 = show time stamps
# sqr::log [LOG LEVEL] [LOG_COLOR] [OPTIONS] [LOG MESSAGE]
# Construct log messages
#
# [OPTIONS]
# -a : append text (placeholder for info and timestamp)
# Uses color from last sqr::log call without -a
# -d : no info and timestamp
# -e : Output to stderr
# -n : no newline
# -- : End of options
#
sqr::log () {
#sqr::debugPause
local appendText=
local direct=0 # no prefix and timestamp
local newline='\n'
local col_end=
col_end="$(col off)"
local outp='/dev/stdout'
local log_level="${1:-}"
shift
local log_color="${1:-}"
shift
# Only output newline on empty args
if ! (( $# )) ; then
direct=1
newline='\n'
fi
while getopts "aden" arg; do
case "${arg}" in
a) appendText=1 ;;
d) direct=1 ;;
e) outp='/dev/stderr' ;;
n) newline='' ;;
*) ;;
esac
done
shift "$((OPTIND-1))"; OPTIND=1
[[ -z "${log_color}" ]] && col_end=""
# all remaining arguments are to be printed
local log_line=""
while IFS=$'\n' read -r log_line ; do
printf '%b' "${log_color}" >${outp}
if ! (( direct )) ; then
if [[ -n "${LOG_TIME}" ]] ; then
if (( appendText )) ; then
printf '%19s' "" >${outp}
else
printf '%s' "$(date +"%Y-%m-%d %H:%M:%S")" >${outp}
fi
fi
if (( ! appendText )) ; then
printf " %3s " "[${log_level}]" >${outp}
printf "%s" "${log_line}" >${outp}
else
# +4 : " [] "
printf "%$((${#log_level} + 4))s%s" "" "${log_line}" >${outp}
fi
else
# direct output
printf '%s' "${log_line}"
fi
printf '%b'"${newline}" "${col_end}" >${outp}
done <<< "${@:-}"
sqr::debugContinue
}
fatal () { sqr::log "stop" "$(col red)" "${@}"; exit 1; }
die () { sqr::log "end" "" "${@}"; exit 1; }
error () { [[ "${LOG_LEVEL:-0}" -ge ${log_error} ]] && sqr::log "e" "$(col red)" "${@}"; true; }
warning () { [[ "${LOG_LEVEL:-0}" -ge ${log_warning} ]] && sqr::log "w" "$(col orange)" "${@}"; true; }
info () { [[ "${LOG_LEVEL:-0}" -ge ${log_info} ]] && sqr::log "i" "" "${@}"; true; }
debug () { [[ "${LOG_LEVEL:-0}" -ge ${log_debug} ]] && sqr::log "dbug" "$(col lightpurple)" "${@}"; true; }
# internal print(s) same loglevel as error
# shellcheck disable=SC2059 # don't use variables in format
sqr::printf () { [[ "${LOG_LEVEL:-0}" -ge ${log_error} ]] && printf "$@"; true; }
sqr::echo () { [[ "${LOG_LEVEL:-0}" -ge ${log_error} ]] && echo "$@"; true; }
sqr::debugPause() {
if (( _sqr_debug )) ; then set +o xtrace; else true; fi
}
sqr::debugContinue() {
if (( _sqr_debug )) ; then set -o xtrace; else true; fi
}
# color <FOREGROUND COLOR> [BACKGROUND COLOR]
color() {
[ ! -t 1 ] && return 0
[ -z "${1:-}" ] && tput sgr0 && return 0
case "${1:-}" in
black)
tput setaf 0 ;;
red)
tput setaf 1 ;;
green)
tput setaf 2 ;;
yellow)
tput setaf 3 ;;
blue)
tput setaf 4 ;;
magenta)
tput setaf 5 ;;
cyan)
tput setaf 6 ;;
white)
tput setaf 7 ;;
none)
tput sgr0
return 0 ;;
*)
tput setaf "${1:-}" ;;
esac
case "${2:-}" in
black)
tput setab 0 ;;
red)
tput setab 1 ;;
green)
tput setab 2 ;;
yellow)
tput setab 3 ;;
blue)
tput setab 4 ;;
magenta)
tput setab 5 ;;
cyan)
tput setab 6 ;;
white)
tput setab 7 ;;
esac
}
# Echo correctly indented additional line to info
readonly _sqr_indentHelp=' : '
readonly _sqr_indentExe=' '
readonly _sqr_indentAppendHelp=' '
echoinfo() {
if [ $_sqr_contextHelp -ne 0 ] ; then
printf '%s' "$_sqr_indentAppendHelp"; echo "$@"
else
printf '%s' "$_sqr_indentExe"; echo "$@"
fi
}
# Echo info about step arguments
# Needs to be called first in _info() function
echoinfoArgs() {
echo -e "${_sqr_restorePos}$*"
if [ $_sqr_contextExe -ne 0 ]; then
printf '%s' "$_sqr_indentExe"
else
printf '%s' "$_sqr_indentHelp"
fi
}
}
## Traps
{
sqr::trap_exit () {
exists -f seq_trapExit && seq_trapExit
debug "Sequencer exit"
}
trap sqr::trap_exit EXIT
# requires `set -o errtrace`
sqr::error_report() {
local error_code=${?}
error "Error in ${sqr_file} in function ${1} on line ${2}"
exit ${error_code}
}
# Uncomment the following line for always providing an error backtrace
# trap 'sqr::error_report "${FUNCNAME:-.}" ${LINENO}' ERR
}
# check if run as root
root() {
[[ $(id -u) -eq 0 ]]
}
# check if there is another PID other than this one
running() {
pidof -o %PPID -x "${0##*/}">>/dev/null
}
# exists [-f] [--] [ELEMENT]
# [ELEMENT]
# : either a variable name or
# -f : a function
exists() {
local func=
for _ in "$@" ; do
case "${1:-}" in
--)
shift && break ;;
-f)
func="${2:-}"
esac
done
if [[ -n "${func}" ]] ; then
declare -F "${func}" &>>/dev/null
else
[[ "${1:-}" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] && [[ -n "${!1:-}" ]]
fi
}
# interactive
# Started without -q to have user interactions
interactive() {
(( _sqr_interactive ))
}
# quiet
# Started with -q to use defaults for confirmations
quiet() {
(( ! _sqr_interactive ))
}
# silent
# Log level equals 0 (fatal)
silent() {
[[ $LOG_LEVEL -eq 0 ]]
}
# dry-run
# Started with --dry-run
dry() {
(( _sqr_dry ))
}
# verbose
# Started with --verbose
verbose() {
(( _sqr_verbose ))
}
contextHelp() {
(( _sqr_contextHelp ))
}
contextExe() {
(( _sqr_contextExe ))
}
# editor [FILE(s)..]
# Starts the detected system text editor
editor() {
exe "${_sqr_editor}" "$@"
}
### interactive
# confirm [OPTIONS] [--] [QUESTION]
# Default (empty character) = no
# Invalid character trigger the default
#
# [OPTIONS]
# -f : interactive even if quiet
# -n : no input help
# -y : default = yes
# -- : end of options
confirm() {
sqr::debugPause
local rexReply='^[Yy]+$' # default no
local inputHelp='[y/N] ' # default no
local noHelp=0
local force=0
for _ in "${@}" ; do
case "${1:-}" in
--)
shift && break ;;
-f)
force=1
shift ;;
-n)
noHelp=1
shift ;;
-y)
rexReply='^[^Nn]*$' # default yes
inputHelp='[Y/n] ' # default yes
shift ;;
esac
done
(( noHelp )) && inputHelp=
if interactive || (( force )) ; then
read -r -p "${1:-} ${inputHelp}" -n 1
# Needed when read stops after one character (-n 1)
# Add a newline only if input is not empty (just enter is pressed)
[[ -z "$REPLY" ]] || printf '\n'
else
REPLY=''
fi
sqr::debugContinue
[[ $REPLY =~ ${rexReply} ]]
}
# ask [OPTION] [QUESTION] [DEFAULT]
# Will ask for input even if quiet, when [DEFAULT] is empty
#
# [OPTION]
# -e : allow empty input
# Ignored if [DEFAULT] is available
# -s : ask for secret (don't print input)
# Does not add a newline. Usage:
# pass=$(ask -s "Password"); echo
# -- : End of options
ask() {
sqr::debugPause
local hidden=
local empty=0
for _ in "$@" ; do
case "${1:-}" in
--)
shift && break ;;
-e)
empty=1
shift ;;
-s)
hidden="-s"
shift ;;
esac
done
local answer=
if [[ -n "${2:-}" ]] ; then
! interactive && printf '%s\n' "${2}" && sqr::debugContinue && return 0
read ${hidden?} -r -p "${1:-"User input"} ($2) " answer
else
read ${hidden?} -r -p "${1:-"User input"} " answer
fi
if [[ -z "$answer" ]] ; then
answer="${2:-}"
fi
printf '%s\n' "${answer}"
# return if answer is empty
sqr::debugContinue
if (( ! empty )) ; then
[[ -n "${answer}" ]]
fi
}
# Escaping non-printable characters with the proposed POSIX $'' syntax
escpath() {
printf "%q" "$*"
}
# saveReturn <ERRNO>
# Function returns with <ERRNO> in case step wants additional evaluation
saveReturn() {
if [[ "${1:-"0"}" -ne 0 ]] ; then
_sqr_errno=${1}
fi
return "${_sqr_errno}"
}
# getReturn
# Returns latest saved $_sqr_errno
getReturn() {
return "${_sqr_errno}"
}
# endReturn [-f] [-o ERRORCODE] [MESSAGE]
# -f : force exit with $_sqr_errno without user input
# -o : override and check given [ERRORCODE]
# [MESSAGE] : Custom error message
#
endReturn() {
local forceExit=0
local errorCode=${_sqr_errno}
local endMessage=""
for _ in "$@" ; do
case "${1:-}" in
-f)
forceExit=1
shift
;;
-o)
shift
local rex='^[-]*[0-9]+$'
# Check if string is a number or alias
if [[ "${1:?}" =~ ${rex} ]] ; then
errorCode="${1:?}"
else
warning "Ignoring invalid error code: $1"
fi
shift
;;
"")
break
;;
*)
endMessage="$*"
break
;;
esac
done
if ( [[ ${errorCode} -ne 0 ]] && ! interactive ) \
|| [[ ${errorCode} -ne 0 && $forceExit -ne 0 ]] ; then
sqr::echo
if [[ -n "${endMessage}" ]] ; then
error -e "${endMessage}"
error -e -a "Sequence stopped"
else
error -e "Return value ${errorCode} detected."
error -e -a "Sequence stopped"
fi
exit "${errorCode}"
fi
if [[ "${errorCode}" -ne 0 ]] ; then
sqr::echo
if [ "${endMessage}" != "" ]; then
error -e "${endMessage}"
else
error -e "Return value ${errorCode} detected."
fi
if confirm -y "End sequence" ; then
error -e "Sequence stopped"
exit "${errorCode}"
else
# reset saved error code if user chooses to continue
_sqr_errno=0
sqr::echo
warning "Continuing sequence..."
fi
fi
}
# endIfEmpty <VariableName> [DESCRIPTION]
# DESCRIPTION : Optional text for error
endIfEmpty() {
local ref=
exists "${1:-}" && ref="${!1:-}"
if [ -z "${ref}" ] ; then
if [ -n "${2:-}" ] ; then
error -- "$2"
error -a "Sequence stopped."
else
error -- "\${${1:-"-"}} must not be empty."
error -a "Sequence stopped."
fi
exit 6
fi
}
# addConf <CONF_MODE> [FILE_MODE] <SOURCE> <DESTINATION_FILE>
# trying to write a file
# if exists, one attempt is made to create bck file of it
# if all fails, a log file is created with the conflicts to be resolved by the user
addConf() {
local addConfBackup=
local confMode=""
local transferCmd="echo"
for _ in "$@" ; do
case "${1:-}" in
-c) # create a new file
confMode="-c"
shift ;;
-a) # append to existing file
confMode="-a"
shift ;;
-s) # skip if CONFIGFILE exists
confMode="-s"
shift ;;
-m) # only add content to missing conf and warn user
confMode="-m"
shift ;;
-f) # choose if source is a file or text
transferCmd="cat"
shift ;;
*) # default
if [ "$confMode" == "" ] ; then
error "Parameter 1 (-a|-c|-m|-s) missing for addConf()"
exit 0
fi ;;
esac
done
local source="${1:-}"
local dest="${2:?}"
if [ "${transferCmd}" == "cat" ] && [ ! -f "${source}" ] ; then
error "Source: \"${source}\" does not exist"
return 1
fi
if dry ; then
info "Writing ${dest} ...dry-run"
return 0
fi
if [ -z "${dest}" ] ; then
error "Destination empty"
return 1
fi
sqr::echo -n " [i] Writing ${dest} ..."
if [[ ${confMode} != "-m" ]] ; then
# try writing config directly if it doesn't exist
if [ ! -f "$dest" ] ; then
"${transferCmd}" "${source}" > "${dest}"
sqr::echo "ok"
return 0
fi
if [[ ${confMode} == "-s" ]] ; then
# if skip is selected, don't try to backup but add confilict entry
sqr::echo "skipping (exists)"
else
# try backup existing config
addConfBackup="${dest}_$(date +%Y%m%d-%H%M%S).bck"
if [ ! -f "${addConfBackup}" ] ; then
cp -ar "${dest}" "${addConfBackup}"
if [[ ${confMode} == "-c" ]] ; then
"${transferCmd}" "${source}" > "${dest}"
else
"${transferCmd}" "${source}" >> "${dest}"
fi
sqr::printf 'ok\n [i] %s\n' "Existing config saved to ${addConfBackup}"
return 0
else
sqr::echo "nok"
warning -e "backup exists"
fi
fi
else
sqr::printf 'ok\n [i] %s' "no change requested"
fi
# add configuration to missingConf file
if [[ "${missingDate:-}" = "" ]] ; then
missingDate="set"
echo -n "### " >> "${sqr_missingConf}"
date >> "${sqr_missingConf}"
fi
local helpText="needs to be added manually"
if [[ "$confMode" == "-s" ]] ; then
helpText="not overwritten"
fi
{
printf '#--- "%s" %s (Option: %s) ---' "${dest}" "${helpText}" "${confMode}"
"${transferCmd}" "${source}"
echo
} >> "${sqr_missingConf}"
warning -e "Check $(realpath "${sqr_missingConf}") for configuration conflicts (${dest})"
return 1
}
# checkStep <Step Number or Alias>
# return 0 - for invalid step
# return Step Number
# Check sanitiy of step number or
# Check if alias exists
checkStep() {
local checkStep_rex='^[0-9]+$'
local checkStep_ref=""
local testRef=
# Check if string is a number or alias
if ! [[ "${1:-}" =~ ${checkStep_rex} ]] ; then
if exists "_sqr_alias_${1:-}" ; then
testRef="_sqr_alias_${1:-}"
checkStep_ref="${!testRef}"
else
checkStep_ref=0
fi
# Catch special character after evaluation
if ! [[ "${checkStep_ref}" =~ ${checkStep_rex} ]] ; then
checkStep_ref=0
fi
else
checkStep_ref="${1:-}"
fi
if (( checkStep_ref < 1 || checkStep_ref > _sqr_stepMax )) ; then
error -e "Invalid step: ${1:-}"
printf '0'
return 1
else
if exists -f "step_$checkStep_ref" ; then
printf '%s' "${checkStep_ref}"
return 0
else
# step doesn't exist
error -e "Invalid step: ${1:-}"
printf '0'
return 1
fi
fi
}
# step <Step Number of Alias>
# execute given step
step() {
local stepNo=0
local stepArgs=("$@")
if ! stepNo="$(checkStep "${1:-}")" ; then
return 1
else
"step_$stepNo" "${stepArgs[@]}"
fi
color
}
# Parse alias functions "step_[STEP NUBER]_alias" to create
# back reference variable of schema:
# alias_[ALIAS]=[STEP NUMBER]
parseAlias() {
sqr::debugPause
local i
for ((i=1; i<=_sqr_stepMax; i++)); do
# Check for alias definition
exists -f "step_${i}_alias" || continue
# Function returns step alias
declare -g "_sqr_alias_$("step_${i}_alias")=$i"
done
sqr::debugContinue
}
# Creating a minimal seq (step definition) template
createTemplate() {
[ -f "${seq_template}" ] && return 1
exe cp "${sqr_origin}/${seq_template}" "${seq_template}"
}
# displayHelp [OPTIONS] [STEP NUMBER OR ALIAS]
# [OPTIONS]
# -t : Ask for template creation
# -s : Display only step help
# [STEP NUMBER OR ALIAS]
# [NO TEMPLATE] must be set
# Display step info function only for given step
#
# - Display sequencer help and, if available, sequence help
# - Cluster continous (more than 1) steps visually together
displayHelp() {
sqr::debugPause
local i
local answer
local arg
local fullHelp=1
local clusterSize=0
local lastClusterSize=0
local createTemplate=0
local stepFound=0
local loopStart=0
local loopEnd="${_sqr_stepMax}"
_sqr_contextHelp=1
while getopts "st" arg; do
case "${arg}" in
s) fullHelp=0 ;;
t) createTemplate=1 ;;
*) ;;
esac
done
shift "$((OPTIND-1))"; OPTIND=1
# check if step definition exists by looking for a step_*() function
for ((i=1; i<=_sqr_stepMax; i++)); do
if ! exists -f "step_${i}" ; then
continue
fi
stepFound=${i}
break
done
# check if help is requested for a single step
if [ -n "${1:-}" ]; then
parseAlias
loopStart="$(checkStep "${1}")"
fi
if (( ! loopStart )) ; then
(( fullHelp )) || (( ! stepFound )) && helpSequencer
loopStart=1
else
# Output loop only for one step
loopEnd=${loopStart}
fi
if (( ! stepFound )) ; then
printf '\n %s\n' "It seems ${0##*/} was called directly."
printf ' %s\n\n' "Please create a sequence script first."
if (( createTemplate )) ; then
if confirm " Create a template now?" ; then
if createTemplate ; then
printf '\n %s\n' "${seq_template} created."
else
printf '\n %s\n' "${seq_template} exists...Nothing to do!"
fi
else
printf '\n Nothing to do!\n'
fi
fi
exit 1
else
printf '\n %s\n' "Step (= alias) [STEP ARGS] : documentation"
for ((i=loopStart; i<=loopEnd; i++)); do
# Display step reference in help if step function exists
if ! exists -f "step_${i}" ; then
if (( clusterSize )) ; then
# Mark the end of a cluster
lastClusterSize=$clusterSize
clusterSize=0
fi
continue
fi
(( clusterSize+=1 ))
if [[ $lastClusterSize -gt 1 ]] ; then
# Add separation at the end of a cluster
lastClusterSize=0
echo
elif [[ $clusterSize -eq 1 ]]; then
# Add separation before the start of a cluster if it is not the first
exists -f "step_$((i+1))" && [[ $i -ne $stepFound ]] && echo
fi
printf ' %3s ' "$i"
# Display alias if exists
if exists -f "step_${i}_alias" ; then
echo -en " = $(col orange)$("step_${i}_alias")$(col off)${_sqr_savePosAlias}"
# Newline only if step info() exists
exists -f "step_${i}_info" && printf '\n%s' "$_sqr_indentHelp"
else
echo -en " : ${_sqr_savePos}"
fi
# Display step help only if info function exists
if exists -f "step_${i}_info" ; then
"step_${i}_info" "$i" || true
else
echo
fi
color ""
done
echo
fi
_sqr_contextHelp=0
sqr::debugContinue
}
# listSteps [FILTER STRING]
# [FILTER STRING]
# show only steps and aliases starting with [FILTER STRING]
listSteps() {
local locAlias=
for ((i=1; i<=_sqr_stepMax; i++)); do
# Display step reference in help if step function exists
exists -f "step_${i}" || continue
# Display alias if exists
if exists -f "step_${i}_alias" ; then
locAlias="$("step_${i}_alias")"
else
locAlias="$i"
fi
# $1 = filter regex
if [[ "$locAlias" =~ ^${1:-.*} ]]; then
printf '%s\n' "${locAlias}"
fi
done
}
# listProfiles [OPTION] [SEARCH]
# List all available profiles for current user
# [OPTION]
# -q : only check for profile support
listProfiles() {
local file=
if [[ ${_seq_configDirName} == $(basename "${seq_configRoot}") ]] \
|| [[ ! -e ${seq_configRoot} ]] ; then
error -e "${seq_name} does not have configuration profiles"
return 1
fi
[[ "${1:-}" == "-q" ]] && return 0
for file in "${seq_configRoot}"/* ; do
file="$(basename -- "${file}")"
[[ ${file%.*} =~ ^${1:-.*} ]] && printf '%s\n' "${file%.*}"
done
}
# showVersion
showVersion() {
printf 'Sequencer: %s\n' "${sqr_versionString}"
printf 'Seq needs: %s\n' "${sqr_minVersion:-"-"}"
}
# initSeqConfig [OPTION] <NAME> [TEMPLATE]
# Create a configuration file in the users' home.
# Source it if already existent
# [OPTION]
# -p : <NAME> is subfolder used for profiles
# -t : Source config also if created from template
# -e : Create empty configuration if no template is found
# Return
# 0 : Sourced configuration or
# (-t) : created and sourced configuration from template
# 1 : Created configuration from template but not sourced
# 2 : Created empty configuration
# 3 : No configuration created
initSeqConfig() {
local answer=n
local retVal=255
local sourceAlways=0
local createEmpty=0
local seqProfiles=0
local configExists=
local configDir=
for _ in "$@" ; do
case "${1:-}" in
-e)
createEmpty=1
shift ;;
-p)
seqProfiles=1
shift ;;
-t)
sourceAlways=1
shift ;;
esac
done
local configLoc="$seq_configRoot/$1"
if [[ $seqProfiles -ne 0 ]] ; then
[ -z "$seq_profileName" ] && seq_profileName=default
configLoc="$seq_configRoot/$1/${seq_profileName}.cfg"
fi
configDir="$(dirname -- "$configLoc")"
local configTemplate="$2"
# Don't create anything if only profiles should be listed
if [ -n "${_seq_profileList}" ] ; then
seq_configRoot="$configDir"
return 0
fi
seq_configRoot="$configDir"
if [ -s "$configLoc" ] ; then
info "Using configuration file: $configLoc"
seq_configFile="$configLoc"
. "$configLoc"
return 0
fi
# Ask for config creation if not existent
if ! quiet && ! dry ; then
sqr::echo " [i] Configuration $configLoc missing"
confirm "Create it now?" || return 3
fi
# Create config subdir in users home
if [ ! -e "$configDir/" ] ; then
sqr::echo -n "Creating $(realpath -- "$configDir")..."
# shellcheck disable=SC2015 # && || is not if else
exe install -m 700 -d "$configDir" && sqr::echo "Ok" || sqr::echo "Nok"
fi
# Config does not exist, check for template
if [ -s "$configTemplate" ] ; then
# Check first if there is an existing configuration at the templates position
configExists="$(dirname -- "$configTemplate")/$1"
if [ -s "$configExists" ] ; then
exe mv "$configExists" "$configLoc"
endReturn -o $? "Unable to use existing configuration: $configExists"
sqr::echo " [i] Using existing configuration: $configExists"
sqr::echo " (Moved to $configDir)"
. "$configLoc"
retVal=0
else
# Install new template to the final location
exe install -m 600 -T "$configTemplate" "$configLoc"
endReturn -o $? "Failed to create configuration"
if [ $sourceAlways -eq 0 ] ; then
if [ $_seq_configEdit -eq 0 ] ; then
warning "Seq configuration created from template but not used"
warning -a "Please modify \"$configLoc\" first"
fi
retVal=1
else
warning "Using seq configuration from template $configTemplate"
warning -a " (Copied to $configDir)"
. "$configTemplate"
retVal=0
fi
fi
seq_configFile="$configLoc"
else
warning "Seq configuration template not found"
fi
if [ $createEmpty -ne 0 ] ; then
# Create empty config file
warning "Created empty configuration file $configLoc"
exe touch "$configLoc"
exe chmod 600 "$configLoc"
seq_configFile="$configLoc"
retVal=2
fi
# Give the user a chance to edit the create config file
if [ $retVal -eq 255 ]; then
error "No seq configuration created"
retVal=3
elif interactive && ! dry && (( ! _seq_configEdit )) ; then
# ! _seq_configEdit : Edit operation will be performed in main
if confirm "Edit configuration file now?" ; then
exe editor "$configLoc"
. "$configLoc"
retVal=0
fi
fi
return $retVal
}
# execute [-q] <Step Number>
# -q: don't stop and don't report step functions which cannot be found
# execute given step_<Step Number> function
execute() {
local answer=
local notFound=0
local noReport=0
local stepAlias=
if [[ "${1:-}" == "-q" ]] ; then
noReport=1
shift
fi
# check if step function exists
#declare -F "step_${1:?}" &>>/dev/null || notFound=1
exists -f "step_${1:?}" || notFound=1
if [ ${notFound} -eq 1 ] && [ ${noReport} -ne 1 ] ; then
error "Step ${1:-"-"} not found"
exit 1
fi
# don't execute step functions which are not available
if (( notFound )) ; then
return 1
fi
if ! quiet ; then
exists -f "step_${1}_alias" && stepAlias=$("step_${1}_alias")
printf '\n [%3d] ' "${1}"
if [ -n "${stepAlias}" ]; then
echo -en "$(col orange)${stepAlias}$(col off)${_sqr_savePosAlias}"
# Only add newline if step info() available
exists -f "step_${1}_info" && printf '\n%s' "${_sqr_indentExe}"
else
echo -en "${_sqr_savePosExe}"
fi
if exists -f "step_${1}_info" ; then
_sqr_contextExe=1
"step_${1}_info" "$1" "${seq_args[@]}"
_sqr_contextExe=0
color none
else
# Add newline if no info is given
echo
fi
fi
if interactive ; then
answer="$(ask "Start: (y)es/[n]o/(s)kip?" "n")"
case "${answer}" in
[yY])
"step_$1" "$1" "${seq_args[@]}"
_seq_stepReturn="$?" ;;
[sS]) # skip step
_seq_stepReturn=0
return 0 ;;
*)
stepAlias="${1}"
# Display alias if exists
exists -f "step_${1}_alias" && stepAlias="$("step_${1}_alias")"
warning "Stopping sequence at step: $stepAlias"
exit 1 ;;
esac
else
"step_$1" "$1" "${seq_args[@]}"
_seq_stepReturn="$?"
fi
color none
}
# continous <Starting Step Number>
# (max $_sqr_stepMax)
# execute sequence continously from given starting step
continous() {
local continous_all=0
local continous_i
local continous_step=0
if [ "${1:-}" == "-a" ]; then
continous_all=1
shift
fi
continous_step="$(checkStep "$1")" || return 1
info "Starting sequence ${seq_name} ..."
for ((continous_i=continous_step; continous_i<=_sqr_stepMax; continous_i++)); do
if ! execute -q "${continous_i}" ; then
[ ${continous_all} -eq 0 ] && break
fi
[ ${_seq_stepReturn} -ne 0 ] && break
done
return ${_seq_stepReturn}
}
# selection <STEP ARRAY>
# execute given step list
# e.g.: selection -q (1, 4, 12)
selection() {
local selection_i
local selection_step=0
local selection_array=("$@")
[ ${#selection_array[@]} -eq 0 ] && return 1
info "Starting sequence ${seq_name} ..."
for selection_i in "${selection_array[@]}" ; do
selection_step="$(checkStep "${selection_i}")" || return 1
execute "${selection_step}"
done
return ${_seq_stepReturn}
}
# exe <COMMAND>
# Handle dry run and verbose output for commands without pipe and/or redirects
exe() {
dry && printf -- '--'
if dry || verbose ; then
(set -x; : "$@")
fi
if ! dry ; then
"$@"
fi
}
# exep <COMMAND AS STRING(S)>
# Handle dry run and verbose output for commands containing pipe and/or redirects
exep() {
if dry ; then
printf -- '--++ : %s\n' "$*"
elif verbose ; then
printf '++ : %s\n' "$*"
fi
if ! dry ; then
bash -c "$*"
fi
}
# Check if sequence is compatible
sqr::compatible() {
if ! exists "sqr_minVersion"; then
sqr_minVersion="${VERSION_SEQREV:-0}"
fi
if [ -z "${sqr_minVersion:-}" ] ; then
warning "No sequence revision found. Trying anyway..."
else
if [ -n "${sqr_minVersion}" ] && [[ ${sqr_minVersion} -gt ${sqr_version} ]] ; then
error "Unsupported sequence revision"
showVersion
return 1
fi
# exclude older versions if needed
if [ -n "${sqr_minVersion}" ] && [[ ${sqr_minVersion} -le 15 ]] ; then
error "Unsupported sequence revision (major changes in version 16)"
error -a "(Use 'sequpgrade.sh $(readlink -f -- "$0")' for a basic upgrade)"
showVersion
return 1
fi
fi
return 0
}
# Used as editor if no system editor could be found
sqr::noEditor() {
error "No editor found (\$EDITOR,\"/etc/alternatives\",nano,vi)"
error -a "Cannot open: $*"
}
sqr::main() {
local emptyCall=1
local quickStartOne=0
local toStart=( "0" )
local startAll=
# options check
for _ in "$@" ; do
case "${1:-}" in
++) # end parameter and quickstart step 1 with -qq
quickStartOne=1
_sqr_interactive=0
LOG_LEVEL=0
shift && break ;;
--) # end parameter
shift && break ;;
--all|-a) # execute all steps; regardless continouity
startAll="-a"
shift ;;
--config|-c) # open sequence configuration file
_seq_configEdit=1
shift ;;
--color) # Output color codes within pipe
_sqr_colorAlways=1
shift ;;
--debug)
sqr_args+=" $1"
_sqr_debug="1"
shift ;;
--dry-run|-d)
sqr_args+=" $1"
_sqr_dry=1
shift ;;
--help) # show only full help
local _sqr_optHelp=2
shift ;;
-h) # show only step help
local _sqr_optHelp=1
shift ;;
--helpapi|-ha) #show build-in functions
local _sqr_optHelpapi=1
shift ;;
--liststeps|-ls)
listSteps "${2:-}"
exit 0 ;;
--profile|-p) # seq profile name
sqr_args+=" $1 ${2:-}"
shift
# Cover the case when only -p is given without profile name
if [ -z "${1:-}" ] ; then
seq_profileName=default
else
seq_profileName="$1" && shift
fi ;;
-pl) # List available profiles with search
shift
[[ ${LOG_LEVEL} -gt 1 ]] && LOG_LEVEL=1 # "only show errors"
_seq_profileList="${1:-".*"}"
[ -n "${1:-}" ] && shift
;;
--quiet|-q)
sqr_args+=" $1"
_sqr_interactive=0
shift ;;
--silent|-qq)
sqr_args+=" $1"
_sqr_interactive=0
LOG_LEVEL=0
shift ;;
--single|-s) # execute only one step and stop
sqr_args+=" $1"
_sqr_single=1
shift ;;
--verbose|-v)
sqr_args+=" $1"
_sqr_verbose=1
shift ;;
--version) # version request
showVersion
exit 0 ;;
esac
done
if (( ${_sqr_optHelp:-} )) ; then
if [[ ${_sqr_optHelp} == 2 ]] ; then
_sqr_optHelp=""
else
_sqr_optHelp="-s"
fi
displayHelp ${_sqr_optHelp:-} "${1:-}" | less -FREX
exit 0
fi
(( ${_sqr_optHelpapi:-} )) && helpApi | less -R && exit 0
# debug mode
if [[ "${_sqr_debug:-}" = "1" ]]; then
set -o xtrace
PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
# Enable error backtracing
trap 'sqr::error_report "${FUNCNAME:-.}" ${LINENO}' ERR
fi
# Don't show help if only configuration should be edited
[ ${_seq_configEdit} -ne 0 ] && [ -z "${1:-}" ] && LOG_LEVEL=1
if [ -z "${1:-}" ] && (( ! quickStartOne )) ; then
if ! quiet && [[ ${LOG_LEVEL} -ge $log_info ]] ; then
# Empty -> show help
displayHelp -st
fi
# Assume starting at one for interactive mode
toStart=( "1" )
elif (( quickStartOne )) ; then
emptyCall=0
toStart=( "1" )
seq_args=( "$@" )
else
emptyCall=0
read -r -a toStart <<< "$1"
shift
seq_args=( "$@" )
fi
# compatibility check of sequence
sqr::compatible || exit 1
# dry run warning
if dry && ! quiet ; then
color yellow
echo
echo " [w] Dry run active."
echo " - Printed commands may not be accurate (e.g. quotation incorrect)"
echo " - Sequence may ignore dry run"
color none
confirm -f -n -y "Press enter to continue or Ctrl + C to abort"
fi
# Determine system default editor
# Change with update-alternatives --config editor
_sqr_editor="$(realpath -eq "/etc/alternatives/editor")"
## Various fallbacks
[ -z "${_sqr_editor}" ] && _sqr_editor="$EDITOR"
[ -z "${_sqr_editor}" ] && _sqr_editor="$(command -v nano)"
[ -z "${_sqr_editor}" ] && _sqr_editor="$(command -v vi)"
## Fall back to error message
[ -z "${_sqr_editor}" ] && _sqr_editor="sqr::noEditor"
parseAlias
# run configuration for sequence only if available and if first step is valid
if exists -f seq_config ; then
# Create/edit configuration file
if (( _seq_configEdit )) ; then
# Suppress seq_config output for editing and allow it to fail
quietSave=${LOG_LEVEL}
LOG_LEVEL=0
seq_config "${seq_args[@]}" || true
LOG_LEVEL=${quietSave}
if [ -w "$seq_configFile" ]; then
exe editor "$seq_configFile"
else
error "No configuration file available"
fi
(( emptyCall )) && exit 0
fi
if checkStep "${toStart[0]}" >/dev/null 2>&1 ; then
if ! seq_config "${seq_args[@]}" ; then
error "Configuring sequence failed"
exit 1
fi
fi
elif (( _seq_configEdit )) ; then
error "Sequence does not have a configuration file"
return 1
fi
# Check for profile support
if [ -n "${_seq_profileList}" ]; then
listProfiles "${_seq_profileList}"
exit $?
elif [ -n "${seq_profileName}" ]; then
listProfiles -q || exit 1
fi
# check if more than one step is given and select execution mode
if (( _sqr_single )) ; then
selection "${toStart[0]}"
elif [ "${#toStart[@]}" -gt "1" ]; then
selection "${toStart[@]}"
else
continous ${startAll} "${toStart[0]}"
fi
}
sqr::main "$@"
_sqr_mainReturn="$?"
silent || printf '\n%s\n' "$seq_name finished"
exit "${_sqr_mainReturn}"