#!/bin/sh
# conform-conventions [-hnq] file ... -- the command line and the secret conventions of the family.
# One stderr line per fault, "<file>:<line>: <what>"; stdout: the count. rc 0 clean, 1 faults, 2
# usage. -n is accepted for the convention of the family and changes nothing.
#   A getopts line that carries the marker conform-ok is an exception written in the script and
#   is not judged.
#   scripts (*.sh, or a shell shebang) that call getopts:
#     the reserved letters keep their shape: f takes an argument; n v q d h take none
#     h is declared, and its arm prints on standard output and does not exit with a code above 0
#     no long option: no case arm of the form --name)
#   example files (*.example, *.conf, files under an examples directory):
#     no key named like a secret (PASS, SECRET, TOKEN, KEY) holds a value on the line
PATH=/bin:/sbin:/usr/bin:/usr/sbin
set -u
export LC_ALL=C

usage() { echo "usage: conform-conventions [-hnq] file ..." >&2; }
quiet=0
while getopts hnq o; do
	case $o in
	h) usage; exit 0 ;;
	n) ;;
	q) quiet=1 ;;
	*) usage; exit 2 ;;
	esac
done
shift $((OPTIND - 1))
[ $# -ge 1 ] || { usage; exit 2; }

faults=0
for f in "$@"; do
	[ -f "$f" ] || { echo "conform-conventions: $f: no such file" >&2; exit 2; }
	case $f in
	*.sh) kind=script ;;
	*.example | *.conf | */examples/*) kind=example ;;
	*) read -r first < "$f" 2>/dev/null || first=
	   case $first in '#!/bin/sh'* | '#!/bin/ksh'*) kind=script ;; *) kind=skip ;; esac ;;
	esac
	[ "$kind" != skip ] || continue
	if [ "$kind" = script ]; then
		out=$(awk '
			function fault(m) { printf "%s:%d: %s\n", FILENAME, FNR, m; n++ }
			function strip(s) { sub(/^[ \t]*#.*/, "", s); return s }
			{ l = strip($0) }
			$0 ~ /getopts[ \t]+[:A-Za-z0-9]+/ && $0 ~ /conform-ok/ { seen = 1; next }
			l ~ /getopts[ \t]+[:A-Za-z0-9]+/ && !seen {
				seen = 1; s = l; sub(/.*getopts[ \t]+/, "", s); sub(/[ \t;].*/, "", s); sub(/^:/, "", s); opts = s; optline = FNR
				for (i = 1; i <= length(s); i++) {
					c = substr(s, i, 1); colon = (substr(s, i + 1, 1) == ":")
					if (c == ":") continue
					if (c == "f" && !colon) fault("-f must take an argument: write f:")
					if (index("nvqdh", c) > 0 && colon) fault("-" c " takes no argument: write " c " without a colon")
				}
				if (index(s, "h") == 0) fault("-h is not declared in the getopts string")
			}
			l ~ /^[ \t]*--[A-Za-z][A-Za-z-]*([|][^)]*)?\)/ { fault("long option in a case arm: " l) }
			seen && l ~ /^[ \t]*h\)/ { inh = 1 }
			inh { if (l ~ />&2/ ) fault("the -h arm writes on standard error; usage on standard output, rc 0")
			      if (l ~ /exit[ \t]+[1-9]/) fault("the -h arm exits with a code above 0")
			      if (l ~ /;;/) inh = 0 }
			END { exit n > 0 ? 1 : 0 }' "$f" 2>&1)
	else
		out=$(awk '
			{ l = $0; sub(/^[ \t]*[#;].*/, "", l) }
			l ~ /^[ \t]*[A-Za-z0-9_.-]*(PASS|PASSWORD|SECRET|TOKEN|KEY)[A-Za-z0-9_.-]*[ \t]*[=:][ \t]*[^ \t#]/ {
				printf "%s:%d: a key named like a secret holds a value on its line\n", FILENAME, FNR; n++ }
			END { exit n > 0 ? 1 : 0 }' "$f" 2>&1)
	fi
	if [ -n "$out" ]; then
		echo "$out" >&2
		faults=$((faults + $(echo "$out" | wc -l)))
	fi
done

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