## testmark.bash: read and patch testmark documents (data hunks embedded in markdown) from bash. ## ## Source this file, by a path containing a slash; it sources the rest of the library from beside itself. ## Needs bash 4.4 or later; on older ones, sourcing fails. ## Every public function is a shim over a private `_testmark_*` implementation, run through `_testmark_call`. ## Data comes out on stdout as exact bytes, and goes in on stdin; errors go to stderr as "testmark: ...", with a nonzero status. ## Documents are addressed by a handle the caller picks at parse time; handles match `[A-Za-z0-9_]+`. # Checked first, in syntax bash 3.2 (macOS's /bin/bash) still runs, so an old bash gets one clear error. if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4))); then >&2 echo "testmark: needs bash 4.4 or later; this is $BASH_VERSION" return 1 fi # shellcheck source=lib/testmark/document.bash source "${BASH_SOURCE[0]%/*}/testmark/document.bash" # shellcheck source=lib/testmark/patch.bash source "${BASH_SOURCE[0]%/*}/testmark/patch.bash" ## Runs an implementation function in a known environment, whatever the caller's settings. ## ## - `LC_ALL=C`: string operations work on bytes, and `[[:space:]]` is exactly ASCII whitespace. ## - `set -u`: a library bug that reads an unset variable exits the script loudly. `local -` restores the caller's setting. ## - `nocasematch` off: pattern matches are exact. It's restored afterwards, since `local -` doesn't cover shopts. ## - The `||` list: inside it, the caller's `set -e` and ERR trap can't fire on the implementation's ordinary nonzero statuses. _testmark_call() { local - LC_ALL=C _testmark_st=0 _testmark_nocase= set -u case ":$BASHOPTS:" in *:nocasematch:*) _testmark_nocase=1 && shopt -u nocasematch ;; esac "$@" || _testmark_st=$? [[ -z $_testmark_nocase ]] || shopt -s nocasematch return "$_testmark_st" } ## Parses FILE and makes it available under HANDLE. ## Parsing again under the same handle replaces the earlier document; if that parse fails, the handle is left unusable. ## ## - @param $1 handle ## - @param $2 file path; also used in error messages. ## - @stderr on failure, one line: "testmark: FILE:LINE: problem" for parse errors. testmark_parse() { _testmark_call _testmark_parse "$@"; } ## - @param $1 handle ## - @stdout the document's hunk names, one per line, in document order. testmark_names() { _testmark_call _testmark_names "$@"; } ## - @param $1 handle ## - @param $2 hunk name ## - @stdout the hunk's body, exactly: its lines, each ending in a newline, or nothing for an empty hunk. testmark_body() { _testmark_call _testmark_body "$@"; } ## - @param $1 handle ## - @param $2 hunk name ## - @stdout the hunk's info string (the opening fence's text after the backticks, trimmed), then a newline. testmark_info() { _testmark_call _testmark_info "$@"; } # The functions below change a document's state, so they refuse to run in a subshell, where the change would be lost: # feed them input with `testmark_check doc x < <(cmd)`, not `cmd | testmark_check doc x`, and don't call them inside `$(...)`. ## Stages a new body for a hunk, for testmark_write to write. Staging the same hunk again replaces what was staged before. ## ## - @param $1 handle ## - @param $2 hunk name ## - @param $3 optional: a new info string; the opening fence line becomes "```" and it. Without it, that line is kept as it is. ## - @stdin the new body, exactly; a final newline is added if it lacks one. No line of it may start with "```". testmark_patch() { _testmark_call _testmark_patch "$@"; } ## Writes what's staged into the parsed file, in place, then parses the file again under the same handle. ## That drops everything staged and every record of what was checked. With nothing staged, it does nothing. ## ## - @param $1 handle testmark_write() { _testmark_call _testmark_write "$@"; }