Files
shell_sequencer/sequencer/sequencer.sh
Martin Winkler c7de26455b new function getReturn which return latest value previously saved by saveReturn
function saveReturn returns with the same errno it saved

refactor documentation replace installation with sequence and aborted with stopped

New starting output with absolute path to seq
2019-11-21 23:30:08 +01:00

575 lines
13 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=6
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 " --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"
}
# 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
echo -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
echo -e " [E] $endMessage\n Sequence stopped"
else
echo -e " [E] Return value $ERNO detected.\n Sequence stopped"
fi
exit $ERNO
fi
if [ $ERNO -ne 0 ] ; then
echo
if [ "$endMessage" != "" ]; then
echo -e " [W] $endMessage"
else
echo " [W] Return value $ERNO detected."
fi
read -p "End sequence: [y]/n? " answer
case $answer in
[nN])
echo
echo " [I] Continuing sequence..."
;;
*)
echo
echo " [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"
echo " [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/[n]? " answer
case $answer in
[yY])
step_$1 $1 $STEP_ARGS
;;
*)
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
echo " [E] Invalid step: $1"
ref=0
fi
else
ref=$1
fi
if (( $ref < 1 || $ref > $MAX_STEP )) ; then
return 0
else
return $ref
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;
;;
--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
echo " [E] Unsupported sequence revision"
showVersion
exit 1
fi
# exclude older versions if needed
if [ ! -z $VERSION_SEQREV ] && [ $VERSION_SEQREV -lt 3 ] ; then
echo " [E] Unsupported sequence revision (addConf)"
showVersion
exit 1
fi
if [ -z $VERSION_SEQREV ] ; then
echo -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;