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.

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")

while [[ "$1" != "" ]]; do
    case $1 in
        -h|--help)
            echo "$help_text"
            exit
            ;;
        *)
            break
            ;;
    esac
    shift
done

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}') ;;
    kb) bytes=$(echo "$val" | awk '{printf "%.0f", $1 * 1024}') ;;
    mb) bytes=$(echo "$val" | awk '{printf "%.0f", $1 * 1024 * 1024}') ;;
    gb) bytes=$(echo "$val" | awk '{printf "%.0f", $1 * 1024 * 1024 * 1024}') ;;
    tb) bytes=$(echo "$val" | awk '{printf "%.0f", $1 * 1024 * 1024 * 1024 * 1024}') ;;
    "") 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_size() {
  local commas=$(printf "%0.f" "$1" | sed ':a;s/\B[0-9]\{3\}\>/,&/;ta')
  local human=$(awk -v b="$1" '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
  }')
  echo "$commas ($human)"
}

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_size $sum)"
echo "Avg:   $(fmt_size $avg)"
echo "Min:   $(fmt_size $min)"
echo "Max:   $(fmt_size $max)"