--- /dev/null
+#!/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 <<END > "$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