document.bash
## document.bash: parsing a testmark document, and reading its hunks.
## Sourced by testmark.bash; the functions here assume they run inside `_testmark_call`.
# Storage: private, and declared `-g` so sourcing from inside a function works; `+=()` makes them set-u safe, and keeps contents if sourced twice.
# Each parse appends its lines to _testmark_lines, and gets a number from _testmark_parses.
# Per-hunk keys are "PARSE:NAME", so a re-parse makes the old entries unreachable, and nothing is ever unset.
# Names are hostile input as keys: touch keyed elements only as `a[$k]=v`, `"${a[$k]}"`, `"${a[$k]-}"`, and `${a[$k]+set}`.
# In particular never in arithmetic, and never with `unset`: those can run `$(...)` from inside a key.
declare -ga _testmark_lines # every document's lines, each keeping its "\n" (a file's last line may lack one), CRLF made LF
_testmark_lines+=()
declare -g _testmark_parses=${_testmark_parses-0}
declare -gA _testmark_doc _testmark_pid _testmark_names # by handle: parse number (empty while unusable), $BASHPID at parse (state changes refuse other pids), names each ending in "\n"
_testmark_doc+=() _testmark_pid+=() _testmark_names+=()
declare -gA _testmark_start _testmark_end _testmark_info # by "PARSE:NAME": body line range [start, end) in _testmark_lines, and info string
_testmark_start+=() _testmark_end+=() _testmark_info+=()
_testmark_err() {
>&2 printf 'testmark: %s\n' "$1"
}
_testmark_parse() {
(($# == 2)) || {
_testmark_err "usage: testmark_parse HANDLE FILE"
return 1
}
local h=$1 file=$2
[[ $h =~ ^[A-Za-z0-9_]+$ ]] || {
_testmark_err "invalid handle \"$h\": must match [A-Za-z0-9_]+"
return 1
}
_testmark_doc[$h]=
# A directory would read as zero lines, so it's rejected here.
[[ -f $file && -r $file ]] || {
_testmark_err "$file: not a readable file"
return 1
}
# Lines are never unset, so the element count is also the next free index.
local base=${#_testmark_lines[@]} end
mapfile -O "$base" _testmark_lines <"$file" || {
_testmark_err "$file: read failed"
return 1
}
end=${#_testmark_lines[@]}
local doc=$((_testmark_parses += 1))
# State: `open` while inside a code block; `k` from an annotation until its hunk closes; `at` is the line number of the annotation or opener.
local i n line name info names='' open='' k='' at=''
for ((i = base; i < end; i++)); do
line=${_testmark_lines[i]}
n=$((i - base + 1))
# mapfile ends a line at a NUL, dropping its "\n". So a line without one is the last line, or has a NUL; read -d '' tells which.
if [[ $line != *$'\n' ]] && IFS= read -r -d '' _ <"$file"; then
_testmark_err "$file:$n: contains a NUL byte"
return 1
fi
if [[ $line == *$'\r\n' ]]; then
line=${line%$'\r\n'}
_testmark_lines[i]=$line$'\n'
else
line=${line%$'\n'}
fi
if [[ -n $open ]]; then
if [[ $line == '```'* ]]; then
[[ -z $k ]] || _testmark_end[$k]=$i
open='' k=''
fi
elif [[ -n $k ]]; then
[[ $line == '```'* ]] || {
_testmark_err "$file:$at: annotation not followed by a code block"
return 1
}
info=${line#'```'}
# Trim: strip the leading run of whitespace, then the trailing one.
info=${info#"${info%%[![:space:]]*}"}
info=${info%"${info##*[![:space:]]}"}
_testmark_info[$k]=$info
_testmark_start[$k]=$((i + 1))
open=1 at=$n
elif [[ $line == '```'* ]]; then
open=1 at=$n
elif [[ $line == '[testmark]:#'* ]]; then
name=${line#'[testmark]:# ('}
name=${name%')'}
if [[ $line != '[testmark]:# ('*')' || ! $name =~ ^[[:space:]]*([^[:space:]]+) ]]; then
_testmark_err "$file:$n: malformed annotation; want \"[testmark]:# (name)\""
return 1
fi
name=${BASH_REMATCH[1]}
k=$doc:$name
[[ -z ${_testmark_start[$k]+set} ]] || {
_testmark_err "$file:$n: duplicate hunk name \"$name\""
return 1
}
names+=$name$'\n'
at=$n
fi
done
if [[ -n $open ]]; then
_testmark_err "$file:$at: code block not closed"
return 1
fi
[[ -z $k ]] || {
_testmark_err "$file:$at: annotation not followed by a code block"
return 1
}
_testmark_names[$h]=$names
_testmark_pid[$h]=$BASHPID
_testmark_doc[$h]=$doc
}
## Sets the caller's local `k` to the storage key of hunk $2 in document $1, or fails with a message.
_testmark_hunk() {
local doc=${_testmark_doc[$1]-}
[[ -n $doc ]] || {
_testmark_err "unknown handle \"$1\""
return 1
}
k=$doc:$2
[[ -n ${_testmark_start[$k]+set} ]] || {
_testmark_err "$1: no hunk named \"$2\""
return 1
}
}
_testmark_names() {
(($# == 1)) || {
_testmark_err "usage: testmark_names HANDLE"
return 1
}
[[ -n ${_testmark_doc[$1]-} ]] || {
_testmark_err "unknown handle \"$1\""
return 1
}
printf '%s' "${_testmark_names[$1]}"
}
_testmark_body() {
(($# == 2)) || {
_testmark_err "usage: testmark_body HANDLE NAME"
return 1
}
local k i start end body=
_testmark_hunk "$1" "$2" || return 1
start=${_testmark_start[$k]} end=${_testmark_end[$k]}
for ((i = start; i < end; i++)); do body+=${_testmark_lines[i]}; done
printf '%s' "$body"
}
_testmark_info() {
(($# == 2)) || {
_testmark_err "usage: testmark_info HANDLE NAME"
return 1
}
local k
_testmark_hunk "$1" "$2" || return 1
printf '%s\n' "${_testmark_info[$k]}"
}
lib/testmark/document.bash · 5.1 KiB · 4469ad6c39