#!/bin/sh
# conform-doc-check [-h] [-q] <page> <script>... -- the options and the environment variables of
# scripts and of their manual page are the same set. One stderr line per difference, rc 1 if any,
# 2 on usage. Reads files only; nothing is run.
#   options   script: the letters of the getopts string   page: .It Fl <letter>
#   conf      a script that takes -f has a section 5 page (*.5) beside its page
#   environ   script: $NAME read and never assigned       page: .It Ev <NAME>
# Several scripts share one page (conform-port and conform-port-survey): their sets are united.
PATH=/bin:/sbin:/usr/bin:/usr/sbin
set -u

usage() { echo "usage: conform-doc-check [-h] [-q] page script ..." >&2; }
quiet=0
while getopts hq o; do
	case $o in
	h) usage; exit 0 ;;
	q) quiet=1 ;;
	*) usage; exit 2 ;;
	esac
done
shift $((OPTIND - 1))
[ $# -ge 2 ] || { usage; exit 2; }
page=$1; shift
for f in "$page" "$@"; do [ -f "$f" ] || { echo "conform-doc-check: $f: no such file" >&2; exit 2; }; done

faults=0
# diff of two sorted word lists: words only in the first, one per line
only_in() { echo "$1" | tr ' ' '\n' | grep -v '^$' | sort -u > "$tmp.a"; echo "$2" | tr ' ' '\n' | grep -v '^$' | sort -u > "$tmp.b"; comm -23 "$tmp.a" "$tmp.b"; }
tmp=$(mktemp "${TMPDIR:-/tmp}/conform-doc-check.XXXXXXXXXX") || exit 2
trap 'rm -f "$tmp" "$tmp.a" "$tmp.b"' EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

code_opts=$(for s in "$@"; do sed -n 's/^[ 	]*\(while[ 	][ 	]*\)\{0,1\}getopts[ 	][ 	]*\([^ 	;]*\).*/\2/p' "$s"; done | tr -d ':"'"'" | sed 's/./& /g')
page_opts=$(awk '$1 == ".It" && $2 == "Fl" && $3 ~ /^[A-Za-z0-9]$/ { print $3 }' "$page")

# variables read, minus those the script assigns or the shell owns
code_env=$(for s in "$@"; do
	awk '{ line = $0; sub(/^[ \t]*#.*/, "", line)
		while (match(line, /\$\{?[A-Z][A-Z0-9_]*/)) { n = substr(line, RSTART, RLENGTH); sub(/^\$\{?/, "", n); used[n] = 1; line = substr(line, RSTART + RLENGTH) }
		s = $0; sub(/^[ \t]*#.*/, "", s)
		while (match(s, /(^|[^A-Za-z0-9_${])[A-Z][A-Z0-9_]*=/)) { a = substr(s, RSTART, RLENGTH); sub(/^[^A-Z]*/, "", a); sub(/=.*/, "", a); rest = substr(s, RSTART + RLENGTH); if (rest !~ ("[$][{]?" a "([^A-Za-z0-9_]|$)")) set[a] = 1; s = rest } }
		END { for (n in used) if (!(n in set)) print n }' "$s"
done | grep -v -E '^(OPTARG|OPTIND|IFS|PWD|HOME|SECONDS|RANDOM|ENV|NF|NR|FNR|PPID|LINENO|PS[0-9])$' | sort -u)
page_env=$(awk '$1 == ".It" && $2 == "Ev" { print $3 }' "$page")

report() { # kind sign words
	for w in $3; do echo "$page: $1 $2$w $4" >&2; faults=$((faults + 1)); done
}
report option - "$(only_in "$code_opts" "$page_opts")" "is in the code and not in the page"
report option - "$(only_in "$page_opts" "$code_opts")" "is in the page and not in the code"
case " $code_opts " in *" f "*) ls "$(dirname "$page")"/*.5 > /dev/null 2>&1 || { echo "$page: the code takes -f and the directory of the page holds no section 5 page" >&2; faults=$((faults + 1)); } ;; esac
report variable "" "$(only_in "$code_env" "$page_env")" "is read by the code and not in the page"
report variable "" "$(only_in "$page_env" "$code_env")" "is in the page and not read by the code"

[ "$quiet" -eq 1 ] || echo "conform-doc-check: $faults faults"
[ "$faults" -eq 0 ]
