24
24
# in order to avoid misinterpreting the ")" in constructs such as "x=$(...)"
25
25
# and "case $x in *)" as ending the subshell.
26
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.
27
+ # Lines missing a final "&&" are flagged with "?!AMP?!", as are lines which
28
+ # chain commands with ";" internally rather than "&&". A line may be flagged
29
+ # for both violations.
30
30
#
31
31
# Detection of a missing &&-link in a multi-line subshell is complicated by the
32
32
# fact that the last statement before the closing ")" must not end with "&&".
47
47
# "?!AMP?!" violation is removed from the "bar" line (retrieved from the "hold"
48
48
# area) since the final statement of a subshell must not end with "&&". The
49
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) .
50
+ # to chain commands together rather than "&&", but an internal "?!AMP ?!" is
51
+ # never removed from a line even though a line-ending "?!AMP?!" might be.
52
52
#
53
53
# Care is taken to recognize the last _statement_ of a multi-line subshell, not
54
54
# necessarily the last textual _line_ within the subshell, since &&-chaining
62
62
# receives similar treatment.
63
63
#
64
64
# Swallowing here-docs with arbitrary tags requires a bit of finesse. When a
65
- # line such as "cat <<EOF >out " is seen, the here-doc tag is moved to the front
66
- # of the line enclosed in angle brackets as a sentinel, giving "<EOF>cat >out ".
65
+ # line such as "cat <<EOF" is seen, the here-doc tag is copied to the front of
66
+ # the line enclosed in angle brackets as a sentinel, giving "<EOF>cat <<EOF ".
67
67
# As each subsequent line is read, it is appended to the target line and a
68
68
# (whitespace-loose) back-reference match /^<(.*)>\n\1$/ is attempted to see if
69
69
# the content inside "<...>" matches the entirety of the newly-read line. For
70
70
# instance, if the next line read is "some data", when concatenated with the
71
- # target line, it becomes "<EOF>cat >out \nsome data", and a match is attempted
71
+ # target line, it becomes "<EOF>cat <<EOF \nsome data", and a match is attempted
72
72
# to see if "EOF" matches "some data". Since it doesn't, the next line is
73
73
# attempted. When a line consisting of only "EOF" (and possible whitespace) is
74
- # encountered, it is appended to the target line giving "<EOF>cat >out \nEOF",
74
+ # encountered, it is appended to the target line giving "<EOF>cat <<EOF \nEOF",
75
75
# in which case the "EOF" inside "<...>" does match the text following the
76
76
# newline, thus the closing here-doc tag has been found. The closing tag line
77
77
# and the "<...>" prefix on the target line are then discarded, leaving just
78
- # the target line "cat >out".
79
- #
80
- # To facilitate regression testing (and manual debugging), a ">" annotation is
81
- # applied to the line containing ")" which closes a subshell, ">>" to a line
82
- # closing a nested subshell, and ">>>" to a line closing both at once. This
83
- # makes it easy to detect whether the heuristics correctly identify
84
- # end-of-subshell.
78
+ # the target line "cat <<EOF".
85
79
# ------------------------------------------------------------------------------
86
80
87
81
# incomplete line -- slurp up next line
94
88
95
89
# here-doc -- swallow it to avoid false hits within its body (but keep the
96
90
# command to which it was attached)
97
- /<<[ ] * [- \\ '"] * [A-Za-z0-9_] / {
98
- s / ^ \( . * \) <<[ ] * [- \\ ' "]* \( [A-Za-z0-9_][A-Za-z0-9_] * \) ['"] * / < \2 > \1 << /
99
- s /[ ] * << / /
91
+ /<<- * [ ] * [\\ '"] * [A-Za-z0-9_] / {
92
+ / " [ ^ "] * <<[^ "] * " / b notdoc
93
+ s /^ \( . * <<- * [ ] * \) [ \\ '"] * \( [A-Za-z0-9_][A-Za-z0-9_] * \) ['"] * / < \2 > \1\2 /
100
94
: hered
101
95
N
102
96
/^ <\( [^ >] * \) >. * \n [ ] * \1 [ ] * $ /! {
106
100
s /^ <[^ >] * > //
107
101
s /\n . * $ //
108
102
}
103
+ : notdoc
109
104
110
105
# one-liner "(...) &&"
111
106
/^ [ ] * !* [ ] * ( .. * ) [ ] * &&[ ] * $ /b oneline
126
121
# "&&" (but not ";" in a string)
127
122
: oneline
128
123
/; /{
129
- /"[^ "] * ;[^ "] * " /! s /^ / ?!SEMI ?! /
124
+ /"[^ "] * ;[^ "] * " /! s /; / ; ?!AMP ?! /
130
125
}
131
126
b
132
127
136
131
h
137
132
b nextln
138
133
}
139
- # "(..." line -- split off and stash "(", then process "..." as its own line
134
+ # "(..." line -- "(" opening subshell cuddled with command; temporarily replace
135
+ # "(" with sentinel "^" and process the line as if "(" had been seen solo on
136
+ # the preceding line; this temporary replacement prevents several rules from
137
+ # accidentally thinking "(" introduces a nested subshell; "^" is changed back
138
+ # to "(" at output time
140
139
x
141
- s /. * /( /
140
+ s /. * //
142
141
x
143
- s /( //
142
+ s /( /^ /
144
143
b slurp
145
144
146
145
: nextln
@@ -157,8 +156,10 @@ s/.*\n//
157
156
/"[^ '"] * '[^ '"] * " /! b sqstr
158
157
}
159
158
: folded
160
- # here-doc -- swallow it
161
- /<<[ ] * [-\\ '"] * [A-Za-z0-9_] /b heredoc
159
+ # here-doc -- swallow it (but not "<<" in a string)
160
+ /<<-* [ ] * [\\ '"] * [A-Za-z0-9_] /{
161
+ /"[^ "] * <<[^ "] * " /! b heredoc
162
+ }
162
163
# comment or empty line -- discard since final non-comment, non-empty line
163
164
# before closing ")", "done", "elsif", "else", or "fi" will need to be
164
165
# re-visited to drop "suspect" marking since final line of those constructs
@@ -171,12 +172,12 @@ s/.*\n//
171
172
/"[^ "] * #[^ "] * " /! s /[ ] #. * $ //
172
173
}
173
174
# one-liner "case ... esac"
174
- /^ [ ] * case[ ] * .. * esac /b chkchn
175
+ /^ [ ^ ] * case[ ] * .. * esac /b chkchn
175
176
# multi-line "case ... esac"
176
- /^ [ ] * case[ ] .. * [ ] in /b case
177
+ /^ [ ^ ] * case[ ] .. * [ ] in /b case
177
178
# multi-line "for ... done" or "while ... done"
178
- /^ [ ] * for[ ] .. * [ ] in /b cont
179
- /^ [ ] * while[ ] /b cont
179
+ /^ [ ^ ] * for[ ] .. * [ ] in /b cont
180
+ /^ [ ^ ] * while[ ] /b cont
180
181
/^ [ ] * do[ ] /b cont
181
182
/^ [ ] * do[ ] * $ /b cont
182
183
/;[ ] * do /b cont
@@ -187,7 +188,7 @@ s/.*\n//
187
188
/|| [ ] * exit[ ] /b cont
188
189
/|| [ ] * exit[ ] * $ /b cont
189
190
# multi-line "if...elsif...else...fi"
190
- /^ [ ] * if[ ] /b cont
191
+ /^ [ ^ ] * if[ ] /b cont
191
192
/^ [ ] * then[ ] /b cont
192
193
/^ [ ] * then[ ] * $ /b cont
193
194
/;[ ] * then /b cont
@@ -200,15 +201,15 @@ s/.*\n//
200
201
/^ [ ] * fi[ ] * [<>|] /b done
201
202
/^ [ ] * fi[ ] * ) /b done
202
203
# nested one-liner "(...) &&"
203
- /^ [ ] * ( . * ) [ ] * &&[ ] * $ /b chkchn
204
+ /^ [ ^ ] * ( . * ) [ ] * &&[ ] * $ /b chkchn
204
205
# nested one-liner "(...)"
205
- /^ [ ] * ( . * ) [ ] * $ /b chkchn
206
+ /^ [ ^ ] * ( . * ) [ ] * $ /b chkchn
206
207
# nested one-liner "(...) >x" (or "2>x" or "<x" or "|x")
207
- /^ [ ] * ( . * ) [ ] * [0-9] * [<>|] /b chkchn
208
+ /^ [ ^ ] * ( . * ) [ ] * [0-9] * [<>|] /b chkchn
208
209
# nested multi-line "(...\n...)"
209
- /^ [ ] * ( /b nest
210
+ /^ [ ^ ] * ( /b nest
210
211
# multi-line "{...\n...}"
211
- /^ [ ] * { /b block
212
+ /^ [ ^ ] * { /b block
212
213
# closing ")" on own line -- exit subshell
213
214
/^ [ ] * ) /b clssolo
214
215
# "$((...))" -- arithmetic expansion; not closing ")"
@@ -230,16 +231,18 @@ s/.*\n//
230
231
# string and not ";;" in one-liner "case...esac")
231
232
/; /{
232
233
/;; /! {
233
- /"[^ "] * ;[^ "] * " /! s /^ / ?!SEMI ?! /
234
+ /"[^ "] * ;[^ "] * " /! s /; / ; ?!AMP ?! /
234
235
}
235
236
}
236
237
# line ends with pipe "...|" -- valid; not missing "&&"
237
238
/| [ ] * $ /b cont
238
239
# missing end-of-line "&&" -- mark suspect
239
- /&&[ ] * $ /! s /^ / ?!AMP?! /
240
+ /&&[ ] * $ /! s /$ / ?!AMP?! /
240
241
: cont
241
242
# retrieve and print previous line
242
243
x
244
+ s /^ \( [ ] * \) ^ /\1 ( /
245
+ s /? !HERE? ! /<< /g
243
246
n
244
247
b slurp
245
248
@@ -280,8 +283,7 @@ bfolded
280
283
# found here-doc -- swallow it to avoid false hits within its body (but keep
281
284
# the command to which it was attached)
282
285
: heredoc
283
- s /^ \( . * \) <<[ ] * [-\\ '"] * \( [A-Za-z0-9_][A-Za-z0-9_] * \) ['"] * /<\2 >\1 << /
284
- s /[ ] * << //
286
+ s /^ \( . * \) <<\( -* [ ] * \) [\\ '"] * \( [A-Za-z0-9_][A-Za-z0-9_] * \) ['"] * /<\3 >\1 ?!HERE?!\2\3 /
285
287
: hdocsub
286
288
N
287
289
/^ <\( [^ >] * \) >. * \n [ ] * \1 [ ] * $ /! {
@@ -295,23 +297,31 @@ bfolded
295
297
# found "case ... in" -- pass through untouched
296
298
: case
297
299
x
300
+ s /^ \( [ ] * \) ^ /\1 ( /
301
+ s /? !HERE? ! /<< /g
298
302
n
303
+ : cascom
304
+ /^ [ ] * # /{
305
+ N
306
+ s /. * \n //
307
+ b cascom
308
+ }
299
309
/^ [ ] * esac /b slurp
300
310
b case
301
311
302
312
# found "else" or "elif" -- drop "suspect" from final line before "else" since
303
313
# that line legitimately lacks "&&"
304
314
: else
305
315
x
306
- s /? !AMP? ! //
316
+ s /\( ? !AMP? !\) * ? !AMP ? ! $ //
307
317
x
308
318
b cont
309
319
310
320
# found "done" closing for-loop or while-loop, or "fi" closing if-then -- drop
311
321
# "suspect" from final contained line since that line legitimately lacks "&&"
312
322
: done
313
323
x
314
- s /? !AMP? ! //
324
+ s /\( ? !AMP? !\) * ? !AMP ? ! $ //
315
325
x
316
326
# is 'done' or 'fi' cuddled with ")" to close subshell?
317
327
/done. * ) /b close
@@ -322,11 +332,18 @@ bchkchn
322
332
: nest
323
333
x
324
334
: nstslrp
335
+ s /^ \( [ ] * \) ^ /\1 ( /
336
+ s /? !HERE? ! /<< /g
325
337
n
338
+ : nstcom
339
+ # comment -- not closing ")" if in comment
340
+ /^ [ ] * # /{
341
+ N
342
+ s /. * \n //
343
+ b nstcom
344
+ }
326
345
# closing ")" on own line -- stop nested slurp
327
346
/^ [ ] * ) /b nstcl
328
- # comment -- not closing ")" if in comment
329
- /^ [ ] * # /b nstcnt
330
347
# "$((...))" -- arithmetic expansion; not closing ")"
331
348
/\$ (( [^ )][^ )] * )) [^ )] * $ /b nstcnt
332
349
# "$(...)" -- command substitution; not closing ")"
337
354
x
338
355
b nstslrp
339
356
: nstcl
340
- s /^ />> /
341
357
# is it "))" which closes nested and parent subshells?
342
358
/) [ ] * ) /b slurp
343
359
b chkchn
344
360
345
361
# found multi-line "{...\n...}" block -- pass through untouched
346
362
: block
347
363
x
364
+ s /^ \( [ ] * \) ^ /\1 ( /
365
+ s /? !HERE? ! /<< /g
348
366
n
367
+ : blkcom
368
+ /^ [ ] * # /{
369
+ N
370
+ s /. * \n //
371
+ b blkcom
372
+ }
349
373
# closing "}" -- stop block slurp
350
374
/} /b chkchn
351
375
b block
@@ -354,16 +378,22 @@ bblock
354
378
# since that line legitimately lacks "&&" and exit subshell loop
355
379
: clssolo
356
380
x
357
- s /? !AMP? ! //
381
+ s /\( ? !AMP? !\) * ? !AMP? !$ //
382
+ s /^ \( [ ] * \) ^ /\1 ( /
383
+ s /? !HERE? ! /<< /g
358
384
p
359
385
x
360
- s /^ /> /
386
+ s /^ \( [ ] * \) ^ /\1 ( /
387
+ s /? !HERE? ! /<< /g
361
388
b
362
389
363
390
# found closing "...)" -- exit subshell loop
364
391
: close
365
392
x
393
+ s /^ \( [ ] * \) ^ /\1 ( /
394
+ s /? !HERE? ! /<< /g
366
395
p
367
396
x
368
- s /^ /> /
397
+ s /^ \( [ ] * \) ^ /\1 ( /
398
+ s /? !HERE? ! /<< /g
369
399
b
0 commit comments