Target coverage: a klammer states the targets it serves
kdesc gains --coverage, which reports for every klammer the set of targets it
can render to, and — the point of it — which klammers' coverage cannot be
derived and must therefore be declared. Three rules: coverage is DERIVED
where the definitions determine it (a general body of klammer calls covers
the intersection of what those klammers cover, by a greatest fixpoint after
loading), DECLARED where the engine cannot interpret what decides it (an
@eval body, whose targets are undecidable), and UNKNOWN where nothing is
written — which never means "deliberately unavailable".
Two new spellings in a definition's name. A comma-separated target list,
"@@table.html,tex :: ...", gives one body several targets; it is surface
syntax, expanded at registration, and each member goes through the
redefinition rules on its own. And "@@date.* :: ..." writes the general
target out, asserting that the klammer works for EVERY target including ones
not yet defined — a stronger claim than a list of the targets defined today,
and the one target declaration that could be mechanically falsified.
The Standard Klammer Set was swept accordingly: it now has no general
definitions at all, every klammer names the targets it serves, six use ".*",
and tex and pdf are at zero undecided.
kdesc's flags are reorganised on two rules: a flag reached for often gets a
single letter (-k klammers, -t targets, -c characters, -i input), a more
specialised topic a multi-letter name (--argtypes, --katoms, --rewrite,
--optionsets, --coverage, --klammerset, --font); and -v says how much to show
about PROCESSING, never what the RESULT contains — so the katom regex column
is "--katoms full" and the coverage detail "--coverage all". NOTE: "-k" now
lists klammers (optionally filtered by a name/description search); the katom
table moved to "--katoms".
Fixes carried along: an option written with no value crashed the command with
SIGSEGV instead of reporting the mistake; two required positional arguments
never parsed; kdesc and kdiag printed an error and exited 0; and definition
diagnostics counted registrations rather than what was written, so one line
could be reported as two definitions and then printed twice.
Four new test suites: target_list, coverage, command_option, kdesc.
(from dev 46f54080bd9a)
2026-08-12 17:20:23 +02:00
|
|
|
#!/bin/bash
|
|
|
|
|
#
|
|
|
|
|
# command_option_test.sh — Command-line option parsing (mac/argv.cpp).
|
|
|
|
|
#
|
|
|
|
|
# The commands' own argument handling, which no suite covered: argv_test.cpp
|
|
|
|
|
# exercises Argv but asserts nothing, so two defects lived there unnoticed.
|
|
|
|
|
#
|
|
|
|
|
# * An option declared with opt() takes a value. Written LAST with nothing
|
|
|
|
|
# after it -- "kdesc -v", "ktext doc.kt -t" -- parse_optional() read one
|
|
|
|
|
# past the end of the word vector and the command died with SIGSEGV,
|
|
|
|
|
# naming nothing. Two bounds checks sat commented out at that spot; they
|
|
|
|
|
# would have returned a half-parsed option instead of reporting the
|
|
|
|
|
# mistake. It is now a located argument error.
|
|
|
|
|
# * kdesc and kdiag printed an error and then exited 0, so a script could
|
|
|
|
|
# not tell a failed run from a successful one. Both return 1 now, as
|
|
|
|
|
# ktext already did.
|
|
|
|
|
#
|
|
|
|
|
# Also here: two required positional arguments. parse_positional() consumes
|
|
|
|
|
# one per required name, but regex_split_prefix() requires its match at
|
|
|
|
|
# position 0 and returned the remainder with the separating space intact, so
|
|
|
|
|
# the second was always "not found". Latent, because no shipped command
|
|
|
|
|
# declares two -- tst/argv_test.cpp is the only program that does.
|
|
|
|
|
#
|
|
|
|
|
# Usage: ./command_option_test.sh (needs KLAMMERTEXT_HOME set; commands on PATH)
|
|
|
|
|
# Exit code: 0 if all tests pass, 1 otherwise.
|
|
|
|
|
|
|
|
|
|
PASS=0
|
|
|
|
|
FAIL=0
|
|
|
|
|
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
|
|
|
|
|
TSTDIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
|
|
|
|
|
|
red=$'\033[31m'
|
|
|
|
|
green=$'\033[32m'
|
|
|
|
|
bold=$'\033[1m'
|
|
|
|
|
reset=$'\033[0m'
|
|
|
|
|
|
|
|
|
|
# A signal death is not an error exit: 128+n, and the point of these tests is
|
|
|
|
|
# the difference. Report it distinctly so a crash can never read as a pass.
|
|
|
|
|
died_by_signal() { [ "$1" -gt 128 ]; }
|
|
|
|
|
|
|
|
|
|
# error NAME PATTERN CMD... — nonzero exit, no signal, PATTERN in the output.
|
|
|
|
|
error() {
|
|
|
|
|
local name="$1" pattern="$2"; shift 2
|
|
|
|
|
local out status
|
|
|
|
|
out=$("$@" 2>&1); status=$?
|
|
|
|
|
if died_by_signal $status; then
|
|
|
|
|
echo "${red}FAIL${reset} $name — died by signal $((status-128))"; FAIL=$((FAIL+1)); return
|
|
|
|
|
fi
|
|
|
|
|
if [ $status -eq 0 ]; then
|
|
|
|
|
echo "${red}FAIL${reset} $name — reported an error but exited 0"; FAIL=$((FAIL+1)); return
|
|
|
|
|
fi
|
|
|
|
|
if printf '%s' "$out" | grep -qF -- "$pattern"; then
|
|
|
|
|
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
|
|
|
|
else
|
|
|
|
|
echo "${red}FAIL${reset} $name — expected [$pattern]"
|
|
|
|
|
echo " got: $(printf '%s' "$out" | head -2)"; FAIL=$((FAIL+1))
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ok NAME CMD... — exits 0 and does not die by signal.
|
|
|
|
|
ok() {
|
|
|
|
|
local name="$1"; shift
|
|
|
|
|
local status
|
|
|
|
|
"$@" >/dev/null 2>&1; status=$?
|
|
|
|
|
if died_by_signal $status; then
|
|
|
|
|
echo "${red}FAIL${reset} $name — died by signal $((status-128))"; FAIL=$((FAIL+1))
|
|
|
|
|
elif [ $status -ne 0 ]; then
|
|
|
|
|
echo "${red}FAIL${reset} $name — exit $status"; FAIL=$((FAIL+1))
|
|
|
|
|
else
|
|
|
|
|
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
echo "${bold}Command option tests${reset}"
|
|
|
|
|
echo "===================="
|
|
|
|
|
echo
|
|
|
|
|
|
|
|
|
|
echo "-- an option written last, with no value --"
|
|
|
|
|
error " 1. kdesc -v" "needs a value" kdesc -v
|
|
|
|
|
error " 2. kdesc with a flag first" "needs a value" kdesc --katoms -v
|
|
|
|
|
error " 3. kdiag -v" "needs a value" kdiag -v
|
|
|
|
|
error " 4. ktext -t" "needs a value" ktext -s '@i-x' -t
|
|
|
|
|
error " 5. the message names the option" "-v <level>" kdesc -v
|
|
|
|
|
error " 6. ... and how to get the usage" "for the list of arguments" kdesc -v
|
|
|
|
|
|
|
|
|
|
echo
|
|
|
|
|
echo "-- the same options WITH a value still work --"
|
|
|
|
|
# NOT "kdesc -v 1": show_usage() treats exactly "<command> -v <n>" as a
|
|
|
|
|
# request for the usage text, which exits 1 by design. Give it another flag.
|
|
|
|
|
ok " 7. kdesc -c -v 1" kdesc -c -v 1
|
|
|
|
|
ok " 8. kdesc --katoms -v 1" kdesc --katoms -v 1
|
|
|
|
|
ok " 9. kdiag -v 1 '@i-x'" kdiag -v 1 '@i-x'
|
|
|
|
|
ok "10. ktext -t html" ktext -s '@i-x' -t html -d
|
|
|
|
|
|
|
|
|
|
echo
|
|
|
|
|
echo "-- an error exit is nonzero, not a printed message and exit 0 --"
|
|
|
|
|
error "11. kdesc reports failure" "needs a value" kdesc -v
|
|
|
|
|
error "12. kdiag reports failure" "needs a value" kdiag -v
|
|
|
|
|
|
|
|
|
|
echo
|
|
|
|
|
echo "-- two required positional arguments --"
|
|
|
|
|
# argv_test is the only program declaring two; it prints what it parsed.
|
|
|
|
|
if [ -x "$TSTDIR/argv_test" ]; then
|
|
|
|
|
out=$("$TSTDIR/argv_test" first second --bool 2>&1 | sed 's/\x1b\[[0-9;]*m//g')
|
|
|
|
|
for expect in "<input> : first" "<input2> : second"; do
|
|
|
|
|
if printf '%s' "$out" | grep -qF "$expect"; then
|
|
|
|
|
echo "${green}PASS${reset} 13/14. positional [$expect]"; PASS=$((PASS+1))
|
|
|
|
|
else
|
|
|
|
|
echo "${red}FAIL${reset} 13/14. positional [$expect] not parsed"; FAIL=$((FAIL+1))
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
else
|
|
|
|
|
echo "SKIP 13/14. two positionals (argv_test not built; run make -C tst)"
|
|
|
|
|
fi
|
|
|
|
|
|
An output policy for the three commands, and @cond as a true special form
A snapshot of the development tree. The substantial changes since the last one:
COMMAND OUTPUT POLICY. The three commands display text in exactly three cases,
and each owns a stream: LOGGING under "-v" greater than 0 and an ERROR before
termination go to STDERR; OUTPUT THE USER ASKED FOR goes to STDOUT. For ktext
that output is a document, so "ktext doc.kt -d | ..." is now safe -- logging
used to share the stream and land inside the document. A bare command prints
its usage and succeeds rather than failing. Colour is emitted only to a
terminal, per stream, and NO_COLOR is honoured.
"-v 1" reports every decision whose outcome you could not have read off your own
input: the klammerset that was loaded and from which file, a font's directory, a
":files" name's file, how "-o" was expanded. Higher levels are the trace.
The commands no longer warn and continue: an anomaly is an error, described with
its location. Two exceptions remain, each for a stated reason -- a condition
that is expected and temporary by design, and a judgment that is a heuristic
rather than exact.
@cond IS NOW A TRUE SPECIAL FORM, resolved at APPLICATION time rather than when
the file is read. Two consequences for a writer:
* a state variable reaches the predicate. "@@@state Flag :value true @@@
@cond *Flag* | T | F @" renders "T"; it used to see the literal "*Flag*" and
silently take the false branch. The document now behaves like a klammer
body, whose arguments are bound before its conditionals are decided.
* nothing in a discarded branch happens -- it is not read, not evaluated, not
expanded. An @eval in the branch not taken used to run anyway.
Its predicate relation is total and strict: true, True, 1; false, False, 0, and
empty; anything else is an error at the @cond rather than silently false.
@eval REACHING OUTSIDE. ":shell" and ":haskell" now keep the command's standard
error out of the document (it appears under "-v 1") and treat a nonzero exit as
an error naming what the command reported. A command that exits nonzero on
purpose -- "grep" finding no match -- says so with "|| true".
KLAMMER SETS. Several combine: "--klammersets a b c" loads all three in the
order given, sharing one namespace, with the definition modes deciding
collisions. "none" means none and may not be combined with other symbols. A
klammerset with symbol X is declared in a file X/X.k, which is what lets two
sets require the same third set without loading it twice.
TESTS. Four new suites: the kdiag command's interface, the @eval primitive's
contract with the outside world, and verbosity at both tiers. Three suites
that could not run on macOS at all now do.
Assembled from dev commit 6c8ee6c22fca.
2026-08-16 01:37:59 +02:00
|
|
|
echo
|
|
|
|
|
echo "-- the output policy: three categories, two streams --"
|
|
|
|
|
# The three commands display text in exactly three cases:
|
|
|
|
|
# 1. logging under "-v" > 0 -> STDERR
|
|
|
|
|
# 2. an error before termination -> STDERR
|
|
|
|
|
# 3. output the user asked for -> STDOUT
|
|
|
|
|
# For ktext, category 3 is a DOCUMENT that may be piped, so nothing else may
|
|
|
|
|
# share the stream. It did: K::log and msg() both wrote to stdout, so
|
|
|
|
|
# "ktext -d -v 1 > doc.txt" put the whole argument dump inside the document.
|
|
|
|
|
OUTF=$(mktemp /tmp/kout.XXXXXX); ERRF=$(mktemp /tmp/kerr.XXXXXX)
|
|
|
|
|
trap 'rm -f "$OUTF" "$ERRF"' EXIT
|
|
|
|
|
|
|
|
|
|
ktext --klammersets none -s 'hello' -d -v 1 > "$OUTF" 2> "$ERRF"
|
|
|
|
|
if [ "$(cat "$OUTF")" = "hello" ]; then
|
|
|
|
|
echo "${green}PASS${reset} 15. stdout under -d -v 1 is the document, nothing else"
|
|
|
|
|
PASS=$((PASS+1))
|
|
|
|
|
else
|
|
|
|
|
echo "${red}FAIL${reset} 15. stdout carried more than the document"
|
|
|
|
|
echo " got: $(head -3 "$OUTF")"; FAIL=$((FAIL+1))
|
|
|
|
|
fi
|
|
|
|
|
if [ -s "$ERRF" ]; then
|
|
|
|
|
echo "${green}PASS${reset} 16. ... and the -v 1 logging went to stderr"; PASS=$((PASS+1))
|
|
|
|
|
else
|
|
|
|
|
echo "${red}FAIL${reset} 16. -v 1 produced no stderr"; FAIL=$((FAIL+1))
|
|
|
|
|
fi
|
|
|
|
|
# -v 0 adds nothing anywhere: silence is the default, on both streams.
|
|
|
|
|
ktext --klammersets none -s 'hello' -d > "$OUTF" 2> "$ERRF"
|
|
|
|
|
if [ "$(cat "$OUTF")" = "hello" ] && [ ! -s "$ERRF" ]; then
|
|
|
|
|
echo "${green}PASS${reset} 17. -v 0 is silent on both streams"; PASS=$((PASS+1))
|
|
|
|
|
else
|
|
|
|
|
echo "${red}FAIL${reset} 17. -v 0 was not silent (stderr: $(head -2 "$ERRF"))"
|
|
|
|
|
FAIL=$((FAIL+1))
|
|
|
|
|
fi
|
|
|
|
|
# An error is category 2: stderr, and nothing on stdout to confuse a pipe.
|
|
|
|
|
ktext --klammersets none -s '@nosuch x @' -d > "$OUTF" 2> "$ERRF"
|
|
|
|
|
if [ ! -s "$OUTF" ] && [ -s "$ERRF" ]; then
|
|
|
|
|
echo "${green}PASS${reset} 18. an error goes to stderr, leaving stdout empty"; PASS=$((PASS+1))
|
|
|
|
|
else
|
|
|
|
|
echo "${red}FAIL${reset} 18. error stream discipline (stdout: $(head -2 "$OUTF"))"
|
|
|
|
|
FAIL=$((FAIL+1))
|
|
|
|
|
fi
|
|
|
|
|
# Colour is emitted only to a terminal, so a redirected stream never carries
|
|
|
|
|
# escape sequences -- into a document, a pipe, or a captured log.
|
|
|
|
|
for pair in "ktext:--klammersets none -s hello -d" "kdesc:-k table" "kdiag:@i-x"; do
|
|
|
|
|
cmd=${pair%%:*}; rest=${pair#*:}
|
|
|
|
|
# shellcheck disable=SC2086
|
|
|
|
|
$cmd $rest > "$OUTF" 2> "$ERRF"
|
|
|
|
|
if ! grep -q $'\033' "$OUTF" && ! grep -q $'\033' "$ERRF"; then
|
|
|
|
|
echo "${green}PASS${reset} 19. no escape sequences from $cmd when redirected"
|
|
|
|
|
PASS=$((PASS+1))
|
|
|
|
|
else
|
|
|
|
|
echo "${red}FAIL${reset} 19. $cmd emitted colour to a non-terminal"; FAIL=$((FAIL+1))
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
echo
|
|
|
|
|
echo "-- a bare command is a request, not a failure --"
|
|
|
|
|
# Usage is the result being asked for: stdout, exit 0. It exited 1, so
|
|
|
|
|
# "kdesc && echo ok" reported failure for a successful help request.
|
|
|
|
|
for cmd in ktext kdesc kdiag; do
|
|
|
|
|
$cmd > "$OUTF" 2> "$ERRF"; status=$?
|
|
|
|
|
if [ $status -eq 0 ] && [ -s "$OUTF" ] && ! [ -s "$ERRF" ]; then
|
|
|
|
|
echo "${green}PASS${reset} 20. $cmd with no arguments: usage on stdout, exit 0"
|
|
|
|
|
PASS=$((PASS+1))
|
|
|
|
|
else
|
|
|
|
|
echo "${red}FAIL${reset} 20. $cmd bare: exit $status, stdout $(wc -l < "$OUTF") lines, stderr $(wc -l < "$ERRF") lines"
|
|
|
|
|
FAIL=$((FAIL+1))
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
Target coverage: a klammer states the targets it serves
kdesc gains --coverage, which reports for every klammer the set of targets it
can render to, and — the point of it — which klammers' coverage cannot be
derived and must therefore be declared. Three rules: coverage is DERIVED
where the definitions determine it (a general body of klammer calls covers
the intersection of what those klammers cover, by a greatest fixpoint after
loading), DECLARED where the engine cannot interpret what decides it (an
@eval body, whose targets are undecidable), and UNKNOWN where nothing is
written — which never means "deliberately unavailable".
Two new spellings in a definition's name. A comma-separated target list,
"@@table.html,tex :: ...", gives one body several targets; it is surface
syntax, expanded at registration, and each member goes through the
redefinition rules on its own. And "@@date.* :: ..." writes the general
target out, asserting that the klammer works for EVERY target including ones
not yet defined — a stronger claim than a list of the targets defined today,
and the one target declaration that could be mechanically falsified.
The Standard Klammer Set was swept accordingly: it now has no general
definitions at all, every klammer names the targets it serves, six use ".*",
and tex and pdf are at zero undecided.
kdesc's flags are reorganised on two rules: a flag reached for often gets a
single letter (-k klammers, -t targets, -c characters, -i input), a more
specialised topic a multi-letter name (--argtypes, --katoms, --rewrite,
--optionsets, --coverage, --klammerset, --font); and -v says how much to show
about PROCESSING, never what the RESULT contains — so the katom regex column
is "--katoms full" and the coverage detail "--coverage all". NOTE: "-k" now
lists klammers (optionally filtered by a name/description search); the katom
table moved to "--katoms".
Fixes carried along: an option written with no value crashed the command with
SIGSEGV instead of reporting the mistake; two required positional arguments
never parsed; kdesc and kdiag printed an error and exited 0; and definition
diagnostics counted registrations rather than what was written, so one line
could be reported as two definitions and then printed twice.
Four new test suites: target_list, coverage, command_option, kdesc.
(from dev 46f54080bd9a)
2026-08-12 17:20:23 +02:00
|
|
|
echo
|
|
|
|
|
echo "===================="
|
|
|
|
|
echo "Results: ${PASS} passed, ${FAIL} failed"
|
|
|
|
|
[ "$FAIL" -eq 0 ] || exit 1
|
|
|
|
|
exit 0
|