From: APTX Date: Wed, 25 Dec 2024 12:07:27 +0000 (+0900) Subject: sh-task-run.sh X-Git-Url: https://gitweb.aptx.org/?a=commitdiff_plain;p=sh-task-run.git sh-task-run.sh Runs a set of commands as "tasks" and stopping on the first failure. Preserves state about ran scripts (which task it failed on). --- c8a399cef62836db4c4982b6133647a4bcaf8021 diff --git a/sh-task-run.sh b/sh-task-run.sh new file mode 100644 index 0000000..1c9d749 --- /dev/null +++ b/sh-task-run.sh @@ -0,0 +1,413 @@ +#!/bin/bash + +#if [ "$1" != "__SHTR_SOURCE" ] +#then +# # shellcheck source=/dev/null +# . <( . "${BASH_SOURCE[0]}" "__SHTR_SOURCE" "$@" ; declare -f init run) +# return 0 +#else +# shift +#fi + +COLOR_RED=31 +COLOR_GREEN=32 +COLOR_YELLOW=33 +COLOR_WHITE=37 +COLOR_DEFAULT_FG=39 +COLOR_DEFAULT_BG=49 +STYLE_BOLD="1" +STYLE_DIM="2" +STYLE_RESET="0" + +style() { + local style=${1:-$STYLE_RESET} + local fg=${2:-$COLOR_DEFAULT_FG} + local bg=${3:-$COLOR_DEFAULT_BG} + + echo -e -n "\x1b[${style};${fg};${bg}m" +} + +shtr_log_level=2 + +debug() { + [ "$shtr_log_level" -ge 3 ] || return 0 + >&2 echo -e "$(style "$STYLE_DIM")Debug:" "$@" "$(style)" +} + +info() { + [ "$shtr_log_level" -ge 2 ] || return 0 + >&2 echo -e "$@" "$(style)" +} + +warn() { + [ "$shtr_log_level" -ge 1 ] || return 0 + >&2 echo -e "$(style "" "$COLOR_YELLOW")Warning$(style):" "$@" "$(style)" +} + +fatal() { + >&2 echo -e "$(style "" "$COLOR_RED")Error$(style):" "$@" "$(style)" + exit 1 +} + +STATE_DIRECTORY="${HOME}/.config/sh-task-run" +HASH_COMMAND="sha1sum" + +mode="run" +step=1 +start_step=1 +total_steps=0 +init_ran=0 +run_opt_name="" +run_opt_ignore_error=0 +shtr_do_resume=0 + +SCRIPT_NAME="$(basename "$0")" +LIB_NAME="$(basename "${BASH_SOURCE[0]}")" + +SCRIPT_HASH="$("$HASH_COMMAND" "$0" | cut -d' ' -f1)" +START_TIME="$(date "+%s")" + +if [ "$SCRIPT_NAME" = "$LIB_NAME" ] +then + fatal "${LIB_NAME} must be sourced from another script" +fi + + +is_mode() { + local test_mode + for test_mode in "$@" + do + if [ "$mode" = "$test_mode" ] + then + return 0 + fi + done + return 1 +} + +get_script_state_path() { + local script_path + local state_file_name + local state_file + script_path="$(realpath "$0")" + state_file_name="${SCRIPT_NAME}-$(echo "${script_path}" | "$HASH_COMMAND" | cut -d' ' -f1).state" + state_file="${STATE_DIRECTORY}/${state_file_name}" + mkdir -p "$STATE_DIRECTORY" + echo "$state_file" +} + + +save_state() { + local cstep + local state_path + cstep="$1" + state_path="$(get_script_state_path)" + cat < "$state_path" +LAST_RUN=${START_TIME} +HASH=${SCRIPT_HASH} +CURRENT_STEP=${cstep} +END +} + +read_state_file() { + local file + file="$1" + ( + # shellcheck source=/dev/null + source "$file" + debug "HASH=$HASH" + debug "CURRENT_STEP=$CURRENT_STEP" + debug "LAST_RUN=$LAST_RUN" + if [ "$HASH" != "$SCRIPT_HASH" ] + then + warn "Script hash changed, discarding state" + exit 0 + fi + if is_num "$CURRENT_STEP" + then + echo "start_step=${CURRENT_STEP}" + fi + if [ -n "$LAST_RUN" ] + then + info "Last run on: $(date --date="@${LAST_RUN}")" + fi + ) +} + +load_state() { + local state_path + state_path="$(get_script_state_path)" + if [ ! -e "$state_path" ] + then + debug "State file not found ${state_path}" + return 0 + fi + eval "$(read_state_file "$state_path")" +} + +clear_state() { + local state_path + state_path="$(get_script_state_path)" + rm "$state_path" +} + +step_str() { + local op + local name + local cstep + op="$1" + name="$2" + cstep="$3" + if [ -z "$name" ] + then + echo "${op} step $(style "$STYLE_BOLD")${cstep}$(style) of $(style "$STYLE_BOLD")${total_steps}$(style)" + else + echo "${op} \"${name}\", step $(style "$STYLE_BOLD")${cstep}$(style) of ${total_steps}$(style)" + fi +} + +is_arg() { + local n + local v + n="$1" + v="$2" + if [ "$n" -eq 0 ] + then + return 1 + fi + if [ -z "$v" ] || [ "$v" = "--" ] + then + return 1 + fi + return 0 +} + + +has_opt_args() { + while [ "$1" != "" ] + do + if [ "$1" = "--" ] + then + return 0 + fi + shift + done + return 1 +} + +parse_run_opts() { + run_opt_name="" + run_opt_ignore_error=0 + + if ! has_opt_args "$@" + then + return 0 + fi + + while [ "$#" -gt 0 ] + do + case "$1" in + --name) + if ! is_arg "$#" "$2" + then + fatal "--name expects an argument" + fi + run_opt_name="$2" + shift + ;; + --ignore-errors) + run_opt_ignore_error=1 + ;; + --) + break + ;; + *) + fatal "Unknown option: ${1}" + ;; + esac + shift + done +} + +run() { + local cstep + + if [ "$init_ran" -ne 1 ] + then + fatal "init not called!" + fi + + cstep=$step + step=$((step+1)) + + if is_mode "count-internal" "count" + then + echo "$((step-1))" + return 0 + fi + + parse_run_opts "$@" + + + if has_opt_args "$@" + then + while [ "$#" -gt 0 ] + do + if [ "$1" == "--" ] + then + shift + break + fi + shift + done + fi + + if [ "$cstep" -lt "$start_step" ] + then + info "$(step_str "$(style "$STYLE_BOLD" "$COLOR_WHITE")Skippping$(style)" "$run_opt_name" "$cstep"):" "$@" + return 0 + fi + info "$(step_str "$(style "$STYLE_BOLD" "$COLOR_WHITE")Starting$(style)" "$run_opt_name" "$cstep"):" "$@" + if is_mode run + then + if ! "$@" + then + if [ "$run_opt_ignore_error" -eq 1 ] + then + warn "$(step_str "$(style "$STYLE_BOLD" "$COLOR_RED")Failed$(style)" "$run_opt_name" "$cstep"):" "$@" "continuing..." + else + save_state "$cstep" + fatal "$(step_str "$(style "$STYLE_BOLD" "$COLOR_RED")Failed$(style)" "$run_opt_name" "$cstep"):" "$@" + fi + fi + else + echo "Would run:" "$@" + fi + info "$(step_str "$(style "$STYLE_BOLD" "$COLOR_GREEN")Finished$(style)" "$run_opt_name" "$cstep"):" "$@" + if [ "$cstep" -eq "$total_steps" ] + then + clear_state + info "All tasks ran!" + else + save_state "$cstep" + fi +} + +is_num() { + case "$1" in + *[!0-9]* | '') + return 1 + ;; + *) + return 0 + ;; + esac +} + +parse_opts() { + while [ "$#" -gt 0 ] + do + case "$1" in + --resume | -r) + shtr_do_resume=1 + ;; + --pretend) + mode="pretend" + ;; + --count) + mode="count" + ;; + --count-internal) + mode="count-internal" + ;; + -d | --debug) + shtr_log_level=3 + ;; + -q | --quiet) + shtr_log_level=0 + ;; + *) + if is_num "$1" + then + start_step="$1" + else + warn "${1} is not a number, expected step number" + fi + esac + shift + done +} + +init() { + if [ "$init_ran" -eq 0 ] + then + init_ran=1 + else + return 0 + fi + + debug "Running script ${SCRIPT_NAME}" + parse_opts "$@" + + if [ "$shtr_do_resume" -eq 1 ] + then + load_state + fi + + if ! is_mode "count-internal" + then + total_steps=$($0 --count-internal 2> /dev/null | tail -1) + fi + + if is_mode "count" + then + echo "$total_steps" + exit 0 + fi + + if is_mode "count-internal" + then + total_steps=0 + return 0 + fi + + if [ -z "$start_step" ] + then + start_step=0 + fi + + if ! is_num "$total_steps" || [ "$total_steps" -eq 0 ] + then + fatal "No steps defined" + fi + + if [ "$start_step" -gt "$total_steps" ] + then + fatal "Start step is higher than the number of steps. This can also happen if the resume state is invalid" + fi +} + +count_positional_args() { + local count + + count=0 + while [ "$1" != "" ] && [ "$1" != "--" ] + do + debug "Arg ${count}: $1" + count=$((count+1)) + shift + done + shift + debug "$count" + debug "$#" +} + +test_args() { + debug "Count: $#" + count_positional_args "$@" +} + +if [ "${NO_AUTO_INIT:-0}" -ne 1 ] +then + init "$@" +fi