#!/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 generated template which can be generated by calling this ## script directly) ## Version information VERSION_REV=12 VERSION_MAJOR=0 VERSION_MINOR=0 ## Start of generic script part QUIET=0 DRY=0 VERBOSE=0 SINGLE=0 ERNO=0 SEQUENCER_ARGS= STEP_ARGS= STEP_RETURN=255 MAX_STEP=512 ALIAS= CONTEXT_HELP=0 SEQ_CONFIG_NAME=".seqs" SEQ_CONFIG_HOME="$HOME/$SEQ_CONFIG_NAME" SEQ_CONFIG_FILE= SEQ_PROFILE_NAME=default TEMPLATE_NAME=seqTemplateExample.sh MISSING_CONF=missingConf.log VERSION_STRING="${VERSION_REV}.${VERSION_MAJOR}.${VERSION_MINOR}" helpSequencer() { cat <,<,|). Supporting: dry-run (-d): only print command without execution verbose (-v): print command before execution exep "[COMMANDLINE]" See exe, but support for pipes or redirects. Important: - Shell commands cd, read, ... won't work because COMMANDLINE is started in a new shell. - All apostrophes need to be esacped since the command line is given as string. initSeqConfig [OPTION] [TEMPLATE] Create a configuration file in $SEQ_CONFIG_HOME/ 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 addConf [SOURCE TYPE] Trying to write or append text or a file () 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 "$MISSING_CONF")" 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. step 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. echoerr [...] echo to stderr [...] : all parameter are forwarded to echo echoinfo [...] echo additional correctly indented line to step info [...] : all parrameter are forwared to echo endCheckEmpty [DESCRIPTION] exit 666 if variable is empty : Name used within eval [DESCRIPTION] : Additional text for error output saveReturn [ERRORCODE] Save ERRORCODE if it is != 0 for later use with endReturn getReturn Return last saved error code endReturn [OPTIONS] [MESSAGE] 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 } # Echo to stderr echoerr() { >&2 echo "$@"; } # Echo additional line to info correctly indented INDENT_HELP=' : ' INDENTAPPEND_HELP=' ' INDENTAPPEND_INFO=' ' echoinfo() { if [ $CONTEXT_HELP -ne 0 ] ; then printf '%s' "$INDENTAPPEND_HELP"; echo "$@" else printf '%s' "$INDENTAPPEND_INFO"; echo "$@" fi } # endCheckEmpty [DESCRIPTION] # DESCRIPTION : Optional text for error endCheckEmpty() { eval 'local ref=$'$1 if [ -z $ref ] ; then if [ ! -z "$2" ] ; then echoerr -e " [E] $2\n Sequence stopped." else echoerr -e " [E] $1 must not be empty.\n Sequence stopped." fi 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 arg 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 } # 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 arg local sourceAlways=0 local createEmpty=0 local seqProfiles=0 for arg in "$@" ; do case "$1" in -e) createEmpty=1 shift ;; -p) seqProfiles=1 shift ;; -t) sourceAlways=1 shift ;; esac done local configLoc="$SEQ_CONFIG_HOME/$1" if [ $seqProfiles -ne 0 ] ; then configLoc="$SEQ_CONFIG_HOME/$1/${SEQ_PROFILE_NAME}.cfg" fi local configDir="$(dirname $configLoc)" local configTemplate="$2" # Create config subdir in users home if [ ! -e "$configDir/" ] ; then echo -n " [I] Creating $(realpath $configDir)..." exe mkdir -p "$configDir" && echo "Ok" || echo "Nok" exe chmod 700 "$configDir" fi SEQ_CONFIG_HOME="$configDir" if [ -s "$configLoc" ] ; then if [ $QUIET -ne 2 ] ; then echo " [I] Using configuration file: $configLoc" ; fi SEQ_CONFIG_FILE="$configLoc" . "$configLoc" return 0 fi # Config does not exist, check for template if [ -s "$configTemplate" ] ; then # Check first if there is an existing configuration at the templates position local configExists="$(dirname $configTemplate)/$1" if [ -s "$configExists" ] ; then exe mv "$configExists" "$configLoc" endReturn -o $? "Unable to use existing configuration: $configExists" echoerr " [I] Using existing configuration: $configExists" echoerr " (Moved to $configDir)" . "$configLoc" return 0 fi exe cp -ar "$configTemplate" "$configLoc" endReturn -o $? "Failed to create configuration" exe chmod 600 "$configLoc" if [ $sourceAlways -eq 0 ] ; then echoerr " [W] Seq configuration created from template but not used" echoerr " Please modify "$configLoc" first and restart sequence" return 1 else echo " [W] Using seq configuration from template $configTemplate" echo " (Copied to $configDir)" SEQ_CONFIG_FILE="$configLoc" . "$configLoc" return 0 fi else echo " [W] Seq configuration template not found" fi if [ $createEmpty -ne 0 ] ; then # Create empty config file echo " [W] Created empty configuration file $configLoc" exe touch "$configLoc" exe chmod 600 "$configLoc" return 2 fi echoerr " [E] No seq configuration created" return 3 } # 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 arg local confMode="" local transferCmd="echo" for arg 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 echoerr " [E] 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 echoerr " [E] Source: \"$source\" does not exist" return 1; fi if [ "$dest" == "" ] ; then echoerr " [E] Destination empty" return 1; fi if [ "$DRY" -ne 0 ] ; then echo " [I] Writing $dest ...dry-run" return 0; fi 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" 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="${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 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 local helpText="needs to be added manually" if [ "$confMode" == "-s" ] ; then helpText="not overwritten" fi echo "#--- \"$dest\" $helpText (Option: $confMode) ---" >> "$MISSING_CONF" $transferCmd "$source" >> "$MISSING_CONF" echo >> "$MISSING_CONF" echoerr " [W] Check $(realpath "$MISSING_CONF") for configuration conflicts ($dest)" 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 -ne 0 ] ; then return $NOTFOUND fi if [ $QUIET -ne 2 ] ; then echo -en "\n [STEP $1] " existsFunction step_${1}_info if [ $? -eq 0 ] ; then step_${1}_info $1 "${STEP_ARGS[@]}" else # Add newline if no info is given echo fi fi if [ $QUIET -eq 0 ] ; then read -p "Start: (y)es/[n]o/(s)kip? " answer case $answer in [yY]) step_$1 $1 "${STEP_ARGS[@]}" STEP_RETURN=$? ;; [sS]) # skip step STEP_RETURN=0 return 0 ;; *) local stepId="$1" # Display alias if exists existsFunction step_${1}_alias if [ $? -eq 0 ] ; then step_${1}_alias stepId="$ALIAS" fi echoerr " [I] Stopping sequence at step: $stepId" exit 1; ;; esac else step_$1 $1 "${STEP_ARGS[@]}" STEP_RETURN=$? fi } # checkStep # return 0 - for invalid step # return Step Number # 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 ref=0 fi else ref=$1 fi if (( $ref < 1 || $ref > $MAX_STEP )) ; then echoerr " [E] Invalid step: $ref" return 0 else existsFunction step_$ref if [ $? -eq 0 ] ; then return $ref else # step doesn't exist echoerr " [E] Invalid step: $ref" return 0 fi fi } # step # execute given step step() { local stepNo=0 local stepArgs=("$@") checkStep "$1" stepNo=$? if [ "$stepNo" == "0" ] ; then return 1 else step_$stepNo "${stepArgs[@]}" fi } # continous # (max $MAX_STEP) # execute sequence continously from given starting step continous() { local i local step=0 checkStep "$1" step=$? if [[ $step == 0 ]] ; then return 1 fi if [ $QUIET -ne 2 ]; then echo " [I] Starting sequence $(realpath $0) ..."; fi for ((i=$step; i<=${MAX_STEP}; i++)); do execute -q $i local res=$? if [ $res -ne 0 ] ; then break fi if [ $STEP_RETURN -ne 0 ] ; then break fi done return $STEP_RETURN } # selection # execute given step list # e.g.: selection -q (1, 4, 12) selection() { local i local step=0 local array=("$@") if [ ${#array[@]} -eq 0 ] ; then return 1 fi if [ $QUIET -ne 2 ]; then echo " [I] Starting sequence $(realpath $0) ..."; fi for i in ${array[@]} ; do checkStep "$i" step=$? if [ $step -eq 0 ] ; then return 1 else execute $step fi done return $STEP_RETURN } # Creating a minimal seq (step definition) template createTemplate() { if [ -f $TEMPLATE_NAME ] ; then return 1 fi cat > $TEMPLATE_NAME << TEMPLATE_EOF #!/bin/bash toolName=mytool # Get script working directory # (when called from a different directory) WDIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" >>/dev/null 2>&1 && pwd)" CONFIG=0 SCRIPT_NAME=\$(basename -- \$0) SCRIPT_NAME=\${SCRIPT_NAME%%.*} CONFIG_FILE_NAME="\${SCRIPT_NAME}.cfg" CONFIG_FILE_TEMPLATE="\$WDIR/\${CONFIG_FILE_NAME}.example" step_config() { echo "Called once before executing steps." ## e.g. to source a config file manually: #. "\$CONFIG_FILE" ## or to use sequencer api with global config file: #initSeqConfig "\$CONFIG_FILE_NAME" "\$CONFIG_FILE_TEMPLATE" ## or to use sequencer api with profile config file support: #initSeqConfig -p "\$SCRIPT_NAME" "\$CONFIG_FILE_TEMPLATE" #if [ \$? -eq 0 ] ; then # CONFIG=1 #fi } step_1_info() { echo "My custom step"; } step_1_alias() { ALIAS="begin"; } step_1() { echo "Doing something for step \$1 ..." echo "Command line arguments starting with argument 2: \$@" # Use exe for regular command # Use exep "command" for commands containing pipes or redirects exe ls exep "dmesg | grep usb" } VERSION_SEQREV=${VERSION_REV} . $0 TEMPLATE_EOF 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() { local i 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 } # displayHelp [NO TEMPLATE] # [NO TEMPLATE] # 0 (default) : Ask for template creation # 1 : Do not ask for template creation # - Always display sequencer help and, if available, sequence help # - Cluster continous (more than 1) steps visually together displayHelp() { local i local answer local clusterSize=0 local lastClusterSize=0 local createTemplate=1 local stepsFound=0 CONTEXT_HELP=1 helpSequencer if [ ! -z $1 ] && [ $1 -eq 1 ] ; then createTemplate=0 fi # 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" if [ $createTemplate -ne 0 ] ; then 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 fi 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 if [ $clusterSize -ne 0 ] ; then lastClusterSize=$clusterSize clusterSize=0 fi continue fi ((clusterSize+=1)) if [ $lastClusterSize -gt 1 ] ; then lastClusterSize=0 echo fi printf ' Step %3s ' $i # Display alias if exists existsFunction step_${i}_alias if [ $? -eq 0 ] ; then step_${i}_alias echo " = $ALIAS" printf '%s' "$INDENT_HELP" 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 echo fi CONTEXT_HELP=0 } # 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 arg 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 SEQUENCER_ARGS+=" $1" shift ;; --help|-h) # show only help displayHelp 1 exit 0; ;; --helpapi|-ha) #show build-in functions helpApi exit 0; ;; --profile|-p) # seq profile name SEQUENCER_ARGS+=" $1 $2" shift SEQ_PROFILE_NAME="$1" shift ;; --quiet|-q|-qq) # detect if option quiet is available SEQUENCER_ARGS+=" $1" if [ "$1" == "-qq" ] ; then QUIET=2 else QUIET=1 fi shift ;; --single|-s) # execute only one step and stop SEQUENCER_ARGS+=" $1" SINGLE=1 shift ;; --verbose|-v) # set verbose flag SEQUENCER_ARGS+=" $1" VERBOSE=1 shift ;; --version) # version request showVersion exit 0; ;; esac done if [ -z "$1" ] || [ "$1" == "" ] ; then if [ $QUIET -eq 0 ] ; then # Empty -> show help displayHelp fi # 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 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 # run configuration for seq only if available and if first step is valid existsFunction step_config if [ $? -eq 0 ] ; then checkStep "${START[0]}" if [ $? -ne 0 ] ; then if [ $QUIET -ne 2 ] ; then echo " [I] Configuring sequence (step_config) ..." ; fi step_config "${STEP_ARGS[@]}" else return 1 fi fi # check if more than one step is given and select execution mode if [ $SINGLE -ne 0 ] ; then selection "${START[0]}" elif [ "${#START[@]}" -gt "1" ]; then selection "${START[@]}" else continous $START fi } main "$@" MAINRETURN=$? if [ $QUIET -ne 2 ] ; then echo echo "${0##*/} finished" fi exit $MAINRETURN;