Parse command-line options without getopts

Bash
#!/usr/bin/env bash
# Parse short and long options with a plain while/case loop.
set -euo pipefail

verbose=false
output=""
files=()

usage() {
  printf 'usage: %s [-v] [-o FILE] [path...]\n' "${0##*/}" >&2
  exit 64
}

while (($# > 0)); do
  case "$1" in
    -v|--verbose) verbose=true; shift ;;
    -o|--output)  output="${2:?missing value for $1}"; shift 2 ;;
    -h|--help)    usage ;;
    --)           shift; files+=("$@"); break ;;
    -*)           printf 'unknown option: %s\n' "$1" >&2; usage ;;
    *)            files+=("$1"); shift ;;
  esac
done

((${#files[@]} > 0)) || usage
if [[ $verbose == true ]]; then
  printf 'verbose=on output=%s paths=%d\n' "${output:-stdout}" "${#files[@]}"
fi