## patch.bash: staging new hunk bodies, and writing them into the document's file. ## Sourced by testmark.bash; the functions here assume they run inside `_testmark_call`. # shellcheck disable=SC2034,SC2178 # shellcheck does not follow namerefs; see document.bash ## Fails with a message if this is a subshell of the shell that parsed document $1, which must exist. ## Whatever a subshell stages is lost when it exits, so a regen there would vanish without a word. _testmark_same_shell() { local -n pid=_testmark_pid_$1 [[ $pid == "$BASHPID" ]] || { _testmark_err "$1: called in a subshell (from a pipe, or inside \$(...)), where any change to the document is lost; instead of 'cmd | testmark_check HANDLE NAME', use 'testmark_check HANDLE NAME < <(cmd)'" return 1 } } ## Reads all of stdin into the caller's local `input`, normalized the way parsing normalizes a hunk's body: ## CRLF becomes LF, and a final newline is added if it's missing. ## So input reads as the body it would become, and a check compares like with like. ## ## - @param $1 what the input is for, as a prefix for the error message _testmark_input() { # Status 0 means read stopped at a NUL; see "NUL bytes vanish unless you check for them" in docs/hacking/bash-pitfalls.md. if IFS= read -r -d '' input; then _testmark_err "$1: input contains a NUL byte" return 1 fi [[ $input != *$'\r\n'* ]] || input=${input//$'\r\n'/$'\n'} [[ -z $input || $input == *$'\n' ]] || input+=$'\n' } ## Stages a new body for hunk $2 of document $1, which must both exist, replacing anything staged for it before. ## ## - @param $3 the body: empty, or ending in a newline ## - @param $4 optional: the new opener line; without it, the hunk's own opener is kept _testmark_stage() { local body=$3 opener=${4-} # Any line starting with ``` closes the code block, so such a body would end its hunk early and corrupt the rest of the doc. [[ $'\n'$body != *$'\n```'* ]] || { _testmark_err "$1: hunk \"$2\": body has a line starting with \`\`\`, which would end its code block" return 1 } local -n lines=_testmark_lines_$1 start=_testmark_start_$1 patch=_testmark_patch_$1 local i=${start[$2]} [[ -n $opener ]] || opener=${lines[i - 1]} patch[$2]=$opener$body } _testmark_patch() { (($# == 2 || $# == 3)) || { _testmark_err "usage: testmark_patch HANDLE NAME [INFO] 0)) || return 0 # The output is a copy of the lines, where each staged hunk's opener line holds its staged opener and body, and its old body lines are emptied. # Slicing out the lines between hunks instead would be quadratic; see "Quadratic operations" in docs/hacking/bash-pitfalls.md. local -n lines=_testmark_lines_$1 start=_testmark_start_$1 end=_testmark_end_$1 file=_testmark_file_$1 local -a newlines=("${lines[@]}") local name i last for name in "${!patch[@]}"; do i=${start[$name]} last=${end[$name]} newlines[i - 1]=${patch[$name]} for (( ; i < last; i++)); do newlines[i]=; done done # In place, so symlinks are followed and the mode is kept; >| so the caller's noclobber doesn't refuse it. printf '%s' "${newlines[@]}" 2>/dev/null >|"$file" || { _testmark_err "$file: not writable" return 1 } # Re-parsing drops what was staged and checked, along with the rest of the old state, and proves the output parses. _testmark_parse "$1" "$file" }