#!/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 < 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 <,<,|). 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 < \\'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 $(col off)" cat < [TEMPLATE]$(col off)" cat < [SOURCE TYPE] $(col off)" cat <) 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. -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 : is a file Text or file (-f) to create or added to Target file to be created or modified. USAGE_API echo -e "$(col green) step $(col off)" cat < [DESCRIPTION]$(col off)" cat < : Name without \$ [DESCRIPTION] : Additional text for error output USAGE_API echo -e "$(col green) saveReturn [ERRORCODE]$(col off)" cat < 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 [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 # Function returns with 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 [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 [FILE_MODE] # 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" warnign -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 # 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 # 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 [NO TEMPLATE] [STEP NUMBER OR ALIAS] # [NO TEMPLATE] # 0 (default) : Ask for template creation # 1 : Do not ask for template creation # [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 clusterSize=0 local lastClusterSize=0 local createTemplate=1 local stepFound=0 local loopStart=0 local loopEnd="${_sqr_stepMax}" _sqr_contextHelp=1 # check if help is requested for a single step if [ -n "${2:-}" ]; then parseAlias loopStart="$(checkStep "$2")" fi if [[ "$loopStart" == "0" ]] ; then helpSequencer loopStart=1 else # Output loop only for one step loopEnd=$loopStart fi if [ -n "${1:-}" ] && [[ ${1:-} -eq 1 ]] ; then createTemplate=0 fi # 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 if [[ "$stepFound" -eq 0 ]] ; then printf '\n %s\n' "It seems ${0##*/} was called directly." printf ' %s\n\n' "Please create a sequence script first." if [ $createTemplate -ne 0 ] ; 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 -ne 0 ] ; 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] [TEMPLATE] # Create a configuration file in the users' home. # Source it if already existent # [OPTION] # -p : 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] # -q: don't stop and don't report step functions which cannot be found # execute given step_ 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 # (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 # 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 # 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 # 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|-h) # show only 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 ((${_sqr_optHelp:-})) && displayHelp 1 "${1:-}" && exit 0 ((${_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 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}"