diff --git a/sequencer.sh b/sequencer.sh index db0bd2f..a73b6e5 100755 --- a/sequencer.sh +++ b/sequencer.sh @@ -45,10 +45,17 @@ set -o pipefail _sqr_dry=0 _sqr_verbose=0 _sqr_errno=0 + _sqr_contextHelp=0 + _sqr_contextExe=0 + _sqr_editor= + + ## 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' - sqr_editor= - - # Colors + ## Terminal colors col_black= ; [ -t 1 ] && col_black='\033[0;30m' col_darkgrey= ; [ -t 1 ] && col_darkgrey='\033[1;30m' col_red= ; [ -t 1 ] && col_red='\033[0;31m' @@ -68,6 +75,46 @@ set -o pipefail col_off= ;[ -t 1 ] && col_off='\033[0m' # No Color } +helpSequencer() { + cat < 0 = fatal (stop) @@ -199,6 +246,29 @@ set -o pipefail tput setab 7 ;; esac } + + # Echo additional line to info correctly indented + 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 @@ -252,6 +322,11 @@ exists() { [[ -n "${!1:-}" ]] fi } +# editor [FILE(s)..] +# Starts the detected system text editor +editor() { + "${_sqr_editor}" "$@" +} # interactive # Started with -q to use defaults for confirmations interactive() { @@ -277,10 +352,17 @@ dry() { verbose() { (( _sqr_verbose )) } +contextHelp() { + (( _sqr_contextHelp )) +} +contextExe() { + (( _sqr_contextExe )) +} ### interactive # confirm [OPTIONS] [--] [QUESTION] # Default (empty character) = no +# Invalid character trigger the default # # [OPTIONS] # -f : interactive even if quiet @@ -290,7 +372,7 @@ verbose() { confirm() { sqr::debugPause local arg= - local rexReply='^[Yy]$' # default no + local rexReply='^[Yy]+$' # default no local inputHelp='[y/N] ' # default no local noHelp=0 local force=0 @@ -306,7 +388,7 @@ confirm() { noHelp=1 shift ;; -y) - rexReply='^[Yy]*$' # default yes + rexReply='^[^Nn]*$' # default yes inputHelp='[Y/n] ' # default yes shift ;; esac @@ -389,9 +471,9 @@ getReturn() { } # endReturn [-f] [-o ERRORCODE] [MESSAGE] -# -f : force exit with $_sqr_errno without user input -# -o : override and check given error code -# MESSAGE : Custom error message +# -f : force exit with $_sqr_errno without user input +# -o : override and check given [ERRORCODE] +# [MESSAGE] : Custom error message # endReturn() { local arg @@ -445,9 +527,9 @@ endReturn() { else error -e "Return value ${errorCode} detected." fi - if confirm -y "End sequence"; then + if confirm -y "End sequence" ; then error -e "Sequence stopped" - exit "${errorCode}"; + exit "${errorCode}" else # reset saved error code if user chooses to continue _sqr_errno=0 @@ -457,6 +539,183 @@ endReturn() { fi } +# 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="" + + # Check if string is a number or alias + if ! [[ "${1:-}" =~ ${checkStep_rex} ]] ; then + eval 'checkStep_ref=${_sqr_alias_'"${1:-}"'}' + # Catch special character after eval + 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 "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 "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() { + local i + for ((i=1; i<=_sqr_stepMax; i++)); do + # Check for alias definition + exists -f "step_${i}_alias" || continue + + # Function returns step alias + eval '_sqr_alias_'"$("step_${i}_alias")"'='"$i" + done +} + +# 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 + sqr::echo "TODO: createTemplate" + if [ $? -eq 0 ] ; then + printf '\n %s\n' "$TEMPLATE_NAME created." + else + printf '\n %s\n' "$TEMPLATE_NAME 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" + 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] @@ -526,6 +785,8 @@ sqr::noEditor() { } sqr::main() { + local quickStartOne=0 + # options check for arg in "$@" ; do case "$1" in @@ -535,6 +796,9 @@ sqr::main() { --dry-run|-d) _sqr_dry=1 shift ;; + --help|-h) # show only help + displayHelp 1 "${2:-}" + exit 0;; --liststeps|-ls) shift listSteps "${1:-}" @@ -563,19 +827,40 @@ sqr::main() { trap 'sqr::error_report "${FUNCNAME:-.}" ${LINENO}' ERR fi + # Don't show help if only configuration should be edited + # TODO [ $SEQ_CONFIG_EDIT -ne 0 ] && [ -z "$1" ] && QUIET=2 + + if [ -z "${1:-}" ] && [ $quickStartOne -eq 0 ] ; then + if ! quiet ; then + # Empty -> show help + displayHelp + fi + # Assume starting at one for interactive mode + START=1 + elif [ $quickStartOne -ne 0 ] ; then + EMPTYCALL=0 + START=1 + STEP_ARGS=( "$@" ) + else + EMPTYCALL=0 + read -r -a START <<< "$1" + shift + STEP_ARGS=( "$@" ) + fi + # Determine system default editor # Change with update-alternatives --config editor - sqr_editor="$(realpath -eq "/etc/alternatives/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)" + [ -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" + [ -z "${_sqr_editor}" ] && _sqr_editor="sqr::noEditor" sqr::printf 'Running...\n' seq_config 2>/dev/null || true - step_1 "$@" + step 1 "$@" } sqr::main "$@"