Files
shell_sequencer/sequencer/sequencer.sh
Martin Winkler 3d1097bb5d New function step to execute other steps also by alias
Possibilty to skip a step, if run without quiet option

WIP build-in function documentation
2019-12-08 21:38:42 +00:00

633 lines
15 KiB
Bash
Executable File

#!/bin/bash
## Sequencer script is doing nothing on its own. It is included by a squence script
## which uses the sequencer.sh to provide sequencial operations with or without
## user interaction (see seqTemplate.sh)
## Version information
VERSION_REV=7
VERSION_MAJOR=0
VERSION_MINOR=0
## Start of generic script part
QUIET=0
DRY=0
VERBOSE=0
ERNO=0
STEP_ARGS=
MAX_STEP=255
ALIAS=
TEMPLATE_NAME=seqTemplateExample.sh
MISSING_CONF=missingConf.log
VERSION_STRING="${VERSION_REV}.${VERSION_MAJOR}.${VERSION_MINOR}"
helpSequencer() {
echo "Usage: ${0##*/} [OPTIONS] [STEP NUMBER(s) or ALIAS] [STEP ARGUMENTS]"
echo
echo " [OPTIONS]"
echo " --dry-run, -d : Only print to console what would be done"
echo " ! Attention - Sequence must support this"
echo " --help, -h : Display help"
echo " --helpapi, -ha : Display help about build-in supporting functions"
echo " (e.g. exe,addconf,echerr,...)"
echo " --quiet, -q : Don't ask for permission to execute steps"
echo " If called without starting step number, only this help is shown"
echo " --verbose, -v : Verbose output (use exe() function to call shell commands in seqs)"
echo " ( e.g.: exe apt update )"
echo " --version : Display version of sequencer and revision of sequence"
echo
echo " [STEP NUMBER\"(s)\" 1-${MAX_STEP} or ALIAS]"
echo " No STEP or ALIAS : assume 1 as starting point"
echo " Single STEP or ALIAS : starting point of sequential process"
echo " Multiple STEPS or ALIAS : execute only given steps"
echo " execute only one step with using special step 0"
echo " ( e.g. only execute step 4: $0 \"4 0\" )"
echo " multiple steps need to be given as string"
echo " [STEP ARGUMENTS]"
echo " * : Arguments will be passed to selected steps as:"
echo " \$2 ..."
echo " \$1 is always the step number"
}
helpApi() {
echo "sequencer.sh build-in functions"
echo
echo " echoerr <...>"
echo " echo output to stderr"
echo " <...> - all parameter are forwarded to echo"
echo
echo " endCheckEmpty <Variable Name> [Description]"
echo " exit 666 if variable is empty"
echo " <Variable Name> - Name used within eval"
echo " [Description] - Additional text for error output"
echo
echo " saveReturn [Error Code]"
echo " Save error code if it is != 0 for later use with endReturn"
echo
echo " endReturn [-f] [Message]"
echo " Notifys user that there was an error (previously saved by saveReturn)"
echo " and asks to continue or end sequence. Always exits with saved error code."
echo " -f : force exit with the last saved error code without user input"
echo
echo " step <Step number or alias>"
echo " Executes a single step also by alias. Useful if step numbers get reorganized."
echo " dry-run is not applied in this function! The executed step is responsible."
echo
}
# Echo to stderr
echoerr() { >&2 echo "$@"; }
# endCheckEmpty <VariableName> [DESCRIPTION]
# DESCRIPTION : Optional text for error
endCheckEmpty() {
local errorText=$1
eval 'local ref=$'$1
if [ ! -z "$2" ] ; then
errorText=$2
fi
if [ -z $ref ] ; then
echoerr -e " [E] $errorText must not be empty.\n Sequence stopped."
exit 666
fi
}
existsFunction() {
local NOTFOUND=0
declare -F $1 &>>/dev/null || NOTFOUND=1
return $NOTFOUND
}
# saveReturn <ERRNO>
# Function returns with <ERRNO> in case step wants additional evaluation
saveReturn() {
if [ $1 -ne 0 ] ; then
ERNO=$1
fi
return $ERNO
}
# getReturn
# Returns latest saved $ERNO
getReturn() {
return $ERNO
}
# endReturn [-f] [MESSAGE]
# -f : force exit with $ERNO without user input
# MESSAGE : Custom error message
#
endReturn() {
local forceExit=0
local endMessage=""
for arg in "$@" ; do
case "$1" in
-f)
forceExit=1
skipStep=0
shift
;;
"")
break
;;
*)
endMessage="$@"
break
;;
esac
done
if [[ ( $ERNO -ne 0 && $QUIET -ne 0 ) || ( $ERNO -ne 0 && $forceExit -ne 0 ) ]] ; then
echo
if [ "$endMessage" != "" ]; then
echoerr -e " [E] $endMessage\n Sequence stopped"
else
echoerr -e " [E] Return value $ERNO detected.\n Sequence stopped"
fi
exit $ERNO
fi
if [ $ERNO -ne 0 ] ; then
echo
if [ "$endMessage" != "" ]; then
echoerr -e " [W] $endMessage"
else
echoerr " [W] Return value $ERNO detected."
fi
read -p "End sequence: [y]/n? " answer
case $answer in
[nN])
echo
echo " [I] Continuing sequence..."
;;
*)
echo
echoerr " [E] Sequence stopped"
exit $ERNO;
;;
esac
fi
}
# addConf <CONF_MODE> <CONFIGTEXT> <CONFIGFILE>
# 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 confMode=""
case "$1" in
-c) # create a new file
confMode="-c"
;;
-a) # append to existing file
confMode="-a"
;;
-s) # skip if CONFIGFILE exists
confMode="-s"
;;
-m) # only add content to missing conf and warn user
confMode="-m"
;;
*) # default
echo "Parameter 1 (-a|-c|-m|-s) missing for addConf()"
exit 0;
;;
esac
if [ "$DRY" -ne 0 ] ; then
echo " [I] Writing $3...dry-run"
return 0;
fi
echo -n " [I] Writing $3..."
if [ $confMode != "-m" ] ; then
# try writing config directly
if [ ! -f "$3" ] ; then
case "$confMode" in
-c|-s)
echo "$2" > "$3"
;;
-a)
echo "$2" >> "$3"
;;
esac
echo "ok"
return 0
fi
if [ $confMode == "-s" ] ; then
# if skip is selected, don't try to backup but add confilict entry
echo "skipping (exists)"
else
# try backup existing config
local addConfBackup="${3}_`date +%Y%m%d-%H%M%S`.bck"
if [ ! -f "$addConfBackup" ] ; then
cp -ar "$3" "$addConfBackup"
if [ $confMode == "-c" ] ; then
echo "$2" > "$3"
else
echo "$2" >> "$3"
fi
echo -e "ok \n [W] Existing config saved to ${addConfBackup}"
return 0
else
echo "nok (backup exists)"
fi
fi
else
echo "nok (no change requested)"
fi
# add configuration to missingConf file
if [ "$missingDate" = "" ] ; then
missingDate=set
echo -n "### " >> "$MISSING_CONF"
date >> "$MISSING_CONF"
fi
echo "#--- $3 ---" >> "$MISSING_CONF"
echo "$2" >> "$MISSING_CONF"
echo >> "$MISSING_CONF"
echoerr " [W] Check $(realpath "$MISSING_CONF") for configuration conflicts ($3)"
return 1
}
# 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 NOTFOUND=0
local NOREPORT=0
if [ $1 == "-q" ] ; then
NOREPORT=1
shift
fi
# check if step function exists
declare -F step_$1 &>>/dev/null || NOTFOUND=1
if [ $NOTFOUND -eq 1 ] && [ $NOREPORT -ne 1 ] ; then
echo "Step $1 not found"
exit 1;
fi
# don't execute step functions which are not available
if [ $NOTFOUND -eq 1 ] ; then
return $NOTFOUND
fi
echo -en "\n [STEP $1] "
existsFunction step_${1}_info
if [ $? -eq 0 ] ; then
step_${1}_info $1
else
# Add newline if no info is given
echo
fi
if [ $QUIET -ne 1 ] ; then
read -p "Start: (y)es/[n]o/(s)kip? " answer
case $answer in
[yY])
step_$1 $1 $STEP_ARGS
;;
[sS]) # skip step
return 0
;;
*)
echo " [I] Stopping sequence at step $1"
exit 1;
;;
esac
else
step_$1 $1 $STEP_ARGS
fi
}
# checkStep <Step Number or Alias>
# return 0 - for invalid step
# Check sanitiy of step number or
# Check if alias exists
checkStep() {
local rex='^[0-9]+$'
local ref=""
# Check if string is a number or alias
if ! [[ "$1" =~ $rex ]] ; then
eval 'ref=$alias_'"$1"
# Catch special character after eval
if ! [[ "$ref" =~ $rex ]] ; then
echoerr " [E] Invalid step: $1"
ref=0
fi
else
ref=$1
fi
if (( $ref < 1 || $ref > $MAX_STEP )) ; then
return 0
else
return $ref
fi
}
# step <Step Number of Alias>
# execute given step
step() {
local stepNo=0
checkStep "$1"
stepNo=$?
if [ "$stepNo" == "0" ] ; then
return 1
else
existsFunction step_${stepNo}
if [ $? -eq 0 ] ; then
eval 'step_'"$stepNo"
else
echoerr " [E] Invalid step: $stepNo"
return 2
fi
fi
}
# continous <Starting Step Number>
# (max $MAX_STEP)
# execute sequence continously from given starting step
continous() {
local step=0
checkStep "$1"
step=$?
if [[ $step == 0 ]] ; then
return 1
fi
for ((i=$step; i<=${MAX_STEP}; i++)); do
execute -q $i
local res=$?
if [ $res -ne 0 ] ; then
break;
fi
done
}
# selection <Step Number List (separated by space)>
# execute given step list
# e.g.: selection -q 1 4 12
selection() {
local step=0
local array=("$@")
for i in ${array[@]} ; do
checkStep "$i"
step=$?
# stop on step 0
if [ $step -eq 0 ] ; then
break
else
execute $step
fi
done
}
# Creating a minimal step definition template
createTemplate() {
if [ -f $TEMPLATE_NAME ] ; then
return 1
fi
echo "#!/bin/bash" > $TEMPLATE_NAME
echo >> $TEMPLATE_NAME
echo "step_1_info() { echo \"My custom step\"; }" >> $TEMPLATE_NAME
echo "step_1_alias() { ALIAS=\"begin\"; }" >> $TEMPLATE_NAME
echo "step_1() {" >> $TEMPLATE_NAME
echo " echo \"Doing something for step \$1 ...\"" >> $TEMPLATE_NAME
echo " echo \"Command line arguments starting with argument 2: \$@\"" >> $TEMPLATE_NAME
echo " # Use exe for regular command" >> $TEMPLATE_NAME
echo " # Use exep \"command\" for commands containing pipes or redirects" >> $TEMPLATE_NAME
echo " exe ls" >> $TEMPLATE_NAME
echo " exep \"dmesg | grep usb\"" >> $TEMPLATE_NAME
echo "}" >> $TEMPLATE_NAME
echo >> $TEMPLATE_NAME
echo "VERSION_SEQREV=${VERSION_REV}" >> $TEMPLATE_NAME
echo ". $0" >> $TEMPLATE_NAME
chmod +x $TEMPLATE_NAME
return 0
}
# Parse alias functions "step_[STEP NUBER]_alias" to create
# back reference variable of schema:
# alias_[ALIAS]=[STEP NUMBER]
parseAlias() {
for ((i=1; i<=${MAX_STEP}; i++)); do
# Check for alias definition
existsFunction step_${i}_alias
if [ $? -ne 0 ] ; then
continue
fi
# Function writes global ALIAS variable
step_${i}_alias
eval 'alias_'$ALIAS'='$i
done
}
# Always display sequencer help and, if available, sequence help
displayHelp() {
local stepsFound=0
helpSequencer
# check if step definition exists by looking for a step_*() function
for ((i=1; i<=${MAX_STEP}; i++)); do
existsFunction step_${i}
if [ $? -ne 0 ] ; then
continue
fi
stepsFound=1
done
if [ $stepsFound -eq 0 ] ; then
echo -e "\n It seems ${0##*/} was called directly."
echo -e " Please create a sequence script first.\n"
read -p " Create a template now? y/[n]? " answer
case $answer in
[yY])
createTemplate
if [ $? -eq 0 ] ; then
echo -e "\n $TEMPLATE_NAME created."
else
echo -e "\n $TEMPLATE_NAME exists...Nothing to do!"
fi
;;
*)
echo -e "\n Nothing to do!"
;;
esac
exit 1;
else
echo -e "\n Step (= alias) documentation:"
for ((i=1; i<=${MAX_STEP}; i++)); do
# Display step reference in help if step function exists
existsFunction step_${i}
if [ $? -ne 0 ] ; then
continue
fi
printf ' Step %3s ' $i
# Display alias if exists
existsFunction step_${i}_alias
if [ $? -eq 0 ] ; then
step_${i}_alias
echo " = $ALIAS"
printf ' : '
else
echo -n " : "
fi
# Display step help only if info function exists
existsFunction step_${i}_info
if [ $? -eq 0 ] ; then
step_${i}_info $i
else
echo " - step_${i}_info() missing"
fi
done
fi
}
# showVersion
showVersion() {
echo "Sequencer ${VERSION_STRING}"
echo -n "Seq Revision "
if [ ! -z "${VERSION_SEQREV}" ] ; then
echo "${VERSION_SEQREV}"
else
echo "-"
fi
}
exe() {
local arr=("$@")
if [ $DRY -ne 0 ] ; then
echo "-- ${arr[@]}"
elif [ $VERBOSE -eq 1 ] ; then
(set -x; "${arr[@]}")
else
"${arr[@]}"
fi
}
# Handle dry run and verbose output for commands containing pipe and/or redirects
# exep <COMMAND TO RUN AS STRING>
exep() {
if [ $DRY -ne 0 ] ; then
echo "-- $1"
elif [ $VERBOSE -eq 1 ] ; then
echo "++ $1"
fi
if [ $DRY -eq 0 ] ; then
bash -c "$1"
fi
}
main() {
local START=0
local EMPTYCALL=1
# options check
for arg in "$@" ; do
case "$1" in
--dry-run|-d) # shows what would be done
DRY=1
shift
;;
--help|-h) # show only help
displayHelp
exit 0;
;;
--helpapi|-ha) #show build-in functions
helpApi
exit 0;
;;
--quiet|-q) # detect if option quiet is available
QUIET=1
shift
;;
--verbose|-v) # set verbose flag
VERBOSE=1
shift
;;
--version) # version request
showVersion
exit 0;
;;
esac
done
if [ -z "$1" ] || [ "$1" == "" ] ; then
# Empty -> show help
displayHelp
# Assume starting at one for interactive mode
START=1
else
EMPTYCALL=0
read -r -a START <<< "$1"
shift
STEP_ARGS="$@"
fi
# compatibility check of sequence
if [ ! -z $VERSION_SEQREV ] && [ $VERSION_SEQREV -gt $VERSION_REV ] ; then
echoerr " [E] Unsupported sequence revision"
showVersion
exit 1
fi
# exclude older versions if needed
if [ ! -z $VERSION_SEQREV ] && [ $VERSION_SEQREV -lt 3 ] ; then
echoerr " [E] Unsupported sequence revision (addConf)"
showVersion
exit 1
fi
if [ -z $VERSION_SEQREV ] ; then
echoerr -e " [W] No sequence revision found. Trying anyway...\n";
fi
# check for starting step
if [ $EMPTYCALL -ne 0 ] ; then
# End here on quiet mode and no step was given
if [ $QUIET -eq 1 ] ; then
exit 1;
fi
fi
if [ $DRY -ne 0 ] && [ $QUIET -eq 0 ] ; then
echo
echo " [W] Dry run active."
echo " Printed commands may not be accurate (e.g. quotation incorrect)"
echo " Sequence may ignore dry run"
read -p "Press enter to continue or Ctrl + c to abort"
fi
parseAlias
echo " [I] Staring sequence $(realpath $0) ..."
# check if more than one step is given and select execution mode
if [ "${#START[@]}" -gt "1" ]; then
selection "${START[@]}"
else
continous $START
fi
echo
echo "${0##*/} finished"
}
main "$@"
exit 0;