1
+#------------------------------------------------------------------------------
2
+# Detect broken &&-chains in tests.
3
+#
4
+# At present, only &&-chains in subshells are examined by this linter;
5
+# top-level &&-chains are instead checked directly by the test framework. Like
6
+# the top-level &&-chain linter, the subshell linter (intentionally) does not
7
+# check &&-chains within {...} blocks.
8
+#
9
+# Checking for &&-chain breakage is done line-by-line by pure textual
10
+# inspection.
11
+#
12
+# Incomplete lines (those ending with "\") are stitched together with following
13
+# lines to simplify processing, particularly of "one-liner" statements.
14
+# Top-level here-docs are swallowed to avoid false positives within the
15
+# here-doc body, although the statement to which the here-doc is attached is
16
+# retained.
17
+#
18
+# Heuristics are used to detect end-of-subshell when the closing ")" is cuddled
19
+# with the final subshell statement on the same line:
20
+#
21
+# (cd foo &&
22
+# bar)
23
+#
24
+# in order to avoid misinterpreting the ")" in constructs such as "x=$(...)"
25
+# and "case $x in *)" as ending the subshell.
26
+#
27
+# Lines missing a final "&&" are flagged with "?!AMP?!", and lines which chain
28
+# commands with ";" internally rather than "&&" are flagged "?!SEMI?!". A line
29
+# may be flagged for both violations.
30
+#
31
+# Detection of a missing &&-link in a multi-line subshell is complicated by the
32
+# fact that the last statement before the closing ")" must not end with "&&".
33
+# Since processing is line-by-line, it is not known whether a missing "&&" is
34
+# legitimate or not until the _next_ line is seen. To accommodate this, within
35
+# multi-line subshells, each line is stored in sed's "hold" area until after
36
+# the next line is seen and processed. If the next line is a stand-alone ")",
37
+# then a missing "&&" on the previous line is legitimate; otherwise a missing
38
+# "&&" is a break in the &&-chain.
39
+#
40
+# (
41
+# cd foo &&
42
+# bar
43
+# )
44
+#
45
+# In practical terms, when "bar" is encountered, it is flagged with "?!AMP?!",
46
+# but when the stand-alone ")" line is seen which closes the subshell, the
47
+# "?!AMP?!" violation is removed from the "bar" line (retrieved from the "hold"
48
+# area) since the final statement of a subshell must not end with "&&". The
49
+# final line of a subshell may still break the &&-chain by using ";" internally
50
+# to chain commands together rather than "&&", so "?!SEMI?!" is never removed
51
+# from a line (even though "?!AMP?!" might be).
52
+#
53
+# Care is taken to recognize the last _statement_ of a multi-line subshell, not
54
+# necessarily the last textual _line_ within the subshell, since &&-chaining
55
+# applies to statements, not to lines. Consequently, blank lines, comment
56
+# lines, and here-docs are swallowed (but not the command to which the here-doc
57
+# is attached), leaving the last statement in the "hold" area, not the last
58
+# line, thus simplifying &&-link checking.
59
+#
60
+# The final statement before "done" in for- and while-loops, and before "elif",
61
+# "else", and "fi" in if-then-else likewise must not end with "&&", thus
62
+# receives similar treatment.
63
+#
64
+# To facilitate regression testing (and manual debugging), a ">" annotation is
65
+# applied to the line containing ")" which closes a subshell, ">>" to a line
66
+# closing a nested subshell, and ">>>" to a line closing both at once. This
67
+# makes it easy to detect whether the heuristics correctly identify
68
+# end-of-subshell.
69
+#------------------------------------------------------------------------------
70
+
71
+# incomplete line -- slurp up next line
72
+:squash
73
+/\\$/ {
74
+ N
75
+ s/\\\n//
76
+ bsquash
77
+}
78
+
79
+# here-doc -- swallow it to avoid false hits within its body (but keep the
80
+# command to which it was attached)
81
+/<<[ ]*[-\\]*EOF[ ]*/ {
82
+ s/[ ]*<<[ ]*[-\\]*EOF//
83
+ h
84
+ :hereslurp
85
+ N
86
+ s/.*\n//
87
+ /^[ ]*EOF[ ]*$/!bhereslurp
88
+ x
89
+}
90
+
91
+# one-liner "(...) &&"
92
+/^[ ]*!*[ ]*(..*)[ ]*&&[ ]*$/boneline
93
+
94
+# same as above but without trailing "&&"
95
+/^[ ]*!*[ ]*(..*)[ ]*$/boneline
96
+
97
+# one-liner "(...) >x" (or "2>x" or "<x" or "|x" or "&"
98
+/^[ ]*!*[ ]*(..*)[ ]*[0-9]*[<>|&]/boneline
99
+
100
+# multi-line "(...\n...)"
101
+/^[ ]*(/bsubshell
102
+
103
+# innocuous line -- print it and advance to next line
104
+b
105
+
106
+# found one-liner "(...)" -- mark suspect if it uses ";" internally rather than
107
+# "&&" (but not ";" in a string)
108
+:oneline
109
+/;/{
110
+ /"[^"]*;[^"]*"/!s/^/?!SEMI?!/
111
+}
112
+b
113
+
114
+:subshell
115
+# bare "(" line?
116
+/^[ ]*([ ]*$/ {
117
+ # stash for later printing
118
+ h
119
+ bnextline
120
+}
121
+# "(..." line -- split off and stash "(", then process "..." as its own line
122
+x
123
+s/.*/(/
124
+x
125
+s/(//
126
+bslurp
127
+
128
+:nextline
129
+N
130
+s/.*\n//
131
+
132
+:slurp
133
+# incomplete line "...\"
134
+/\\$/bincomplete
135
+# multi-line quoted string "...\n..."
136
+/^[^"]*"[^"]*$/bdqstring
137
+# multi-line quoted string '...\n...' (but not contraction in string "it's so")
138
+/^[^']*'[^']*$/{
139
+ /"[^'"]*'[^'"]*"/!bsqstring
140
+}
141
+# here-doc -- swallow it
142
+/<<[ ]*[-\\]*EOF/bheredoc
143
+/<<[ ]*[-\\]*EOT/bheredoc
144
+/<<[ ]*[-\\]*INPUT_END/bheredoc
145
+# comment or empty line -- discard since final non-comment, non-empty line
146
+# before closing ")", "done", "elsif", "else", or "fi" will need to be
147
+# re-visited to drop "suspect" marking since final line of those constructs
148
+# legitimately lacks "&&", so "suspect" mark must be removed
149
+/^[ ]*#/bnextline
150
+/^[ ]*$/bnextline
151
+# in-line comment -- strip it (but not "#" in a string, Bash ${#...} array
152
+# length, or Perforce "//depot/path#42" revision in filespec)
153
+/[ ]#/{
154
+ /"[^"]*#[^"]*"/!s/[ ]#.*$//
155
+}
156
+# one-liner "case ... esac"
157
+/^[ ]*case[ ]*..*esac/bcheckchain
158
+# multi-line "case ... esac"
159
+/^[ ]*case[ ]..*[ ]in/bcase
160
+# multi-line "for ... done" or "while ... done"
161
+/^[ ]*for[ ]..*[ ]in/bcontinue
162
+/^[ ]*while[ ]/bcontinue
163
+/^[ ]*do[ ]/bcontinue
164
+/^[ ]*do[ ]*$/bcontinue
165
+/;[ ]*do/bcontinue
166
+/^[ ]*done[ ]*&&[ ]*$/bdone
167
+/^[ ]*done[ ]*$/bdone
168
+/^[ ]*done[ ]*[<>|]/bdone
169
+/^[ ]*done[ ]*)/bdone
170
+/||[ ]*exit[ ]/bcontinue
171
+/||[ ]*exit[ ]*$/bcontinue
172
+# multi-line "if...elsif...else...fi"
173
+/^[ ]*if[ ]/bcontinue
174
+/^[ ]*then[ ]/bcontinue
175
+/^[ ]*then[ ]*$/bcontinue
176
+/;[ ]*then/bcontinue
177
+/^[ ]*elif[ ]/belse
178
+/^[ ]*elif[ ]*$/belse
179
+/^[ ]*else[ ]/belse
180
+/^[ ]*else[ ]*$/belse
181
+/^[ ]*fi[ ]*&&[ ]*$/bdone
182
+/^[ ]*fi[ ]*$/bdone
183
+/^[ ]*fi[ ]*[<>|]/bdone
184
+/^[ ]*fi[ ]*)/bdone
185
+# nested one-liner "(...) &&"
186
+/^[ ]*(.*)[ ]*&&[ ]*$/bcheckchain
187
+# nested one-liner "(...)"
188
+/^[ ]*(.*)[ ]*$/bcheckchain
189
+# nested one-liner "(...) >x" (or "2>x" or "<x" or "|x")
190
+/^[ ]*(.*)[ ]*[0-9]*[<>|]/bcheckchain
191
+# nested multi-line "(...\n...)"
192
+/^[ ]*(/bnest
193
+# multi-line "{...\n...}"
194
+/^[ ]*{/bblock
195
+# closing ")" on own line -- exit subshell
196
+/^[ ]*)/bclosesolo
197
+# "$((...))" -- arithmetic expansion; not closing ")"
198
+/\$(([^)][^)]*))[^)]*$/bcheckchain
199
+# "$(...)" -- command substitution; not closing ")"
200
+/\$([^)][^)]*)[^)]*$/bcheckchain
201
+# multi-line "$(...\n...)" -- command substitution; treat as nested subshell
202
+/\$([ ]*$/bnest
203
+# "=(...)" -- Bash array assignment; not closing ")"
204
+/=(/bcheckchain
205
+# closing "...) &&"
206
+/)[ ]*&&[ ]*$/bclose
207
+# closing "...)"
208
+/)[ ]*$/bclose
209
+# closing "...) >x" (or "2>x" or "<x" or "|x")
210
+/)[ ]*[<>|]/bclose
211
+:checkchain
212
+# mark suspect if line uses ";" internally rather than "&&" (but not ";" in a
213
+# string and not ";;" in one-liner "case...esac")
214
+/;/{
215
+ /;;/!{
216
+ /"[^"]*;[^"]*"/!s/^/?!SEMI?!/
217
+ }
218
+}
219
+# line ends with pipe "...|" -- valid; not missing "&&"
220
+/|[ ]*$/bcontinue
221
+# missing end-of-line "&&" -- mark suspect
222
+/&&[ ]*$/!s/^/?!AMP?!/
223
+:continue
224
+# retrieve and print previous line
225
+x
226
+n
227
+bslurp
228
+
229
+# found incomplete line "...\" -- slurp up next line
230
+:incomplete
231
+N
232
+s/\\\n//
233
+bslurp
234
+
235
+# found multi-line double-quoted string "...\n..." -- slurp until end of string
236
+:dqstring
237
+s/"//g
238
+N
239
+s/\n//
240
+/"/!bdqstring
241
+bcheckchain
242
+
243
+# found multi-line single-quoted string '...\n...' -- slurp until end of string
244
+:sqstring
245
+s/'//g
246
+N
247
+s/\n//
248
+/'/!bsqstring
249
+bcheckchain
250
+
251
+# found here-doc -- swallow it to avoid false hits within its body (but keep
252
+# the command to which it was attached); take care to handle here-docs nested
253
+# within here-docs by only recognizing closing tag matching outer here-doc
254
+# opening tag
255
+:heredoc
256
+/EOF/{ s/[ ]*<<[ ]*[-\\]*EOF//; s/^/EOF/; }
257
+/EOT/{ s/[ ]*<<[ ]*[-\\]*EOT//; s/^/EOT/; }
258
+/INPUT_END/{ s/[ ]*<<[ ]*[-\\]*INPUT_END//; s/^/INPUT_END/; }
259
+:hereslurpsub
260
+N
261
+/^EOF.*\n[ ]*EOF[ ]*$/bhereclose
262
+/^EOT.*\n[ ]*EOT[ ]*$/bhereclose
263
+/^INPUT_END.*\n[ ]*INPUT_END[ ]*$/bhereclose
264
+bhereslurpsub
265
+:hereclose
266
+s/^EOF//
267
+s/^EOT//
268
+s/^INPUT_END//
269
+s/\n.*$//
270
+bcheckchain
271
+
272
+# found "case ... in" -- pass through untouched
273
+:case
274
+x
275
+n
276
+/^[ ]*esac/bslurp
277
+bcase
278
+
279
+# found "else" or "elif" -- drop "suspect" from final line before "else" since
280
+# that line legitimately lacks "&&"
281
+:else
282
+x
283
+s/?!AMP?!//
284
+x
285
+bcontinue
286
+
287
+# found "done" closing for-loop or while-loop, or "fi" closing if-then -- drop
288
+# "suspect" from final contained line since that line legitimately lacks "&&"
289
+:done
290
+x
291
+s/?!AMP?!//
292
+x
293
+# is 'done' or 'fi' cuddled with ")" to close subshell?
294
+/done.*)/bclose
295
+/fi.*)/bclose
296
+bcheckchain
297
+
298
+# found nested multi-line "(...\n...)" -- pass through untouched
299
+:nest
300
+x
301
+:nestslurp
302
+n
303
+# closing ")" on own line -- stop nested slurp
304
+/^[ ]*)/bnestclose
305
+# comment -- not closing ")" if in comment
306
+/^[ ]*#/bnestcontinue
307
+# "$((...))" -- arithmetic expansion; not closing ")"
308
+/\$(([^)][^)]*))[^)]*$/bnestcontinue
309
+# "$(...)" -- command substitution; not closing ")"
310
+/\$([^)][^)]*)[^)]*$/bnestcontinue
311
+# closing "...)" -- stop nested slurp
312
+/)/bnestclose
313
+:nestcontinue
314
+x
315
+bnestslurp
316
+:nestclose
317
+s/^/>>/
318
+# is it "))" which closes nested and parent subshells?
319
+/)[ ]*)/bslurp
320
+bcheckchain
321
+
322
+# found multi-line "{...\n...}" block -- pass through untouched
323
+:block
324
+x
325
+n
326
+# closing "}" -- stop block slurp
327
+/}/bcheckchain
328
+bblock
329
+
330
+# found closing ")" on own line -- drop "suspect" from final line of subshell
331
+# since that line legitimately lacks "&&" and exit subshell loop
332
+:closesolo
333
+x
334
+s/?!AMP?!//
335
+p
336
+x
337
+s/^/>/
338
+b
339
+
340
+# found closing "...)" -- exit subshell loop
341
+:close
342
+x
343
+p
344
+x
345
+s/^/>/
346
+b