From: Eric Sunshine Date: Tue, 26 Jun 2018 07:30:01 GMT Subject: [PATCH 29/29] t/test-lib: teach --chain-lint to detect broken &&-chains in subshells Message-ID: <20180626073001.6555-30-sunshine@sunshineco.com> In-Reply-To: <20180626073001.6555-1-sunshine@sunshineco.com> The --chain-lint option detects broken &&-chains by forcing the test to exit early (as the very first step) with a sentinel value. If that sentinel is the test's overall exit code, then the &&-chain is intact; if not, then the chain is broken. Unfortunately, this detection does not extend to &&-chains within subshells even when the subshell itself is properly linked into the outer &&-chain. Address this shortcoming by eliminating the subshell during the "linting" phase and incorporating its body directly into the surrounding &&-chain. To keep this transformation cheap, no attempt is made at properly parsing shell code. Instead, the manipulations are purely textual. For example: statement1 && ( statement2 && statement3 ) && statement4 is transformed to: statement1 && statement2 && statement3 && statement4 Notice how "statement3" gains the "&&" which dwelt originally on the closing ") &&" line. Since this manipulation is purely textual (in fact, line-by-line), special care is taken to ensure that the "&&" is moved to the final _statement_ before the closing ")", not necessarily the final _line_ of text within the subshell. For example, with a here-doc, the "&&" needs to be added to the opening "< --- t/test-lib.sh | 245 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 244 insertions(+), 1 deletion(-) diff --git a/t/test-lib.sh b/t/test-lib.sh index 28315706be..ade5066fff 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -664,6 +664,248 @@ test_eval_ () { return $test_eval_ret_ } +test_subshell_sed_=' +# incomplete line -- slurp up next line +/[^\\]\\$/ { + N + s/\\\n// +} + +# here-doc -- swallow it to avoid false hits within its body (but keep the +# command to which it was attached) +/<<[ ]*[-\\]*EOF[ ]*&&[ ]*$/ { + s/<<[ ]*[-\\]*EOF// + h + :hereslurp + N + s/.*\n// + /^[ ]*EOF[ ]*$/!bhereslurp + x + } + +# one-liner "(... || git ...)" or "(... || test ...)" -- short-hand for +# "if ... then : else ...", so leave untouched; contrast with "(... || true)" +# which ought to be replaced with "test_might_fail ..." to avoid breaking +# &&-chain +/^[ ]*(..*||[ ]*git[ ]..*)[ ]*&&[ ]*$/b +/^[ ]*(..*||[ ]*git[ ]..*)[ ]*$/b +/^[ ]*(..*||[ ]*test..*)[ ]*&&[ ]*$/b +/^[ ]*(..*||[ ]*test..*)[ ]*$/b + +# one-liner "(... &) &&" backgrounder -- needs to remain in subshell to avoid +# becoming syntactically invalid "... & &&" +/^[ ]*(..*&[ ]*)[ ]*&&[ ]*$/b + +# one-liner "(...) &&" -- strip "(" and ")" +/^[ ]*(..*)[ ]*&&[ ]*$/ { + s/(// + s/)[ ]*&&[ ]*$/ \&\&/ + b +} + +# same as above but without trailing "&&" +/^[ ]*(..*)[ ]*$/ { + s/(// + s/)[ ]*$// + b +} + +# one-liner "(...) >x" (or "2>x" or "|&]/ { + s/(// + s/)[ ]*\([0-9]*[<>|&]\)/\1/ + b +} + +# multi-line "(..." -- strip "(" and pass-thru enclosed lines until ")" +/^[ ]*(/ { + # strip "(" and stash for later printing + s/(// + h + + :discard + N + s/.*\n// + + # loop: slurp enclosed lines + :slurp + # end-of-file + $beof + # incomplete line + /[^\\]\\$/bincomplete + # here-doc + /<<[ ]*[-\\]*EOF/bheredoc + # comment or empty line -- discard since closing ") &&" will need to + # add "&&" to final non-comment, non-empty subshell line + /^[ ]*#/bdiscard + /^[ ]*$/bdiscard + # one-liner "case ... esac" + /^[ ]*case[ ]*..*esac/bpassthru + # multi-line "case ... esac" + /^[ ]*case[ ]..*[ ]in/bcase + # nested one-liner "(...) &&" + /^[ ]*(.*)[ ]*&&[ ]*$/boneline + # nested one-liner "(...)" + /^[ ]*(.*)[ ]*$/boneline + # nested one-liner "(...) >x" (or "2>x" or "|]/bonelineredir + # nested multi-line "(...\n...)" + /^[ ]*(/bnest + # closing ") &&" on own line + /^[ ]*)[ ]*&&[ ]*$/bcloseamp + # closing ")" on own line + /^[ ]*)[ ]*$/bclose + # closing ") >x" (or "2>x" or "|]/bcloseredir + # "$((...))" -- not closing ")" + /\$(([^)][^)]*))[^)]*$/bpassthru + # "$(...)" -- not closing ")" + /\$([^)][^)]*)[^)]*$/bpassthru + # "=(...)" -- Bash array assignment; not closing ")" + /=(/bpassthru + # closing "...) &&" + /)[ ]*&&[ ]*$/bcloseampx + # closing "...)" + /)[ ]*$/bclosex + # closing "...) >x" (or "2>x" or "|]/bcloseredirx + :passthru + # retrieve and print previous line + x + n + bslurp + + # end-of-file -- must be closing "...)" line or empty line; if empty, + # strip ")" from previous line, else strip ")" from this line + :eof + /^[ ]*$/bempty + x + p + :empty + x + /)[ ]*[<>|]/s/[<>|]..*$// + s/)[ ]*$// + b + + # found "...\" -- slurp up next line + :incomplete + N + s/\\\n// + bslurp + + # found here-doc inside subshell: when a subshell ends, we append + # "&&" to the final subshell line to chain it with lines outside the + # subshell, however, we cannot append "&&" to the ending EOF of a + # here-doc since "&&" belongs on the "<x" (or "2>x" or "|]\)/\1/ + bpassthru + + # found nested multi-line "(...\n...)" -- pass-thru untouched + :nest + x + p + x + :nslurp + n + # closing ")" on own line -- stop nested slurp + /^[ ]*)/bnclose + # "$((...))" -- not closing ")" + /\$(([^)][^)]*))[^)]*$/bnslurp + # "$(...)" -- not closing ")" + /\$([^)][^)]*)[^)]*$/bnslurp + # closing "...)" -- stop nested slurp + /)/bnclose + bnslurp + :nclose + # stash ")" (or ") &&", etc.) line for later printing and drop + # leftover gunk from hold area + x + bdiscard + + # found ") &&" -- drop ") &&" and add "&&" to final subshell line + :closeamp + x + s/$/ \&\&/ + b + + # found ")" -- drop it and print final subshell line + :close + x + b + + # found ") >x" (or "2>x" or "x" (or "2>x" or "|]\)/ \&\& : \1/ + b +} +' + test_run_ () { test_cleanup=: expecting_failure=$2 @@ -675,7 +917,8 @@ test_run_ () { trace= # 117 is magic because it is unlikely to match the exit # code of other programs - if test "OK-117" != "$(test_eval_ "(exit 117) && $1${LF}${LF}echo OK-\$?" 3>&1)" + test_linter=$(printf '%s\n' "$1" | sed -e "$test_subshell_sed_") + if test "OK-117" != "$(test_eval_ "(exit 117) && ${test_linter}${LF}${LF}echo OK-\$?" 3>&1)" then error "bug in the test script: broken &&-chain or run-away HERE-DOC: $1" fi -- 2.18.0.419.gfe4b301394