tot - Sum, average, min, max calculator with size unit support

#!/bin/bash
help_text="
NAME
    tot - Sum, average, min, max for a list of values with optional size units.

USAGE
    tot [options] [values...]

OPTIONS
    -h|--help
        Show help text.

    -u [<unit>]
        Display values with size units.  Without a value, auto-selects the
        best unit.  With a value (b, kb, mb, gb, tb), displays all values
        in that unit.  Units are also shown automatically when any input
        value includes a unit suffix.

DESCRIPTION
    Calculates sum, average, min and max for a list of numeric values.
    Values can include size units: b, kb, mb, gb, tb (e.g. 2.5gb, 300kb).
    Values without a unit are treated as plain numbers.

    If values are passed as arguments they are used directly.  Otherwise
    an interactive prompt reads values (one or many per line) until Ctrl-D.

AUTHOR
    mjnurse.github.io - 2026
"
help_line="Sum, average, min, max calculator with size unit support"
web_desc_line="Sum, average, min, max calculator with size unit support"

tmp="${help_text##*USAGE}"
usage=$(echo "Usage: ${tmp%%OPTIONS*}" | tr -d "\n" | sed "s/  */ /g")

show_units=false
forced_unit=""

while [[ "$1" != "" ]]; do
    case $1 in
        -h|--help)
            echo "$help_text"
            exit
            ;;
        -u)
            show_units=true
            if [[ -n "$2" && "$2" =~ ^(b|kb|mb|gb|tb)$ ]]; then
                forced_unit="$2"
                shift
            fi
            ;;
        *)
            break
            ;;
    esac
    shift
done

has_unit_input=false

parse_token() {
  local token=$1
  local val=$(echo "$token" | grep -oiE '^[0-9]*\.?[0-9]+')
  local unit=$(echo "$token" | grep -oiE '[a-z]+$' | tr '[:upper:]' '[:lower:]')

  if [[ -z "$val" ]]; then
    echo "Skipping invalid: $token" >&2
    return
  fi

  local bytes
  case "$unit" in
    b)  bytes=$(echo "$val" | awk '{printf "%.0f", $1}'); has_unit_input=true ;;
    kb) bytes=$(echo "$val" | awk '{printf "%.0f", $1 * 1024}'); has_unit_input=true ;;
    mb) bytes=$(echo "$val" | awk '{printf "%.0f", $1 * 1024 * 1024}'); has_unit_input=true ;;
    gb) bytes=$(echo "$val" | awk '{printf "%.0f", $1 * 1024 * 1024 * 1024}'); has_unit_input=true ;;
    tb) bytes=$(echo "$val" | awk '{printf "%.0f", $1 * 1024 * 1024 * 1024 * 1024}'); has_unit_input=true ;;
    "") bytes=$(echo "$val" | awk '{printf "%.0f", $1}') ;;
    *)  echo "Unknown unit '$unit' in: $token" >&2; return ;;
  esac

  values+=("$bytes")
}

values=()

if [[ $# -gt 0 ]]; then
  for token in "$@"; do
    parse_token "$token"
  done
else
  echo "Enter values (e.g. 100 2.5gb 300kb), Ctrl-D to finish:"
  echo ""
  while IFS= read -r line || [[ -n "$line" ]]; do
    for token in $line; do
      parse_token "$token"
    done
  done
fi
if [[ ${#values[@]} -eq 0 ]]; then
  echo "No values entered."
  exit 1
fi

fmt_val() {
  local v=$1
  local commas=$(printf "%0.f" "$v" | sed ':a;s/\B[0-9]\{3\}\>/,&/;ta')

  if [[ "$show_units" == true || "$has_unit_input" == true ]]; then
    local human
    if [[ -n "$forced_unit" ]]; then
      case "$forced_unit" in
        b)  human=$(awk -v b="$v" 'BEGIN { printf "%.0f b", b }') ;;
        kb) human=$(awk -v b="$v" 'BEGIN { printf "%.2f kb", b / 1024 }') ;;
        mb) human=$(awk -v b="$v" 'BEGIN { printf "%.2f mb", b / 1024^2 }') ;;
        gb) human=$(awk -v b="$v" 'BEGIN { printf "%.2f gb", b / 1024^3 }') ;;
        tb) human=$(awk -v b="$v" 'BEGIN { printf "%.2f tb", b / 1024^4 }') ;;
      esac
    else
      human=$(awk -v b="$v" 'BEGIN {
        if (b >= 1024^4)      printf "%.2f tb", b / 1024^4
        else if (b >= 1024^3) printf "%.2f gb", b / 1024^3
        else if (b >= 1024^2) printf "%.2f mb", b / 1024^2
        else if (b >= 1024)   printf "%.2f kb", b / 1024
        else                  printf "%.0f b", b
      }')
    fi
    echo "$commas ($human)"
  else
    echo "$commas"
  fi
}

sum=0
min=${values[0]}
max=${values[0]}

for v in "${values[@]}"; do
  sum=$(echo "$sum + $v" | bc)
  if (( $(echo "$v < $min" | bc -l) )); then min=$v; fi
  if (( $(echo "$v > $max" | bc -l) )); then max=$v; fi
done

avg=$(echo "scale=0; $sum / ${#values[@]}" | bc)

echo ""
echo "Count: ${#values[@]}"
echo "Sum:   $(fmt_val $sum)"
echo "Avg:   $(fmt_val $avg)"
echo "Min:   $(fmt_val $min)"
echo "Max:   $(fmt_val $max)"