#!/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=8 VERSION_MAJOR=0 VERSION_MINOR=0 ## Start of generic script part QUIET=0 DRY=0 VERBOSE=0 ERNO=0 STEP_ARGS= MAX_STEP=512 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 API" echo echo "The sequencer.sh build-in functions are available in all sequence functions:" echo "- step_config" echo " If optional step_config is defined in the sequence, it will be called once before any step." echo "- step_[1-${MAX_STEP}]_info" echo "- step_[1-${MAX_STEP}]_alias" echo "- step_[1-${MAX_STEP}]" echo echo "sequencer.sh build-in functions:" echo echo " exe [COMMANDLINE]" echo " Execute command line without pipes or redirects (>,<,|)." echo " Supporting: dry-run (-d): only print command without execution" echo " verbose (-v): print command before execution" echo echo " exep \"[COMMANDLINE]\"" echo " See exe, but support for pipes or redirects." echo " Important:" echo " - Shell commands cd, read, ... won't work because COMMANDLINE is started in a new shell." echo " - All apostrophes need to be esacped since the command line is given as string." echo echo " addConf " echo " Trying to write or append to a file." echo " If the CONFIGFILE exists, a backup (name_%Y%m%d-%H%M%S.bck) is saved at the same location." echo " If -s fails or -m, \"$(realpath "$MISSING_CONF")\" is created with the conflicts" echo " to be resolved by the user." echo " " echo " -c : create a new file" echo " -a : append to existing file" echo " -s : skip if CONFIGFILE exists (no backup and entry in missing conf)" echo " -m : only add content to missing conf and warn user" echo " " echo " Text to be created or added to " echo " " echo " Target file to be created or modified." echo echo " step " 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 " echoerr [...]" echo " echo to stderr" echo " [...] : all parameter are forwarded to echo" echo echo " endCheckEmpty [DESCRIPTION]" echo " exit 666 if variable is empty" echo " : Name used within eval" echo " [DESCRIPTION] : Additional text for error output" echo echo " saveReturn [ERRORCODE]" echo " Save ERRORCODE if it is != 0 for later use with endReturn" echo echo " getReturn" echo " Return last saved error code" echo echo " endReturn [OPTIONS] [MESSAGE]" echo " Notifys user that there was an error (previously saved by saveReturn," echo " or -o [ERRORCODE]) and asks to continue or end the sequence." echo " Always exits with evaluated error code." echo " [OPTIONS]" echo " -f : force exit without user input, if error code is not 0" echo " -o ERRORCODE : override stored error code and check ERRORCODE" echo " [MESSAGE]" echo " String which is displayed in the error output" echo } # Echo to stderr echoerr() { >&2 echo "$@"; } # endCheckEmpty [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 # Function returns with 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] [-o ERRORCODE] [MESSAGE] # -f : force exit with $ERNO without user input # -o : override and check given error code # MESSAGE : Custom error message # endReturn() { local forceExit=0 local errorCode=$ERNO local endMessage="" for arg 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 echoerr " [W] Ignoring invalid error code: $1" fi shift ;; "") break ;; *) endMessage="$@" break ;; esac done if [[ ( $errorCode -ne 0 && $QUIET -ne 0 ) || ( $errorCode -ne 0 && $forceExit -ne 0 ) ]] ; then echo if [ "$endMessage" != "" ]; then echoerr -e " [E] $endMessage\n Sequence stopped" else echoerr -e " [E] Return value $errorCode detected.\n Sequence stopped" fi exit $errorCode fi if [ $errorCode -ne 0 ] ; then echo if [ "$endMessage" != "" ]; then echoerr -e " [W] $endMessage" else echoerr " [W] Return value $errorCode detected." fi read -p "End sequence: [y]/n? " answer case $answer in [nN]) # reset saved error code if user chooses to continue ERNO=0 echo echo " [I] Continuing sequence..." ;; *) echo echoerr " [E] Sequence stopped" exit $errorCode; ;; esac fi } # addConf # 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 echoerr " [E] 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 [I] Existing config saved to ${addConfBackup}" return 0 else echo "nok" echoerr " [W] backup exists" fi fi else echo -e "ok \n [I] 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] # -q: don't stop and don't report step functions which cannot be found # execute given step_ 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 echoerr " [E] 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 ;; *) local stepId="$1" # Display alias if exists existsFunction step_${1}_alias if [ $? -eq 0 ] ; then step_${i}_alias stepId="$ALIAS" fi echoerr " [I] Stopping sequence at step: $stepId" exit 1; ;; esac else step_$1 $1 "${STEP_ARGS[@]}" fi } # checkStep # 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 # 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 # (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 # 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 "toolName=mytool" >> $TEMPLATE_NAME echo >> $TEMPLATE_NAME echo "# Get script working directory" >> $TEMPLATE_NAME echo "# (when called from a different directory)" >> $TEMPLATE_NAME echo "WDIR=\"\$( cd \"\$( dirname \"\${BASH_SOURCE[0]}\" )\" >>/dev/null 2>&1 && pwd )\"" >> $TEMPLATE_NAME echo "CONFIG_FILE=\"\$WDIR/\${toolName}.cfg\"" >> $TEMPLATE_NAME echo "CONFIG_FILE_DEFAULT=\"\${CONFIG_FILE}.example\"" >> $TEMPLATE_NAME echo >> $TEMPLATE_NAME echo "step_config() {" >> $TEMPLATE_NAME echo " echo \"Called once before executing steps.\"" >> $TEMPLATE_NAME echo " echo \"e.g. to source a config file:\"" >> $TEMPLATE_NAME echo " #. \"\$CONFIG_FILE\"" >> $TEMPLATE_NAME echo "}" >> $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 -n "--" fi if [ $DRY -ne 0 ] || [ $VERBOSE -eq 1 ] ; then (set -x; : "${arr[@]}") fi if [ $DRY -eq 0 ] ; then "${arr[@]}" fi } # Handle dry run and verbose output for commands containing pipe and/or redirects # exep 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) ..." existsFunction step_config if [ $? -eq 0 ] ; then echo " [I] Running...step_config" step_config fi # 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;