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 "<<EOF" line, not to the "EOF"
line which closes it.
In addition to modern subshell formatting shown above, old-style
formatting is also recognized:
statement1 &&
(statement2 &&
statement3) &&
statement4
Heuristics are employed to properly identify the extent of a subshell
formatted in the old-style since a number of legitimate constructs may
superficially appear to close the subshell even though they don't. For
instance, the ")" in "x=$(command) &&" and "case $x in *)" is specially
recognized to avoid being falsely considered the end of a subshell.
Due to the complexities of line-by-line detection (and limitations of
the tool, 'sed'), only subshells one level deep are handled, as well as
one-liner subshells one level below that. Subshells deeper than that or
multi-line subshells at level two are passed through as-is, thus
&&-chains in their bodies are not checked.
Signed-off-by: Eric Sunshine <[email protected]>
---
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 "<x" or "|x" or "&" -- strip "(" and ")"
+/^[ ]*(..*)[ ]*[0-9]*[<>|&]/ {
+ 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 "<x" or "|x")
+ /^[ ]*(.*)[ ]*[0-9]*[<>|]/bonelineredir
+ # nested multi-line "(...\n...)"
+ /^[ ]*(/bnest
+ # closing ") &&" on own line
+ /^[ ]*)[ ]*&&[ ]*$/bcloseamp
+ # closing ")" on own line
+ /^[ ]*)[ ]*$/bclose
+ # closing ") >x" (or "2>x" or "<x" or "|x") on own line
+ /^[ ]*)[ ]*[0-9]*[<>|]/bcloseredir
+ # "$((...))" -- not closing ")"
+ /\$(([^)][^)]*))[^)]*$/bpassthru
+ # "$(...)" -- not closing ")"
+ /\$([^)][^)]*)[^)]*$/bpassthru
+ # "=(...)" -- Bash array assignment; not closing ")"
+ /=(/bpassthru
+ # closing "...) &&"
+ /)[ ]*&&[ ]*$/bcloseampx
+ # closing "...)"
+ /)[ ]*$/bclosex
+ # closing "...) >x" (or "2>x" or "<x" or "|x")
+ /)[ ]*[<>|]/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 "<<EOF" opening line, so just
+ # discard the here-doc altogether (but keep the command to which it
+ # was attached)
+ :heredoc
+ s/<<[ ]*[-\\]*EOF//
+ x
+ p
+ :hereslurpsub
+ N
+ s/.*\n//
+ /^[ ]*EOF[ ]*$/bdiscard
+ bhereslurpsub
+
+ # found "case ... in" -- pass-thru untouched to avoid "...)" arms
+ # being misidentified as subshell closing ")"
+ :case
+ x
+ p
+ x
+ :caseslurp
+ n
+ /^[ ]*esac/besac
+ bcaseslurp
+ :esac
+ x
+ bdiscard
+
+ # found one-liner "(...) &&" or "(...)" -- strip "(" and ")"
+ :oneline
+ s/(//
+ s/)[ ]*\(&&\)*[ ]*$/ \1/
+ bpassthru
+
+ # found one-liner "(...) >x" (or "2>x" or "<x" or "|x") -- strip
+ # "(" and ")"
+ :onelineredir
+ s/(//
+ s/)[ ]*\([0-9]*[<>|]\)/\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 "|x" or "|") -- replace ")" with
+ # ":" to keep ")|\n" syntactically valid, and add "&&" to final
+ # subshell line
+ :closeredir
+ x
+ s/$/ \&\&/
+ p
+ x
+ s/)/:/
+ b
+
+ # found "...) &&" -- drop ")" but keep "..."
+ :closeampx
+ x
+ p
+ x
+ s/)[ ]*&&[ ]*$/ \&\&/
+ b
+
+ # found "...)" -- drop ")" but keep "..."
+ :closex
+ x
+ p
+ x
+ s/)[ ]*$//
+ b
+
+ # found "...) >x" (or "2>x" or "<x" or "|x") -- drop ")" but keep "..."
+ :closeredirx
+ x
+ p
+ x
+ s/)[ ]*\([<>|]\)/ \&\& : \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