]> Some of my projects - sh-task-run.git/commitdiff
sh-task-run.sh master
authorAPTX <aptx@aptx.org>
Wed, 25 Dec 2024 12:07:27 +0000 (21:07 +0900)
committerAPTX <aptx@aptx.org>
Sun, 5 Jan 2025 02:06:06 +0000 (11:06 +0900)
Runs a set of commands as "tasks" and stopping on the first failure.
Preserves state about ran scripts (which task it failed on).

sh-task-run.sh [new file with mode: 0644]

diff --git a/sh-task-run.sh b/sh-task-run.sh
new file mode 100644 (file)
index 0000000..1c9d749
--- /dev/null
@@ -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 <<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