about summary refs log tree commit diff
path: root/t/chainlint
diff options
context:
space:
mode:
authorVincent Ambo <Vincent Ambo>2020-01-11T23·36+0000
committerVincent Ambo <Vincent Ambo>2020-01-11T23·36+0000
commit1b593e1ea4d2af0f6444d9a7788d5d99abd6fde5 (patch)
treee3accb9beed5c4c1b5a05c99db71ab2841f0ed04 /t/chainlint
Squashed 'third_party/git/' content from commit cb71568594
git-subtree-dir: third_party/git
git-subtree-split: cb715685942260375e1eb8153b0768a376e4ece7
Diffstat (limited to 't/chainlint')
-rw-r--r--t/chainlint/arithmetic-expansion.expect9
-rw-r--r--t/chainlint/arithmetic-expansion.test11
-rw-r--r--t/chainlint/bash-array.expect10
-rw-r--r--t/chainlint/bash-array.test12
-rw-r--r--t/chainlint/blank-line.expect4
-rw-r--r--t/chainlint/blank-line.test10
-rw-r--r--t/chainlint/block.expect12
-rw-r--r--t/chainlint/block.test15
-rw-r--r--t/chainlint/broken-chain.expect6
-rw-r--r--t/chainlint/broken-chain.test8
-rw-r--r--t/chainlint/case.expect19
-rw-r--r--t/chainlint/case.test23
-rw-r--r--t/chainlint/close-nested-and-parent-together.expect4
-rw-r--r--t/chainlint/close-nested-and-parent-together.test3
-rw-r--r--t/chainlint/close-subshell.expect25
-rw-r--r--t/chainlint/close-subshell.test27
-rw-r--r--t/chainlint/command-substitution.expect9
-rw-r--r--t/chainlint/command-substitution.test11
-rw-r--r--t/chainlint/comment.expect4
-rw-r--r--t/chainlint/comment.test11
-rw-r--r--t/chainlint/complex-if-in-cuddled-loop.expect10
-rw-r--r--t/chainlint/complex-if-in-cuddled-loop.test11
-rw-r--r--t/chainlint/cuddled-if-then-else.expect7
-rw-r--r--t/chainlint/cuddled-if-then-else.test7
-rw-r--r--t/chainlint/cuddled-loop.expect5
-rw-r--r--t/chainlint/cuddled-loop.test7
-rw-r--r--t/chainlint/cuddled.expect21
-rw-r--r--t/chainlint/cuddled.test23
-rw-r--r--t/chainlint/exit-loop.expect24
-rw-r--r--t/chainlint/exit-loop.test27
-rw-r--r--t/chainlint/exit-subshell.expect5
-rw-r--r--t/chainlint/exit-subshell.test6
-rw-r--r--t/chainlint/for-loop.expect11
-rw-r--r--t/chainlint/for-loop.test19
-rw-r--r--t/chainlint/here-doc-close-subshell.expect2
-rw-r--r--t/chainlint/here-doc-close-subshell.test5
-rw-r--r--t/chainlint/here-doc-multi-line-command-subst.expect5
-rw-r--r--t/chainlint/here-doc-multi-line-command-subst.test9
-rw-r--r--t/chainlint/here-doc-multi-line-string.expect4
-rw-r--r--t/chainlint/here-doc-multi-line-string.test8
-rw-r--r--t/chainlint/here-doc.expect9
-rw-r--r--t/chainlint/here-doc.test37
-rw-r--r--t/chainlint/if-in-loop.expect12
-rw-r--r--t/chainlint/if-in-loop.test15
-rw-r--r--t/chainlint/if-then-else.expect19
-rw-r--r--t/chainlint/if-then-else.test28
-rw-r--r--t/chainlint/incomplete-line.expect4
-rw-r--r--t/chainlint/incomplete-line.test12
-rw-r--r--t/chainlint/inline-comment.expect9
-rw-r--r--t/chainlint/inline-comment.test12
-rw-r--r--t/chainlint/loop-in-if.expect12
-rw-r--r--t/chainlint/loop-in-if.test15
-rw-r--r--t/chainlint/multi-line-nested-command-substitution.expect18
-rw-r--r--t/chainlint/multi-line-nested-command-substitution.test18
-rw-r--r--t/chainlint/multi-line-string.expect15
-rw-r--r--t/chainlint/multi-line-string.test27
-rw-r--r--t/chainlint/negated-one-liner.expect5
-rw-r--r--t/chainlint/negated-one-liner.test7
-rw-r--r--t/chainlint/nested-cuddled-subshell.expect19
-rw-r--r--t/chainlint/nested-cuddled-subshell.test31
-rw-r--r--t/chainlint/nested-here-doc.expect7
-rw-r--r--t/chainlint/nested-here-doc.test33
-rw-r--r--t/chainlint/nested-subshell-comment.expect11
-rw-r--r--t/chainlint/nested-subshell-comment.test13
-rw-r--r--t/chainlint/nested-subshell.expect12
-rw-r--r--t/chainlint/nested-subshell.test14
-rw-r--r--t/chainlint/one-liner.expect9
-rw-r--r--t/chainlint/one-liner.test12
-rw-r--r--t/chainlint/p4-filespec.expect4
-rw-r--r--t/chainlint/p4-filespec.test5
-rw-r--r--t/chainlint/pipe.expect8
-rw-r--r--t/chainlint/pipe.test12
-rw-r--r--t/chainlint/semicolon.expect20
-rw-r--r--t/chainlint/semicolon.test25
-rw-r--r--t/chainlint/subshell-here-doc.expect11
-rw-r--r--t/chainlint/subshell-here-doc.test39
-rw-r--r--t/chainlint/subshell-one-liner.expect14
-rw-r--r--t/chainlint/subshell-one-liner.test24
-rw-r--r--t/chainlint/t7900-subtree.expect10
-rw-r--r--t/chainlint/t7900-subtree.test22
-rw-r--r--t/chainlint/while-loop.expect11
-rw-r--r--t/chainlint/while-loop.test19
82 files changed, 1108 insertions, 0 deletions
diff --git a/t/chainlint/arithmetic-expansion.expect b/t/chainlint/arithmetic-expansion.expect
new file mode 100644
index 000000000000..09457d319661
--- /dev/null
+++ b/t/chainlint/arithmetic-expansion.expect
@@ -0,0 +1,9 @@
+(
+	foo &&
+	bar=$((42 + 1)) &&
+	baz
+>) &&
+(
+?!AMP?!	bar=$((42 + 1))
+	baz
+>)
diff --git a/t/chainlint/arithmetic-expansion.test b/t/chainlint/arithmetic-expansion.test
new file mode 100644
index 000000000000..16206960d8b0
--- /dev/null
+++ b/t/chainlint/arithmetic-expansion.test
@@ -0,0 +1,11 @@
+(
+	foo &&
+# LINT: closing ")" of $((...)) not misinterpreted as subshell-closing ")"
+	bar=$((42 + 1)) &&
+	baz
+) &&
+(
+# LINT: missing "&&" on $((...))
+	bar=$((42 + 1))
+	baz
+)
diff --git a/t/chainlint/bash-array.expect b/t/chainlint/bash-array.expect
new file mode 100644
index 000000000000..c4a830d1c1d6
--- /dev/null
+++ b/t/chainlint/bash-array.expect
@@ -0,0 +1,10 @@
+(
+	foo &&
+	bar=(gumbo stumbo wumbo) &&
+	baz
+>) &&
+(
+	foo &&
+	bar=${#bar[@]} &&
+	baz
+>)
diff --git a/t/chainlint/bash-array.test b/t/chainlint/bash-array.test
new file mode 100644
index 000000000000..92bbb777b82a
--- /dev/null
+++ b/t/chainlint/bash-array.test
@@ -0,0 +1,12 @@
+(
+	foo &&
+# LINT: ")" in Bash array assignment not misinterpreted as subshell-closing ")"
+	bar=(gumbo stumbo wumbo) &&
+	baz
+) &&
+(
+	foo &&
+# LINT: Bash array length operator not misinterpreted as comment
+	bar=${#bar[@]} &&
+	baz
+)
diff --git a/t/chainlint/blank-line.expect b/t/chainlint/blank-line.expect
new file mode 100644
index 000000000000..3be939ed388c
--- /dev/null
+++ b/t/chainlint/blank-line.expect
@@ -0,0 +1,4 @@
+(
+	nothing &&
+	something
+>)
diff --git a/t/chainlint/blank-line.test b/t/chainlint/blank-line.test
new file mode 100644
index 000000000000..f6dd14302b2b
--- /dev/null
+++ b/t/chainlint/blank-line.test
@@ -0,0 +1,10 @@
+(
+
+	nothing &&
+
+	something
+# LINT: swallow blank lines since final _statement_ before subshell end is
+# LINT: significant to "&&"-check, not final _line_ (which might be blank)
+
+
+)
diff --git a/t/chainlint/block.expect b/t/chainlint/block.expect
new file mode 100644
index 000000000000..fed7e89ae8ff
--- /dev/null
+++ b/t/chainlint/block.expect
@@ -0,0 +1,12 @@
+(
+	foo &&
+	{
+		echo a
+		echo b
+	} &&
+	bar &&
+	{
+		echo c
+?!AMP?!	}
+	baz
+>)
diff --git a/t/chainlint/block.test b/t/chainlint/block.test
new file mode 100644
index 000000000000..d859151af1d9
--- /dev/null
+++ b/t/chainlint/block.test
@@ -0,0 +1,15 @@
+(
+# LINT: missing "&&" in block not currently detected (for consistency with
+# LINT: --chain-lint at top level and to provide escape hatch if needed)
+	foo &&
+	{
+		echo a
+		echo b
+	} &&
+	bar &&
+# LINT: missing "&&" at closing "}"
+	{
+		echo c
+	}
+	baz
+)
diff --git a/t/chainlint/broken-chain.expect b/t/chainlint/broken-chain.expect
new file mode 100644
index 000000000000..55b0f42a534d
--- /dev/null
+++ b/t/chainlint/broken-chain.expect
@@ -0,0 +1,6 @@
+(
+	foo &&
+?!AMP?!	bar
+	baz &&
+	wop
+>)
diff --git a/t/chainlint/broken-chain.test b/t/chainlint/broken-chain.test
new file mode 100644
index 000000000000..3cc67b65d0c9
--- /dev/null
+++ b/t/chainlint/broken-chain.test
@@ -0,0 +1,8 @@
+(
+	foo &&
+# LINT: missing "&&" from 'bar'
+	bar
+	baz &&
+# LINT: final statement before closing ")" legitimately lacks "&&"
+	wop
+)
diff --git a/t/chainlint/case.expect b/t/chainlint/case.expect
new file mode 100644
index 000000000000..41f121fbbf9c
--- /dev/null
+++ b/t/chainlint/case.expect
@@ -0,0 +1,19 @@
+(
+	case "$x" in
+	x) foo ;;
+	*) bar ;;
+	esac &&
+	foobar
+>) &&
+(
+	case "$x" in
+	x) foo ;;
+	*) bar ;;
+?!AMP?!	esac
+	foobar
+>) &&
+(
+	case "$x" in 1) true;; esac &&
+?!AMP?!	case "$y" in 2) false;; esac
+	foobar
+>)
diff --git a/t/chainlint/case.test b/t/chainlint/case.test
new file mode 100644
index 000000000000..5ef6ff7db5ea
--- /dev/null
+++ b/t/chainlint/case.test
@@ -0,0 +1,23 @@
+(
+# LINT: "...)" arms in 'case' not misinterpreted as subshell-closing ")"
+	case "$x" in
+	x) foo ;;
+	*) bar ;;
+	esac &&
+	foobar
+) &&
+(
+# LINT: missing "&&" on 'esac'
+	case "$x" in
+	x) foo ;;
+	*) bar ;;
+	esac
+	foobar
+) &&
+(
+# LINT: "...)" arm in one-liner 'case' not misinterpreted as closing ")"
+	case "$x" in 1) true;; esac &&
+# LINT: same but missing "&&"
+	case "$y" in 2) false;; esac
+	foobar
+)
diff --git a/t/chainlint/close-nested-and-parent-together.expect b/t/chainlint/close-nested-and-parent-together.expect
new file mode 100644
index 000000000000..2a910f9d6660
--- /dev/null
+++ b/t/chainlint/close-nested-and-parent-together.expect
@@ -0,0 +1,4 @@
+(
+cd foo &&
+	(bar &&
+>>>		baz))
diff --git a/t/chainlint/close-nested-and-parent-together.test b/t/chainlint/close-nested-and-parent-together.test
new file mode 100644
index 000000000000..72d482f76dd2
--- /dev/null
+++ b/t/chainlint/close-nested-and-parent-together.test
@@ -0,0 +1,3 @@
+(cd foo &&
+	(bar &&
+		baz))
diff --git a/t/chainlint/close-subshell.expect b/t/chainlint/close-subshell.expect
new file mode 100644
index 000000000000..184688718a9c
--- /dev/null
+++ b/t/chainlint/close-subshell.expect
@@ -0,0 +1,25 @@
+(
+	foo
+>) &&
+(
+	bar
+>) >out &&
+(
+	baz
+>) 2>err &&
+(
+	boo
+>) <input &&
+(
+	bip
+>) | wuzzle &&
+(
+	bop
+>) | fazz 	fozz &&
+(
+	bup
+>) |
+fuzzle &&
+(
+	yop
+>)
diff --git a/t/chainlint/close-subshell.test b/t/chainlint/close-subshell.test
new file mode 100644
index 000000000000..508ca447fd6c
--- /dev/null
+++ b/t/chainlint/close-subshell.test
@@ -0,0 +1,27 @@
+# LINT: closing ")" with various decorations ("&&", ">", "|", etc.)
+(
+	foo
+) &&
+(
+	bar
+) >out &&
+(
+	baz
+) 2>err &&
+(
+	boo
+) <input &&
+(
+	bip
+) | wuzzle &&
+(
+	bop
+) | fazz \
+	fozz &&
+(
+	bup
+) |
+fuzzle &&
+(
+	yop
+)
diff --git a/t/chainlint/command-substitution.expect b/t/chainlint/command-substitution.expect
new file mode 100644
index 000000000000..ad4118e537e4
--- /dev/null
+++ b/t/chainlint/command-substitution.expect
@@ -0,0 +1,9 @@
+(
+	foo &&
+	bar=$(gobble) &&
+	baz
+>) &&
+(
+?!AMP?!	bar=$(gobble blocks)
+	baz
+>)
diff --git a/t/chainlint/command-substitution.test b/t/chainlint/command-substitution.test
new file mode 100644
index 000000000000..3bbb002a4c74
--- /dev/null
+++ b/t/chainlint/command-substitution.test
@@ -0,0 +1,11 @@
+(
+	foo &&
+# LINT: closing ")" of $(...) not misinterpreted as subshell-closing ")"
+	bar=$(gobble) &&
+	baz
+) &&
+(
+# LINT: missing "&&" on $(...)
+	bar=$(gobble blocks)
+	baz
+)
diff --git a/t/chainlint/comment.expect b/t/chainlint/comment.expect
new file mode 100644
index 000000000000..3be939ed388c
--- /dev/null
+++ b/t/chainlint/comment.expect
@@ -0,0 +1,4 @@
+(
+	nothing &&
+	something
+>)
diff --git a/t/chainlint/comment.test b/t/chainlint/comment.test
new file mode 100644
index 000000000000..113c0c466f17
--- /dev/null
+++ b/t/chainlint/comment.test
@@ -0,0 +1,11 @@
+(
+# LINT: swallow comment lines
+	# comment 1
+	nothing &&
+	# comment 2
+	something
+# LINT: swallow comment lines since final _statement_ before subshell end is
+# LINT: significant to "&&"-check, not final _line_ (which might be comment)
+	# comment 3
+	# comment 4
+)
diff --git a/t/chainlint/complex-if-in-cuddled-loop.expect b/t/chainlint/complex-if-in-cuddled-loop.expect
new file mode 100644
index 000000000000..9674b88cf252
--- /dev/null
+++ b/t/chainlint/complex-if-in-cuddled-loop.expect
@@ -0,0 +1,10 @@
+(
+for i in a b c; do
+   if test "$(echo $(waffle bat))" = "eleventeen" &&
+     test "$x" = "$y"; then
+     :
+   else
+     echo >file
+   fi
+> done) &&
+test ! -f file
diff --git a/t/chainlint/complex-if-in-cuddled-loop.test b/t/chainlint/complex-if-in-cuddled-loop.test
new file mode 100644
index 000000000000..571bbd85cdfc
--- /dev/null
+++ b/t/chainlint/complex-if-in-cuddled-loop.test
@@ -0,0 +1,11 @@
+# LINT: 'for' loop cuddled with "(" and ")" and nested 'if' with complex
+# LINT: multi-line condition; indented with spaces, not tabs
+(for i in a b c; do
+   if test "$(echo $(waffle bat))" = "eleventeen" &&
+     test "$x" = "$y"; then
+     :
+   else
+     echo >file
+   fi
+ done) &&
+test ! -f file
diff --git a/t/chainlint/cuddled-if-then-else.expect b/t/chainlint/cuddled-if-then-else.expect
new file mode 100644
index 000000000000..ab2a026fbc2f
--- /dev/null
+++ b/t/chainlint/cuddled-if-then-else.expect
@@ -0,0 +1,7 @@
+(
+if test -z ""; then
+    echo empty
+ else
+    echo bizzy
+> fi) &&
+echo foobar
diff --git a/t/chainlint/cuddled-if-then-else.test b/t/chainlint/cuddled-if-then-else.test
new file mode 100644
index 000000000000..eed774a9d643
--- /dev/null
+++ b/t/chainlint/cuddled-if-then-else.test
@@ -0,0 +1,7 @@
+# LINT: 'if' cuddled with "(" and ")"; indented with spaces, not tabs
+(if test -z ""; then
+    echo empty
+ else
+    echo bizzy
+ fi) &&
+echo foobar
diff --git a/t/chainlint/cuddled-loop.expect b/t/chainlint/cuddled-loop.expect
new file mode 100644
index 000000000000..8c0260d7f1d9
--- /dev/null
+++ b/t/chainlint/cuddled-loop.expect
@@ -0,0 +1,5 @@
+(
+ while read x
+  do foobar bop || exit 1
+>  done <file ) &&
+outside subshell
diff --git a/t/chainlint/cuddled-loop.test b/t/chainlint/cuddled-loop.test
new file mode 100644
index 000000000000..a841d781f04f
--- /dev/null
+++ b/t/chainlint/cuddled-loop.test
@@ -0,0 +1,7 @@
+# LINT: 'while' loop cuddled with "(" and ")", with embedded (allowed)
+# LINT: "|| exit {n}" to exit loop early, and using redirection "<" to feed
+# LINT: loop; indented with spaces, not tabs
+( while read x
+  do foobar bop || exit 1
+  done <file ) &&
+outside subshell
diff --git a/t/chainlint/cuddled.expect b/t/chainlint/cuddled.expect
new file mode 100644
index 000000000000..b506d4622197
--- /dev/null
+++ b/t/chainlint/cuddled.expect
@@ -0,0 +1,21 @@
+(
+cd foo &&
+	bar
+>) &&
+
+(
+?!AMP?!cd foo
+	bar
+>) &&
+
+(
+	cd foo &&
+>	bar) &&
+
+(
+cd foo &&
+>	bar) &&
+
+(
+?!AMP?!cd foo
+>	bar)
diff --git a/t/chainlint/cuddled.test b/t/chainlint/cuddled.test
new file mode 100644
index 000000000000..0499fa418059
--- /dev/null
+++ b/t/chainlint/cuddled.test
@@ -0,0 +1,23 @@
+# LINT: first subshell statement cuddled with opening "("; for implementation
+# LINT: simplicity, "(..." is split into two lines, "(" and "..."
+(cd foo &&
+	bar
+) &&
+
+# LINT: same with missing "&&"
+(cd foo
+	bar
+) &&
+
+# LINT: closing ")" cuddled with final subshell statement
+(
+	cd foo &&
+	bar) &&
+
+# LINT: "(" and ")" cuddled with first and final subshell statements
+(cd foo &&
+	bar) &&
+
+# LINT: same with missing "&&"
+(cd foo
+	bar)
diff --git a/t/chainlint/exit-loop.expect b/t/chainlint/exit-loop.expect
new file mode 100644
index 000000000000..84d8bdebc026
--- /dev/null
+++ b/t/chainlint/exit-loop.expect
@@ -0,0 +1,24 @@
+(
+	for i in a b c
+	do
+		foo || exit 1
+		bar &&
+		baz
+	done
+>) &&
+(
+	while true
+	do
+		foo || exit 1
+		bar &&
+		baz
+	done
+>) &&
+(
+	i=0 &&
+	while test $i -lt 10
+	do
+		echo $i || exit
+		i=$(($i + 1))
+	done
+>)
diff --git a/t/chainlint/exit-loop.test b/t/chainlint/exit-loop.test
new file mode 100644
index 000000000000..2f038207e194
--- /dev/null
+++ b/t/chainlint/exit-loop.test
@@ -0,0 +1,27 @@
+(
+	for i in a b c
+	do
+# LINT: "|| exit {n}" valid for-loop escape in subshell; no "&&" needed
+		foo || exit 1
+		bar &&
+		baz
+	done
+) &&
+(
+	while true
+	do
+# LINT: "|| exit {n}" valid while-loop escape in subshell; no "&&" needed
+		foo || exit 1
+		bar &&
+		baz
+	done
+) &&
+(
+	i=0 &&
+	while test $i -lt 10
+	do
+# LINT: "|| exit" (sans exit code) valid escape in subshell; no "&&" needed
+		echo $i || exit
+		i=$(($i + 1))
+	done
+)
diff --git a/t/chainlint/exit-subshell.expect b/t/chainlint/exit-subshell.expect
new file mode 100644
index 000000000000..bf78454f74c8
--- /dev/null
+++ b/t/chainlint/exit-subshell.expect
@@ -0,0 +1,5 @@
+(
+	foo || exit 1
+	bar &&
+	baz
+>)
diff --git a/t/chainlint/exit-subshell.test b/t/chainlint/exit-subshell.test
new file mode 100644
index 000000000000..4e6ab69b8867
--- /dev/null
+++ b/t/chainlint/exit-subshell.test
@@ -0,0 +1,6 @@
+(
+# LINT: "|| exit {n}" valid subshell escape without hurting &&-chain
+	foo || exit 1
+	bar &&
+	baz
+)
diff --git a/t/chainlint/for-loop.expect b/t/chainlint/for-loop.expect
new file mode 100644
index 000000000000..c33cf56ee73a
--- /dev/null
+++ b/t/chainlint/for-loop.expect
@@ -0,0 +1,11 @@
+(
+	for i in a b c
+	do
+?!AMP?!		echo $i
+		cat
+?!AMP?!	done
+	for i in a b c; do
+		echo $i &&
+		cat $i
+	done
+>)
diff --git a/t/chainlint/for-loop.test b/t/chainlint/for-loop.test
new file mode 100644
index 000000000000..7db76262bc2e
--- /dev/null
+++ b/t/chainlint/for-loop.test
@@ -0,0 +1,19 @@
+(
+# LINT: 'for', 'do', 'done' do not need "&&"
+	for i in a b c
+	do
+# LINT: missing "&&" on 'echo'
+		echo $i
+# LINT: last statement of while does not need "&&"
+		cat <<-\EOF
+		bar
+		EOF
+# LINT: missing "&&" on 'done'
+	done
+
+# LINT: 'do' on same line as 'for'
+	for i in a b c; do
+		echo $i &&
+		cat $i
+	done
+)
diff --git a/t/chainlint/here-doc-close-subshell.expect b/t/chainlint/here-doc-close-subshell.expect
new file mode 100644
index 000000000000..f011e335e5f1
--- /dev/null
+++ b/t/chainlint/here-doc-close-subshell.expect
@@ -0,0 +1,2 @@
+(
+>	cat)
diff --git a/t/chainlint/here-doc-close-subshell.test b/t/chainlint/here-doc-close-subshell.test
new file mode 100644
index 000000000000..b857ff546765
--- /dev/null
+++ b/t/chainlint/here-doc-close-subshell.test
@@ -0,0 +1,5 @@
+(
+# LINT: line contains here-doc and closes nested subshell
+	cat <<-\INPUT)
+	fizz
+	INPUT
diff --git a/t/chainlint/here-doc-multi-line-command-subst.expect b/t/chainlint/here-doc-multi-line-command-subst.expect
new file mode 100644
index 000000000000..e5fb752d2fc2
--- /dev/null
+++ b/t/chainlint/here-doc-multi-line-command-subst.expect
@@ -0,0 +1,5 @@
+(
+	x=$(bobble &&
+?!AMP?!>>		wiffle)
+	echo $x
+>)
diff --git a/t/chainlint/here-doc-multi-line-command-subst.test b/t/chainlint/here-doc-multi-line-command-subst.test
new file mode 100644
index 000000000000..899bc5de8bce
--- /dev/null
+++ b/t/chainlint/here-doc-multi-line-command-subst.test
@@ -0,0 +1,9 @@
+(
+# LINT: line contains here-doc and opens multi-line $(...)
+	x=$(bobble <<-\END &&
+		fossil
+		vegetable
+		END
+		wiffle)
+	echo $x
+)
diff --git a/t/chainlint/here-doc-multi-line-string.expect b/t/chainlint/here-doc-multi-line-string.expect
new file mode 100644
index 000000000000..32038a070c2a
--- /dev/null
+++ b/t/chainlint/here-doc-multi-line-string.expect
@@ -0,0 +1,4 @@
+(
+?!AMP?!	cat && echo "multi-line	string"
+	bap
+>)
diff --git a/t/chainlint/here-doc-multi-line-string.test b/t/chainlint/here-doc-multi-line-string.test
new file mode 100644
index 000000000000..a53edbcc8d89
--- /dev/null
+++ b/t/chainlint/here-doc-multi-line-string.test
@@ -0,0 +1,8 @@
+(
+# LINT: line contains here-doc and opens multi-line string
+	cat <<-\TXT && echo "multi-line
+	string"
+	fizzle
+	TXT
+	bap
+)
diff --git a/t/chainlint/here-doc.expect b/t/chainlint/here-doc.expect
new file mode 100644
index 000000000000..534b065e38ba
--- /dev/null
+++ b/t/chainlint/here-doc.expect
@@ -0,0 +1,9 @@
+boodle wobba        gorgo snoot        wafta snurb &&
+
+cat >foo &&
+
+cat >bar &&
+
+cat >boo &&
+
+horticulture
diff --git a/t/chainlint/here-doc.test b/t/chainlint/here-doc.test
new file mode 100644
index 000000000000..ad4ce8afd9b5
--- /dev/null
+++ b/t/chainlint/here-doc.test
@@ -0,0 +1,37 @@
+# LINT: stitch together incomplete \-ending lines
+# LINT: swallow here-doc to avoid false positives in content
+boodle wobba \
+       gorgo snoot \
+       wafta snurb <<EOF &&
+quoth the raven,
+nevermore...
+EOF
+
+# LINT: swallow here-doc with arbitrary tag
+cat <<-Arbitrary_Tag_42 >foo &&
+snoz
+boz
+woz
+Arbitrary_Tag_42
+
+# LINT: swallow 'quoted' here-doc
+cat <<'FUMP' >bar &&
+snoz
+boz
+woz
+FUMP
+
+# LINT: swallow "quoted" here-doc
+cat <<"zump" >boo &&
+snoz
+boz
+woz
+zump
+
+# LINT: swallow here-doc (EOF is last line of test)
+horticulture <<\EOF
+gomez
+morticia
+wednesday
+pugsly
+EOF
diff --git a/t/chainlint/if-in-loop.expect b/t/chainlint/if-in-loop.expect
new file mode 100644
index 000000000000..03d3ceb22d89
--- /dev/null
+++ b/t/chainlint/if-in-loop.expect
@@ -0,0 +1,12 @@
+(
+	for i in a b c
+	do
+		if false
+		then
+?!AMP?!			echo "err"
+			exit 1
+?!AMP?!		fi
+		foo
+?!AMP?!	done
+	bar
+>)
diff --git a/t/chainlint/if-in-loop.test b/t/chainlint/if-in-loop.test
new file mode 100644
index 000000000000..daf22da16476
--- /dev/null
+++ b/t/chainlint/if-in-loop.test
@@ -0,0 +1,15 @@
+(
+	for i in a b c
+	do
+		if false
+		then
+# LINT: missing "&&" on 'echo'
+			echo "err"
+			exit 1
+# LINT: missing "&&" on 'fi'
+		fi
+		foo
+# LINT: missing "&&" on 'done'
+	done
+	bar
+)
diff --git a/t/chainlint/if-then-else.expect b/t/chainlint/if-then-else.expect
new file mode 100644
index 000000000000..5953c7bfbc2e
--- /dev/null
+++ b/t/chainlint/if-then-else.expect
@@ -0,0 +1,19 @@
+(
+	if test -n ""
+	then
+?!AMP?!		echo very
+		echo empty
+	elif test -z ""
+		echo foo
+	else
+		echo foo &&
+		cat
+?!AMP?!	fi
+	echo poodle
+>) &&
+(
+	if test -n ""; then
+		echo very &&
+?!AMP?!		echo empty
+	if
+>)
diff --git a/t/chainlint/if-then-else.test b/t/chainlint/if-then-else.test
new file mode 100644
index 000000000000..9bd8e9a4c68c
--- /dev/null
+++ b/t/chainlint/if-then-else.test
@@ -0,0 +1,28 @@
+(
+# LINT: 'if', 'then', 'elif', 'else', 'fi' do not need "&&"
+	if test -n ""
+	then
+# LINT: missing "&&" on 'echo'
+		echo very
+# LINT: last statement before 'elif' does not need "&&"
+		echo empty
+	elif test -z ""
+# LINT: last statement before 'else' does not need "&&"
+		echo foo
+	else
+		echo foo &&
+# LINT: last statement before 'fi' does not need "&&"
+		cat <<-\EOF
+		bar
+		EOF
+# LINT: missing "&&" on 'fi'
+	fi
+	echo poodle
+) &&
+(
+# LINT: 'then' on same line as 'if'
+	if test -n ""; then
+		echo very &&
+		echo empty
+	if
+)
diff --git a/t/chainlint/incomplete-line.expect b/t/chainlint/incomplete-line.expect
new file mode 100644
index 000000000000..2f3ebabdc286
--- /dev/null
+++ b/t/chainlint/incomplete-line.expect
@@ -0,0 +1,4 @@
+line 1 line 2 line 3 line 4 &&
+(
+	line 5 	line 6 	line 7 	line 8
+>)
diff --git a/t/chainlint/incomplete-line.test b/t/chainlint/incomplete-line.test
new file mode 100644
index 000000000000..d8566580839b
--- /dev/null
+++ b/t/chainlint/incomplete-line.test
@@ -0,0 +1,12 @@
+# LINT: stitch together all incomplete \-ending lines
+line 1 \
+line 2 \
+line 3 \
+line 4 &&
+(
+# LINT: stitch together all incomplete \-ending lines (subshell)
+	line 5 \
+	line 6 \
+	line 7 \
+	line 8
+)
diff --git a/t/chainlint/inline-comment.expect b/t/chainlint/inline-comment.expect
new file mode 100644
index 000000000000..fc9f250ac48e
--- /dev/null
+++ b/t/chainlint/inline-comment.expect
@@ -0,0 +1,9 @@
+(
+	foobar &&
+?!AMP?!	barfoo
+	flibble "not a # comment"
+>) &&
+
+(
+cd foo &&
+>	flibble "not a # comment")
diff --git a/t/chainlint/inline-comment.test b/t/chainlint/inline-comment.test
new file mode 100644
index 000000000000..8f26856e775b
--- /dev/null
+++ b/t/chainlint/inline-comment.test
@@ -0,0 +1,12 @@
+(
+# LINT: swallow inline comment (leaving command intact)
+	foobar && # comment 1
+# LINT: mispositioned "&&" (correctly) swallowed with comment
+	barfoo # wrong position for &&
+# LINT: "#" in string not misinterpreted as comment
+	flibble "not a # comment"
+) &&
+
+# LINT: "#" in string in cuddled subshell not misinterpreted as comment
+(cd foo &&
+	flibble "not a # comment")
diff --git a/t/chainlint/loop-in-if.expect b/t/chainlint/loop-in-if.expect
new file mode 100644
index 000000000000..088e622c3141
--- /dev/null
+++ b/t/chainlint/loop-in-if.expect
@@ -0,0 +1,12 @@
+(
+	if true
+	then
+		while true
+		do
+?!AMP?!			echo "pop"
+			echo "glup"
+?!AMP?!		done
+		foo
+?!AMP?!	fi
+	bar
+>)
diff --git a/t/chainlint/loop-in-if.test b/t/chainlint/loop-in-if.test
new file mode 100644
index 000000000000..93e8ba8e4d9a
--- /dev/null
+++ b/t/chainlint/loop-in-if.test
@@ -0,0 +1,15 @@
+(
+	if true
+	then
+		while true
+		do
+# LINT: missing "&&" on 'echo'
+			echo "pop"
+			echo "glup"
+# LINT: missing "&&" on 'done'
+		done
+		foo
+# LINT: missing "&&" on 'fi'
+	fi
+	bar
+)
diff --git a/t/chainlint/multi-line-nested-command-substitution.expect b/t/chainlint/multi-line-nested-command-substitution.expect
new file mode 100644
index 000000000000..59b6c8b850a1
--- /dev/null
+++ b/t/chainlint/multi-line-nested-command-substitution.expect
@@ -0,0 +1,18 @@
+(
+	foo &&
+	x=$(
+		echo bar |
+		cat
+>>	) &&
+	echo ok
+>) |
+sort &&
+(
+	bar &&
+	x=$(echo bar |
+		cat
+>>	) &&
+	y=$(echo baz |
+>>		fip) &&
+	echo fail
+>)
diff --git a/t/chainlint/multi-line-nested-command-substitution.test b/t/chainlint/multi-line-nested-command-substitution.test
new file mode 100644
index 000000000000..300058341b6f
--- /dev/null
+++ b/t/chainlint/multi-line-nested-command-substitution.test
@@ -0,0 +1,18 @@
+(
+	foo &&
+	x=$(
+		echo bar |
+		cat
+	) &&
+	echo ok
+) |
+sort &&
+(
+	bar &&
+	x=$(echo bar |
+		cat
+	) &&
+	y=$(echo baz |
+		fip) &&
+	echo fail
+)
diff --git a/t/chainlint/multi-line-string.expect b/t/chainlint/multi-line-string.expect
new file mode 100644
index 000000000000..170cb5999322
--- /dev/null
+++ b/t/chainlint/multi-line-string.expect
@@ -0,0 +1,15 @@
+(
+	x="line 1		line 2		line 3" &&
+?!AMP?!	y='line 1		line2'
+	foobar
+>) &&
+(
+	echo "there's nothing to see here" &&
+	exit
+>) &&
+(
+	echo "xyz" "abc		def		ghi" &&
+	echo 'xyz' 'abc		def		ghi' &&
+	echo 'xyz' "abc		def		ghi" &&
+	barfoo
+>)
diff --git a/t/chainlint/multi-line-string.test b/t/chainlint/multi-line-string.test
new file mode 100644
index 000000000000..287ab8970548
--- /dev/null
+++ b/t/chainlint/multi-line-string.test
@@ -0,0 +1,27 @@
+(
+	x="line 1
+		line 2
+		line 3" &&
+# LINT: missing "&&" on assignment
+	y='line 1
+		line2'
+	foobar
+) &&
+(
+# LINT: apostrophe (in a contraction) within string not misinterpreted as
+# LINT: starting multi-line single-quoted string
+	echo "there's nothing to see here" &&
+	exit
+) &&
+(
+	echo "xyz" "abc
+		def
+		ghi" &&
+	echo 'xyz' 'abc
+		def
+		ghi' &&
+	echo 'xyz' "abc
+		def
+		ghi" &&
+	barfoo
+)
diff --git a/t/chainlint/negated-one-liner.expect b/t/chainlint/negated-one-liner.expect
new file mode 100644
index 000000000000..cf18429d0397
--- /dev/null
+++ b/t/chainlint/negated-one-liner.expect
@@ -0,0 +1,5 @@
+! (foo && bar) &&
+! (foo && bar) >baz &&
+
+?!SEMI?!! (foo; bar) &&
+?!SEMI?!! (foo; bar) >baz
diff --git a/t/chainlint/negated-one-liner.test b/t/chainlint/negated-one-liner.test
new file mode 100644
index 000000000000..c9598e915307
--- /dev/null
+++ b/t/chainlint/negated-one-liner.test
@@ -0,0 +1,7 @@
+# LINT: top-level one-liner subshell
+! (foo && bar) &&
+! (foo && bar) >baz &&
+
+# LINT: top-level one-liner subshell missing internal "&&"
+! (foo; bar) &&
+! (foo; bar) >baz
diff --git a/t/chainlint/nested-cuddled-subshell.expect b/t/chainlint/nested-cuddled-subshell.expect
new file mode 100644
index 000000000000..c2a59ffc335c
--- /dev/null
+++ b/t/chainlint/nested-cuddled-subshell.expect
@@ -0,0 +1,19 @@
+(
+	(cd foo &&
+		bar
+>>	) &&
+	(cd foo &&
+		bar
+?!AMP?!>>	)
+	(
+		cd foo &&
+>>		bar) &&
+	(
+		cd foo &&
+?!AMP?!>>		bar)
+	(cd foo &&
+>>		bar) &&
+	(cd foo &&
+?!AMP?!>>		bar)
+	foobar
+>)
diff --git a/t/chainlint/nested-cuddled-subshell.test b/t/chainlint/nested-cuddled-subshell.test
new file mode 100644
index 000000000000..8fd656c7b598
--- /dev/null
+++ b/t/chainlint/nested-cuddled-subshell.test
@@ -0,0 +1,31 @@
+(
+# LINT: opening "(" cuddled with first nested subshell statement
+	(cd foo &&
+		bar
+	) &&
+
+# LINT: same but "&&" missing
+	(cd foo &&
+		bar
+	)
+
+# LINT: closing ")" cuddled with final nested subshell statement
+	(
+		cd foo &&
+		bar) &&
+
+# LINT: same but "&&" missing
+	(
+		cd foo &&
+		bar)
+
+# LINT: "(" and ")" cuddled with first and final subshell statements
+	(cd foo &&
+		bar) &&
+
+# LINT: same but "&&" missing
+	(cd foo &&
+		bar)
+
+	foobar
+)
diff --git a/t/chainlint/nested-here-doc.expect b/t/chainlint/nested-here-doc.expect
new file mode 100644
index 000000000000..0c9ef1cfc695
--- /dev/null
+++ b/t/chainlint/nested-here-doc.expect
@@ -0,0 +1,7 @@
+cat >foop &&
+
+(
+	cat &&
+?!AMP?!	cat
+	foobar
+>)
diff --git a/t/chainlint/nested-here-doc.test b/t/chainlint/nested-here-doc.test
new file mode 100644
index 000000000000..f35404bf0f93
--- /dev/null
+++ b/t/chainlint/nested-here-doc.test
@@ -0,0 +1,33 @@
+# LINT: inner "EOF" not misintrepreted as closing ARBITRARY here-doc
+cat <<ARBITRARY >foop &&
+naddle
+fub <<EOF
+	nozzle
+	noodle
+EOF
+formp
+ARBITRARY
+
+(
+# LINT: inner "EOF" not misintrepreted as closing INPUT_END here-doc
+	cat <<-\INPUT_END &&
+	fish are mice
+	but geese go slow
+	data <<EOF
+		perl is lerp
+		and nothing else
+	EOF
+	toink
+	INPUT_END
+
+# LINT: same but missing "&&"
+	cat <<-\EOT
+	text goes here
+	data <<EOF
+		data goes here
+	EOF
+	more test here
+	EOT
+
+	foobar
+)
diff --git a/t/chainlint/nested-subshell-comment.expect b/t/chainlint/nested-subshell-comment.expect
new file mode 100644
index 000000000000..15b68d437379
--- /dev/null
+++ b/t/chainlint/nested-subshell-comment.expect
@@ -0,0 +1,11 @@
+(
+	foo &&
+	(
+		bar &&
+		# bottles wobble while fiddles gobble
+		# minor numbers of cows (or do they?)
+		baz &&
+		snaff
+?!AMP?!>>	)
+	fuzzy
+>)
diff --git a/t/chainlint/nested-subshell-comment.test b/t/chainlint/nested-subshell-comment.test
new file mode 100644
index 000000000000..0ff136ab3cf1
--- /dev/null
+++ b/t/chainlint/nested-subshell-comment.test
@@ -0,0 +1,13 @@
+(
+	foo &&
+	(
+		bar &&
+# LINT: ")" in comment in nested subshell not misinterpreted as closing ")"
+		# bottles wobble while fiddles gobble
+		# minor numbers of cows (or do they?)
+		baz &&
+		snaff
+# LINT: missing "&&" on ')'
+	)
+	fuzzy
+)
diff --git a/t/chainlint/nested-subshell.expect b/t/chainlint/nested-subshell.expect
new file mode 100644
index 000000000000..c8165ad19ec5
--- /dev/null
+++ b/t/chainlint/nested-subshell.expect
@@ -0,0 +1,12 @@
+(
+	cd foo &&
+	(
+		echo a &&
+		echo b
+>>	) >file &&
+	cd foo &&
+	(
+		echo a
+		echo b
+>>	) >file
+>)
diff --git a/t/chainlint/nested-subshell.test b/t/chainlint/nested-subshell.test
new file mode 100644
index 000000000000..998b05a47d30
--- /dev/null
+++ b/t/chainlint/nested-subshell.test
@@ -0,0 +1,14 @@
+(
+	cd foo &&
+	(
+		echo a &&
+		echo b
+	) >file &&
+
+	cd foo &&
+	(
+# LINT: nested multi-line subshell not presently checked for missing "&&"
+		echo a
+		echo b
+	) >file
+)
diff --git a/t/chainlint/one-liner.expect b/t/chainlint/one-liner.expect
new file mode 100644
index 000000000000..237f22734963
--- /dev/null
+++ b/t/chainlint/one-liner.expect
@@ -0,0 +1,9 @@
+(foo && bar) &&
+(foo && bar) |
+(foo && bar) >baz &&
+
+?!SEMI?!(foo; bar) &&
+?!SEMI?!(foo; bar) |
+?!SEMI?!(foo; bar) >baz
+
+(foo "bar; baz")
diff --git a/t/chainlint/one-liner.test b/t/chainlint/one-liner.test
new file mode 100644
index 000000000000..ec9acb98253d
--- /dev/null
+++ b/t/chainlint/one-liner.test
@@ -0,0 +1,12 @@
+# LINT: top-level one-liner subshell
+(foo && bar) &&
+(foo && bar) |
+(foo && bar) >baz &&
+
+# LINT: top-level one-liner subshell missing internal "&&"
+(foo; bar) &&
+(foo; bar) |
+(foo; bar) >baz
+
+# LINT: ";" in string not misinterpreted as broken &&-chain
+(foo "bar; baz")
diff --git a/t/chainlint/p4-filespec.expect b/t/chainlint/p4-filespec.expect
new file mode 100644
index 000000000000..98b3d881fda9
--- /dev/null
+++ b/t/chainlint/p4-filespec.expect
@@ -0,0 +1,4 @@
+(
+	p4 print -1 //depot/fiddle#42 >file &&
+	foobar
+>)
diff --git a/t/chainlint/p4-filespec.test b/t/chainlint/p4-filespec.test
new file mode 100644
index 000000000000..4fd2d6e2b80b
--- /dev/null
+++ b/t/chainlint/p4-filespec.test
@@ -0,0 +1,5 @@
+(
+# LINT: Perforce revspec in filespec not misinterpreted as in-line comment
+	p4 print -1 //depot/fiddle#42 >file &&
+	foobar
+)
diff --git a/t/chainlint/pipe.expect b/t/chainlint/pipe.expect
new file mode 100644
index 000000000000..211b901dbc42
--- /dev/null
+++ b/t/chainlint/pipe.expect
@@ -0,0 +1,8 @@
+(
+	foo |
+	bar |
+	baz &&
+	fish |
+?!AMP?!	cow
+	sunder
+>)
diff --git a/t/chainlint/pipe.test b/t/chainlint/pipe.test
new file mode 100644
index 000000000000..e6af4de91672
--- /dev/null
+++ b/t/chainlint/pipe.test
@@ -0,0 +1,12 @@
+(
+# LINT: no "&&" needed on line ending with "|"
+	foo |
+	bar |
+	baz &&
+
+# LINT: final line of pipe sequence ('cow') lacking "&&"
+	fish |
+	cow
+
+	sunder
+)
diff --git a/t/chainlint/semicolon.expect b/t/chainlint/semicolon.expect
new file mode 100644
index 000000000000..1d79384606d2
--- /dev/null
+++ b/t/chainlint/semicolon.expect
@@ -0,0 +1,20 @@
+(
+?!AMP?!?!SEMI?!	cat foo ; echo bar
+?!SEMI?!	cat foo ; echo bar
+>) &&
+(
+?!SEMI?!	cat foo ; echo bar &&
+?!SEMI?!	cat foo ; echo bar
+>) &&
+(
+	echo "foo; bar" &&
+?!SEMI?!	cat foo; echo bar
+>) &&
+(
+?!SEMI?!	foo;
+>) &&
+(
+cd foo &&
+	for i in a b c; do
+?!SEMI?!		echo;
+>	done)
diff --git a/t/chainlint/semicolon.test b/t/chainlint/semicolon.test
new file mode 100644
index 000000000000..d82c8ebbc006
--- /dev/null
+++ b/t/chainlint/semicolon.test
@@ -0,0 +1,25 @@
+(
+# LINT: missing internal "&&" and ending "&&"
+	cat foo ; echo bar
+# LINT: final statement before ")" only missing internal "&&"
+	cat foo ; echo bar
+) &&
+(
+# LINT: missing internal "&&"
+	cat foo ; echo bar &&
+	cat foo ; echo bar
+) &&
+(
+# LINT: not fooled by semicolon in string
+	echo "foo; bar" &&
+	cat foo; echo bar
+) &&
+(
+# LINT: unnecessary terminating semicolon
+	foo;
+) &&
+(cd foo &&
+	for i in a b c; do
+# LINT: unnecessary terminating semicolon
+		echo;
+	done)
diff --git a/t/chainlint/subshell-here-doc.expect b/t/chainlint/subshell-here-doc.expect
new file mode 100644
index 000000000000..74723e734043
--- /dev/null
+++ b/t/chainlint/subshell-here-doc.expect
@@ -0,0 +1,11 @@
+(
+	echo wobba 	       gorgo snoot 	       wafta snurb &&
+?!AMP?!	cat >bip
+	echo >bop
+>) &&
+(
+	cat >bup &&
+	cat >bup2 &&
+	cat >bup3 &&
+	meep
+>)
diff --git a/t/chainlint/subshell-here-doc.test b/t/chainlint/subshell-here-doc.test
new file mode 100644
index 000000000000..f6b3ba4214a4
--- /dev/null
+++ b/t/chainlint/subshell-here-doc.test
@@ -0,0 +1,39 @@
+(
+# LINT: stitch together incomplete \-ending lines
+# LINT: swallow here-doc to avoid false positives in content
+	echo wobba \
+	       gorgo snoot \
+	       wafta snurb <<-EOF &&
+	quoth the raven,
+	nevermore...
+	EOF
+
+# LINT: missing "&&" on 'cat'
+	cat <<EOF >bip
+	fish fly high
+	EOF
+
+# LINT: swallow here-doc (EOF is last line of subshell)
+	echo <<-\EOF >bop
+	gomez
+	morticia
+	wednesday
+	pugsly
+	EOF
+) &&
+(
+# LINT: swallow here-doc with arbitrary tag
+	cat <<-\ARBITRARY >bup &&
+	glink
+	FIZZ
+	ARBITRARY
+	cat <<-'ARBITRARY2' >bup2 &&
+	glink
+	FIZZ
+	ARBITRARY2
+	cat <<-"ARBITRARY3" >bup3 &&
+	glink
+	FIZZ
+	ARBITRARY3
+	meep
+)
diff --git a/t/chainlint/subshell-one-liner.expect b/t/chainlint/subshell-one-liner.expect
new file mode 100644
index 000000000000..51162821d7e1
--- /dev/null
+++ b/t/chainlint/subshell-one-liner.expect
@@ -0,0 +1,14 @@
+(
+	(foo && bar) &&
+	(foo && bar) |
+	(foo && bar) >baz &&
+?!SEMI?!	(foo; bar) &&
+?!SEMI?!	(foo; bar) |
+?!SEMI?!	(foo; bar) >baz &&
+	(foo || exit 1) &&
+	(foo || exit 1) |
+	(foo || exit 1) >baz &&
+?!AMP?!	(foo && bar)
+?!AMP?!?!SEMI?!	(foo && bar; baz)
+	foobar
+>)
diff --git a/t/chainlint/subshell-one-liner.test b/t/chainlint/subshell-one-liner.test
new file mode 100644
index 000000000000..37fa643c20a9
--- /dev/null
+++ b/t/chainlint/subshell-one-liner.test
@@ -0,0 +1,24 @@
+(
+# LINT: nested one-liner subshell
+	(foo && bar) &&
+	(foo && bar) |
+	(foo && bar) >baz &&
+
+# LINT: nested one-liner subshell missing internal "&&"
+	(foo; bar) &&
+	(foo; bar) |
+	(foo; bar) >baz &&
+
+# LINT: nested one-liner subshell with "|| exit"
+	(foo || exit 1) &&
+	(foo || exit 1) |
+	(foo || exit 1) >baz &&
+
+# LINT: nested one-liner subshell lacking ending "&&"
+	(foo && bar)
+
+# LINT: nested one-liner subshell missing internal "&&" and lacking ending "&&"
+	(foo && bar; baz)
+
+	foobar
+)
diff --git a/t/chainlint/t7900-subtree.expect b/t/chainlint/t7900-subtree.expect
new file mode 100644
index 000000000000..c9913429e64b
--- /dev/null
+++ b/t/chainlint/t7900-subtree.expect
@@ -0,0 +1,10 @@
+(
+	chks="sub1sub2sub3sub4" &&
+	chks_sub=$(cat | sed 's,^,sub dir/,'
+>>) &&
+	chkms="main-sub1main-sub2main-sub3main-sub4" &&
+	chkms_sub=$(cat | sed 's,^,sub dir/,'
+>>) &&
+	subfiles=$(git ls-files) &&
+	check_equal "$subfiles" "$chkms$chks"
+>)
diff --git a/t/chainlint/t7900-subtree.test b/t/chainlint/t7900-subtree.test
new file mode 100644
index 000000000000..277d8358dfd5
--- /dev/null
+++ b/t/chainlint/t7900-subtree.test
@@ -0,0 +1,22 @@
+(
+	chks="sub1
+sub2
+sub3
+sub4" &&
+	chks_sub=$(cat <<TXT | sed 's,^,sub dir/,'
+$chks
+TXT
+) &&
+	chkms="main-sub1
+main-sub2
+main-sub3
+main-sub4" &&
+	chkms_sub=$(cat <<TXT | sed 's,^,sub dir/,'
+$chkms
+TXT
+) &&
+
+	subfiles=$(git ls-files) &&
+	check_equal "$subfiles" "$chkms
+$chks"
+)
diff --git a/t/chainlint/while-loop.expect b/t/chainlint/while-loop.expect
new file mode 100644
index 000000000000..13cff2c0a511
--- /dev/null
+++ b/t/chainlint/while-loop.expect
@@ -0,0 +1,11 @@
+(
+	while true
+	do
+?!AMP?!		echo foo
+		cat
+?!AMP?!	done
+	while true; do
+		echo foo &&
+		cat bar
+	done
+>)
diff --git a/t/chainlint/while-loop.test b/t/chainlint/while-loop.test
new file mode 100644
index 000000000000..f1df085bf03b
--- /dev/null
+++ b/t/chainlint/while-loop.test
@@ -0,0 +1,19 @@
+(
+# LINT: 'while, 'do', 'done' do not need "&&"
+	while true
+	do
+# LINT: missing "&&" on 'echo'
+		echo foo
+# LINT: last statement of while does not need "&&"
+		cat <<-\EOF
+		bar
+		EOF
+# LINT: missing "&&" on 'done'
+	done
+
+# LINT: 'do' on same line as 'while'
+	while true; do
+		echo foo &&
+		cat bar
+	done
+)