bash - ast_compound.go
1 package bash
2
3 import "vimagination.zapto.org/parser"
4
5 // Compound represents one of the Bash compound statements. One,
6 // and only of the compounds must be set.
7 type Compound struct {
8 IfCompound *IfCompound
9 CaseCompound *CaseCompound
10 LoopCompound *LoopCompound
11 ForCompound *ForCompound
12 SelectCompound *SelectCompound
13 GroupingCompound *GroupingCompound
14 TestCompound *TestCompound
15 ArithmeticCompound *ArithmeticExpansion
16 FunctionCompound *FunctionCompound
17 Redirections []Redirection
18 Tokens Tokens
19 }
20
21 func (cc *Compound) parse(b *bashParser) error {
22 var err error
23
24 c := b.NewGoal()
25
26 if c.Peek().Type == TokenFunctionIdentifier {
27 cc.FunctionCompound = new(FunctionCompound)
28 err = cc.FunctionCompound.parse(c)
29 } else {
30 switch c.Peek() {
31 case parser.Token{Type: TokenKeyword, Data: "if"}:
32 cc.IfCompound = new(IfCompound)
33 err = cc.IfCompound.parse(c)
34 case parser.Token{Type: TokenKeyword, Data: "case"}:
35 cc.CaseCompound = new(CaseCompound)
36 err = cc.CaseCompound.parse(c)
37 case parser.Token{Type: TokenKeyword, Data: "while"}, parser.Token{Type: TokenKeyword, Data: "until"}:
38 cc.LoopCompound = new(LoopCompound)
39 err = cc.LoopCompound.parse(c)
40 case parser.Token{Type: TokenKeyword, Data: "for"}:
41 cc.ForCompound = new(ForCompound)
42 err = cc.ForCompound.parse(c)
43 case parser.Token{Type: TokenKeyword, Data: "select"}:
44 cc.SelectCompound = new(SelectCompound)
45 err = cc.SelectCompound.parse(c)
46 case parser.Token{Type: TokenKeyword, Data: "function"}:
47 cc.FunctionCompound = new(FunctionCompound)
48 err = cc.FunctionCompound.parse(c)
49 case parser.Token{Type: TokenKeyword, Data: "[["}:
50 cc.TestCompound = new(TestCompound)
51 err = cc.TestCompound.parse(c)
52 case parser.Token{Type: TokenPunctuator, Data: "(("}:
53 cc.ArithmeticCompound = new(ArithmeticExpansion)
54 err = cc.ArithmeticCompound.parse(c)
55 case parser.Token{Type: TokenPunctuator, Data: "("}, parser.Token{Type: TokenPunctuator, Data: "{"}:
56 cc.GroupingCompound = new(GroupingCompound)
57 err = cc.GroupingCompound.parse(c)
58 }
59 }
60
61 if err != nil {
62 return b.Error("Compound", err)
63 }
64
65 b.Score(c)
66
67 c = b.NewGoal()
68
69 c.AcceptRunWhitespace()
70
71 for isRedirection(c) {
72 b.Score(c)
73
74 c = b.NewGoal()
75
76 var r Redirection
77
78 if err := r.parse(c); err != nil {
79 return b.Error("Compound", err)
80 }
81
82 cc.Redirections = append(cc.Redirections, r)
83
84 b.Score(c)
85
86 c = b.NewGoal()
87
88 c.AcceptRunWhitespace()
89 }
90
91 c = b.NewGoal()
92 c.AcceptRunWhitespace()
93
94 switch tk := c.Peek(); tk.Type {
95 case TokenLineTerminator, TokenComment, TokenKeyword, parser.TokenDone:
96 default:
97 switch tk {
98 case parser.Token{Type: TokenPunctuator, Data: ";"}, parser.Token{Type: TokenPunctuator, Data: "&"}, parser.Token{Type: TokenPunctuator, Data: ";;"}, parser.Token{Type: TokenPunctuator, Data: ";&"}, parser.Token{Type: TokenPunctuator, Data: ";;&"}, parser.Token{Type: TokenPunctuator, Data: "|"}, parser.Token{Type: TokenPunctuator, Data: "&&"}, parser.Token{Type: TokenPunctuator, Data: "||"}, parser.Token{Type: TokenPunctuator, Data: ")"}, parser.Token{Type: TokenPunctuator, Data: "}"}:
99 default:
100 return c.Error("Compound", ErrInvalidEndOfStatement)
101 }
102 }
103
104 cc.Tokens = b.ToTokens()
105
106 return nil
107 }
108
109 func (cc *Compound) isMultiline(v bool) bool {
110 if cc.IfCompound != nil || cc.CaseCompound != nil || cc.LoopCompound != nil || cc.ForCompound != nil || cc.SelectCompound != nil {
111 return true
112 } else if cc.GroupingCompound != nil && cc.GroupingCompound.isMultiline(v) {
113 return true
114 } else if cc.TestCompound != nil && cc.TestCompound.isMultiline(v) {
115 return true
116 } else if cc.ArithmeticCompound != nil && cc.ArithmeticCompound.isMultiline(v) {
117 return true
118 } else if cc.FunctionCompound != nil && cc.FunctionCompound.isMultiline(v) {
119 return true
120 }
121
122 for _, r := range cc.Redirections {
123 if r.isMultiline(v) {
124 return true
125 }
126 }
127
128 return false
129 }
130
131 func (cc *Compound) parseHeredocs(b *bashParser) error {
132 for n := range cc.Redirections {
133 c := b.NewGoal()
134
135 if err := cc.Redirections[n].parseHeredocs(c); err != nil {
136 return b.Error("Compound", err)
137 }
138
139 b.Score(c)
140 }
141
142 return nil
143 }
144
145 // IfCompound represents and if compound with optional elif and else sections.
146 type IfCompound struct {
147 If TestConsequence
148 ElIf []TestConsequence
149 Else *File
150 Tokens Tokens
151 }
152
153 func (i *IfCompound) parse(b *bashParser) error {
154 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "if"})
155 b.AcceptRunAllWhitespace()
156
157 c := b.NewGoal()
158
159 if err := i.If.parse(c); err != nil {
160 return b.Error("IfCompound", err)
161 }
162
163 b.Score(c)
164 b.AcceptRunAllWhitespace()
165
166 for b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "elif"}) {
167 b.AcceptRunAllWhitespace()
168
169 c := b.NewGoal()
170
171 var tc TestConsequence
172
173 if err := tc.parse(c); err != nil {
174 return b.Error("IfCompound", err)
175 }
176
177 i.ElIf = append(i.ElIf, tc)
178
179 b.Score(c)
180 b.AcceptRunAllWhitespace()
181 }
182
183 if b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "else"}) {
184 c := b.NewFileGoal()
185 i.Else = new(File)
186
187 if err := i.Else.parse(c); err != nil {
188 return b.Error("IfCompound", err)
189 }
190
191 b.Score(c)
192 b.AcceptRunAllWhitespace()
193 }
194
195 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "fi"})
196
197 i.Tokens = b.ToTokens()
198
199 return nil
200 }
201
202 // TestConsequence represents the conditional test and body of an if or elif
203 // section.
204 //
205 // The Consequence must contain at least one statement.
206 //
207 // The comments are parsed after the test statement, before the 'then' keyword.
208 type TestConsequence struct {
209 Test Statement
210 Consequence File
211 Comments Comments
212 Tokens
213 }
214
215 func (t *TestConsequence) parse(b *bashParser) error {
216 c := b.NewGoal()
217
218 if err := t.Test.parse(c, true); err != nil {
219 return b.Error("TestConsequence", err)
220 }
221
222 b.Score(c)
223
224 t.Comments = b.AcceptRunAllWhitespaceComments()
225
226 b.AcceptRunAllWhitespace()
227
228 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "then"})
229
230 c = b.NewFileGoal()
231
232 if err := t.Consequence.parse(c); err != nil {
233 return b.Error("TestConsequence", err)
234 }
235
236 b.Score(c)
237
238 t.Tokens = b.ToTokens()
239
240 return nil
241 }
242
243 // CaseCompound represents a case select compound.
244 //
245 // The first two comment groups represent comments on either side on the 'in'
246 // keyword, and the third group represents comments from just before the
247 // closing 'esac' keyword.
248 type CaseCompound struct {
249 Word Word
250 Matches []PatternLines
251 Comments [3]Comments
252 Tokens Tokens
253 }
254
255 func (cc *CaseCompound) parse(b *bashParser) error {
256 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "case"})
257 b.AcceptRunAllWhitespace()
258
259 c := b.NewGoal()
260
261 if err := cc.Word.parse(c, false); err != nil {
262 return b.Error("CaseCompound", err)
263 }
264
265 b.Score(c)
266
267 cc.Comments[0] = b.AcceptRunAllWhitespaceComments()
268
269 b.AcceptRunAllWhitespaceNoComments()
270
271 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "in"})
272
273 cc.Comments[1] = b.AcceptRunWhitespaceComments()
274
275 b.AcceptRunAllWhitespaceNoComments()
276
277 for {
278 c := b.NewGoal()
279
280 cc.Comments[2] = c.AcceptRunAllWhitespaceComments()
281
282 c.AcceptRunAllWhitespaceNoComments()
283
284 if c.AcceptToken(parser.Token{Type: TokenKeyword, Data: "esac"}) {
285 b.Score(c)
286
287 break
288 }
289
290 b.AcceptRunAllWhitespaceNoComments()
291
292 c = b.NewGoal()
293
294 var pl PatternLines
295
296 if err := pl.parse(c); err != nil {
297 return b.Error("CaseCompound", err)
298 }
299
300 cc.Matches = append(cc.Matches, pl)
301
302 b.Score(c)
303 }
304
305 cc.Tokens = b.ToTokens()
306
307 return nil
308 }
309
310 // CaseTerminationType represents the final punctuation of a case match.
311 //
312 // Must be one of CaseTerminationNone, CaseTerminationEnd,
313 // CaseTerminationContinue, or CaseTerminationFallthrough.
314 type CaseTerminationType uint8
315
316 // CaseTermination types.
317 const (
318 CaseTerminationNone CaseTerminationType = iota
319 CaseTerminationEnd
320 CaseTerminationContinue
321 CaseTerminationFallthrough
322 )
323
324 // PatternLines represents a CaseCompound pattern and the code to run for that
325 // match.
326 //
327 // The Comments are parsed from just before the pattern.
328 type PatternLines struct {
329 Patterns []Word
330 Lines File
331 CaseTerminationType
332 Comments Comments
333 Tokens Tokens
334 }
335
336 func (pl *PatternLines) parse(b *bashParser) error {
337 pl.Comments = b.AcceptRunWhitespaceComments()
338
339 b.AcceptRunAllWhitespaceNoComments()
340
341 for {
342 c := b.NewGoal()
343
344 var w Word
345
346 if err := w.parse(c, false); err != nil {
347 return b.Error("PatternLines", err)
348 }
349
350 pl.Patterns = append(pl.Patterns, w)
351
352 b.Score(c)
353 b.AcceptRunWhitespace()
354
355 if !b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "|"}) {
356 break
357 }
358 }
359
360 if !b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: ")"}) {
361 return b.Error("PatternLines", ErrMissingClosingPattern)
362 }
363
364 c := b.NewFileGoal()
365
366 if err := pl.Lines.parse(c); err != nil {
367 return b.Error("PatternLines", err)
368 }
369
370 b.Score(c)
371
372 c = b.NewGoal()
373
374 c.AcceptRunAllWhitespace()
375
376 if c.AcceptToken(parser.Token{Type: TokenPunctuator, Data: ";;"}) {
377 b.Score(c)
378
379 pl.CaseTerminationType = CaseTerminationEnd
380 } else if c.AcceptToken(parser.Token{Type: TokenPunctuator, Data: ";&"}) {
381 b.Score(c)
382
383 pl.CaseTerminationType = CaseTerminationContinue
384 } else if c.AcceptToken(parser.Token{Type: TokenPunctuator, Data: ";;&"}) {
385 b.Score(c)
386
387 pl.CaseTerminationType = CaseTerminationFallthrough
388 }
389
390 pl.Tokens = b.ToTokens()
391
392 return nil
393 }
394
395 // LoopCompound represents either While or Until loops.
396 //
397 // The File must contain at least one statement.
398 //
399 // The comments are parsed after statement, before the 'do' keyword.
400 type LoopCompound struct {
401 Until bool
402 Statement Statement
403 File File
404 Comments Comments
405 Tokens Tokens
406 }
407
408 func (l *LoopCompound) parse(b *bashParser) error {
409 if !b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "while"}) {
410 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "until"})
411
412 l.Until = true
413 }
414
415 b.AcceptRunWhitespace()
416
417 c := b.NewGoal()
418
419 if err := l.Statement.parse(c, true); err != nil {
420 return b.Error("LoopCompound", err)
421 }
422
423 b.Score(c)
424
425 l.Comments = b.AcceptRunAllWhitespaceComments()
426
427 b.AcceptRunAllWhitespaceNoComments()
428 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "do"})
429
430 c = b.NewFileGoal()
431
432 if err := l.File.parse(c); err != nil {
433 return b.Error("LoopCompound", err)
434 }
435
436 b.Score(c)
437 b.AcceptRunAllWhitespace()
438 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "done"})
439
440 l.Tokens = b.ToTokens()
441
442 return nil
443 }
444
445 // ForCompound represents a For loop.
446 //
447 // One, and only one, of Identifier and ArithmeticExpansion must be set.
448 //
449 // The File must contain at least one statement.
450 //
451 // The first set of comments are from after an Identifier; the second set of
452 // comments are from just before the 'do' keyword.
453 type ForCompound struct {
454 Identifier *Token
455 Words []Word
456 ArithmeticExpansion *ArithmeticExpansion
457 File File
458 Comments [2]Comments
459 Tokens Tokens
460 }
461
462 func (f *ForCompound) parse(b *bashParser) error {
463 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "for"})
464 b.AcceptRunWhitespace()
465
466 if b.Accept(TokenIdentifier) {
467 f.Identifier = b.GetLastToken()
468
469 f.Comments[0] = b.AcceptRunAllWhitespaceComments()
470
471 b.AcceptRunAllWhitespaceNoComments()
472
473 if b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "in"}) {
474 b.AcceptRunWhitespace()
475
476 f.Words = []Word{}
477
478 for {
479 if tk := b.Peek(); tk == (parser.Token{Type: TokenPunctuator, Data: ";"}) || tk.Type == TokenLineTerminator || tk.Type == TokenComment {
480 break
481 }
482
483 c := b.NewGoal()
484
485 var w Word
486
487 if err := w.parse(c, false); err != nil {
488 return b.Error("ForCompound", err)
489 }
490
491 f.Words = append(f.Words, w)
492
493 b.Score(c)
494 b.AcceptRunWhitespace()
495 }
496 }
497 } else {
498 c := b.NewGoal()
499 f.ArithmeticExpansion = new(ArithmeticExpansion)
500
501 if err := f.ArithmeticExpansion.parse(c); err != nil {
502 return b.Error("ForCompound", err)
503 }
504
505 b.Score(c)
506 }
507
508 if f.Comments[1] = b.AcceptRunAllWhitespaceComments(); len(f.Comments[1]) == 0 {
509 b.AcceptRunWhitespace()
510 b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: ";"})
511
512 f.Comments[1] = b.AcceptRunAllWhitespaceComments()
513 }
514
515 b.AcceptRunAllWhitespaceNoComments()
516 b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: ";"})
517 b.AcceptRunAllWhitespace()
518 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "do"})
519 b.AcceptRunAllWhitespace()
520
521 c := b.NewGoal()
522
523 if err := f.File.parse(c); err != nil {
524 return b.Error("ForCompound", err)
525 }
526
527 b.Score(c)
528 b.AcceptRunAllWhitespace()
529 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "done"})
530
531 f.Tokens = b.ToTokens()
532
533 return nil
534 }
535
536 // SelectCompound represents a Select loop.
537 //
538 // The Identifier must be set and the File must contain at least one statement.
539 //
540 // The first set of Comments is from just after the Identifier and the second
541 // are from just before the 'do' keyword.
542 type SelectCompound struct {
543 Identifier *Token
544 Words []Word
545 File File
546 Comments [2]Comments
547 Tokens Tokens
548 }
549
550 func (s *SelectCompound) parse(b *bashParser) error {
551 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "select"})
552 b.AcceptRunWhitespace()
553 b.Accept(TokenIdentifier)
554
555 s.Identifier = b.GetLastToken()
556
557 s.Comments[0] = b.AcceptRunAllWhitespaceComments()
558
559 b.AcceptRunAllWhitespaceNoComments()
560
561 if b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "in"}) {
562 b.AcceptRunWhitespace()
563
564 s.Words = []Word{}
565
566 for {
567 if tk := b.Peek(); tk == (parser.Token{Type: TokenPunctuator, Data: ";"}) || tk.Type == TokenLineTerminator || tk.Type == TokenComment {
568 break
569 }
570
571 c := b.NewGoal()
572
573 var w Word
574
575 if err := w.parse(c, false); err != nil {
576 return b.Error("SelectCompound", err)
577 }
578
579 s.Words = append(s.Words, w)
580
581 b.Score(c)
582 b.AcceptRunWhitespace()
583 }
584 }
585
586 b.AcceptRunWhitespace()
587 b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: ";"})
588
589 s.Comments[1] = b.AcceptRunAllWhitespaceComments()
590
591 b.AcceptRunAllWhitespaceNoComments()
592 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "do"})
593
594 c := b.NewFileGoal()
595
596 if err := s.File.parse(c); err != nil {
597 return b.Error("SelectCompound", err)
598 }
599
600 b.Score(c)
601 b.AcceptRunAllWhitespace()
602 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "done"})
603
604 s.Tokens = b.ToTokens()
605
606 return nil
607 }
608
609 // TestCompound represents the wrapping of a '[[ ... ]]' compound.
610 //
611 // The first set of comments are from just after the opening '[[' and the second
612 // set are from just before the closing ']]'.
613 type TestCompound struct {
614 Tests Tests
615 Comments [2]Comments
616 Tokens Tokens
617 }
618
619 func (t *TestCompound) parse(b *bashParser) error {
620 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "[["})
621
622 t.Comments[0] = b.AcceptRunWhitespaceComments()
623
624 b.AcceptRunAllWhitespaceNoComments()
625
626 c := b.NewGoal()
627
628 if err := t.Tests.parse(c); err != nil {
629 return b.Error("TestCompound", err)
630 }
631
632 b.Score(c)
633
634 t.Comments[1] = b.AcceptRunAllWhitespaceComments()
635
636 b.AcceptRunAllWhitespaceNoComments()
637 b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "]]"})
638
639 t.Tokens = b.ToTokens()
640
641 return nil
642 }
643
644 func (t *TestCompound) isMultiline(v bool) bool {
645 return len(t.Comments[0]) > 0 || len(t.Comments[1]) > 0 || t.Tests.isMultiline(v)
646 }
647
648 // TestOperator represents the type of test being represented.
649 type TestOperator uint8
650
651 const (
652 TestOperatorNone TestOperator = iota
653 TestOperatorFileExists
654 TestOperatorFileIsBlock
655 TestOperatorFileIsCharacter
656 TestOperatorDirectoryExists
657 TestOperatorFileIsRegular
658 TestOperatorFileHasSetGroupID
659 TestOperatorFileIsSymbolic
660 TestOperatorFileHasStickyBit
661 TestOperatorFileIsPipe
662 TestOperatorFileIsReadable
663 TestOperatorFileIsNonZero
664 TestOperatorFileIsTerminal
665 TestOperatorFileHasSetUserID
666 TestOperatorFileIsWritable
667 TestOperatorFileIsExecutable
668 TestOperatorFileIsOwnedByEffectiveGroup
669 TestOperatorFileWasModifiedSinceLastRead
670 TestOperatorFileIsOwnedByEffectiveUser
671 TestOperatorFileIsSocket
672 TestOperatorOptNameIsEnabled
673 TestOperatorVarNameIsSet
674 TestOperatorVarnameIsRef
675 TestOperatorStringIsZero
676 TestOperatorStringIsNonZero
677 TestOperatorStringsEqual
678 TestOperatorStringsMatch
679 TestOperatorStringsNotEqual
680 TestOperatorStringBefore
681 TestOperatorStringAfter
682 TestOperatorEqual
683 TestOperatorNotEqual
684 TestOperatorLessThan
685 TestOperatorLessThanEqual
686 TestOperatorGreaterThan
687 TestOperatorGreaterThanEqual
688 TestOperatorFilesAreSameInode
689 TestOperatorFileIsNewerThan
690 TestOperatorFileIsOlderThan
691 )
692
693 // Tests represents the actual test conditions of a TestCompound.
694 type Tests struct {
695 Not bool
696 Test TestOperator
697 Word *Word
698 Pattern *Pattern
699 Parens *Tests
700 LogicalOperator LogicalOperator
701 Tests *Tests
702 Comments [5]Comments
703 Tokens Tokens
704 }
705
706 func (t *Tests) parse(b *bashParser) error {
707 t.Comments[0] = b.AcceptRunAllWhitespaceComments()
708 b.AcceptRunAllWhitespaceNoComments()
709
710 if b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "!"}) {
711 t.Not = true
712
713 t.Comments[1] = b.AcceptRunAllWhitespaceComments()
714
715 b.AcceptRunAllWhitespaceNoComments()
716 }
717
718 if tk := b.Peek(); tk.Type == TokenKeyword {
719 switch tk.Data {
720 case "-a", "-e":
721 t.Test = TestOperatorFileExists
722 case "-b":
723 t.Test = TestOperatorFileIsBlock
724 case "-c":
725 t.Test = TestOperatorFileIsCharacter
726 case "-d":
727 t.Test = TestOperatorDirectoryExists
728 case "-f":
729 t.Test = TestOperatorFileIsRegular
730 case "-g":
731 t.Test = TestOperatorFileHasSetGroupID
732 case "-h", "-L":
733 t.Test = TestOperatorFileIsSymbolic
734 case "-k":
735 t.Test = TestOperatorFileHasStickyBit
736 case "-p":
737 t.Test = TestOperatorFileIsPipe
738 case "-r":
739 t.Test = TestOperatorFileIsReadable
740 case "-s":
741 t.Test = TestOperatorFileIsNonZero
742 case "-t":
743 t.Test = TestOperatorFileIsTerminal
744 case "-u":
745 t.Test = TestOperatorFileHasSetUserID
746 case "-w":
747 t.Test = TestOperatorFileIsWritable
748 case "-x":
749 t.Test = TestOperatorFileIsExecutable
750 case "-G":
751 t.Test = TestOperatorFileIsOwnedByEffectiveGroup
752 case "-N":
753 t.Test = TestOperatorFileWasModifiedSinceLastRead
754 case "-O":
755 t.Test = TestOperatorFileIsOwnedByEffectiveUser
756 case "-S":
757 t.Test = TestOperatorFileIsSocket
758 case "-o":
759 t.Test = TestOperatorOptNameIsEnabled
760 case "-v":
761 t.Test = TestOperatorVarNameIsSet
762 case "-R":
763 t.Test = TestOperatorVarnameIsRef
764 case "-z":
765 t.Test = TestOperatorStringIsZero
766 case "-n":
767 t.Test = TestOperatorStringIsNonZero
768 }
769
770 b.Next()
771 b.AcceptRunWhitespace()
772
773 c := b.NewGoal()
774 t.Word = new(Word)
775
776 if err := t.Word.parse(c, false); err != nil {
777 return b.Error("Tests", err)
778 }
779
780 b.Score(c)
781 } else if b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "("}) {
782 t.Comments[2] = b.AcceptRunWhitespaceComments()
783
784 b.AcceptRunAllWhitespaceNoComments()
785
786 c := b.NewGoal()
787 t.Parens = new(Tests)
788
789 if err := t.Parens.parse(c); err != nil {
790 return b.Error("Tests", err)
791 }
792
793 b.Score(c)
794
795 t.Comments[3] = b.AcceptRunAllWhitespaceComments()
796
797 b.AcceptRunAllWhitespaceNoComments()
798
799 if !b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: ")"}) {
800 return b.Error("Tests", ErrMissingClosingParen)
801 }
802 } else {
803 c := b.NewGoal()
804 t.Word = new(Word)
805
806 if err := t.Word.parse(c, false); err != nil {
807 return b.Error("Tests", err)
808 }
809
810 b.Score(c)
811
812 c = b.NewGoal()
813 c.AcceptRunWhitespace()
814
815 if tk := c.Peek(); tk.Type == TokenKeyword && tk.Data != "]]" {
816 b.Score(c)
817
818 switch tk.Data {
819 case "-ef":
820 t.Test = TestOperatorFilesAreSameInode
821 case "-nt":
822 t.Test = TestOperatorFileIsNewerThan
823 case "-ot":
824 t.Test = TestOperatorFileIsOlderThan
825 case "-eq":
826 t.Test = TestOperatorEqual
827 case "-ne":
828 t.Test = TestOperatorNotEqual
829 case "-lt":
830 t.Test = TestOperatorLessThan
831 case "-le":
832 t.Test = TestOperatorLessThanEqual
833 case "-gt":
834 t.Test = TestOperatorGreaterThan
835 case "-ge":
836 t.Test = TestOperatorGreaterThanEqual
837 }
838
839 b.Next()
840
841 b.AcceptRunWhitespace()
842
843 c = b.NewGoal()
844 t.Pattern = new(Pattern)
845
846 if err := t.Pattern.parse(c); err != nil {
847 return b.Error("Tests", err)
848 }
849
850 b.Score(c)
851 } else if tk.Type == TokenBinaryOperator {
852 b.Score(c)
853
854 switch tk.Data {
855 case "=", "==":
856 t.Test = TestOperatorStringsEqual
857 case "!=":
858 t.Test = TestOperatorStringsNotEqual
859 case "=~":
860 t.Test = TestOperatorStringsMatch
861 case "<":
862 t.Test = TestOperatorStringBefore
863 case ">":
864 t.Test = TestOperatorStringAfter
865 }
866
867 b.Next()
868
869 b.AcceptRunWhitespace()
870
871 c = b.NewGoal()
872 t.Pattern = new(Pattern)
873
874 if err := t.Pattern.parse(c); err != nil {
875 return b.Error("Tests", err)
876 }
877
878 b.Score(c)
879 }
880 }
881
882 c := b.NewGoal()
883
884 t.Comments[4] = c.AcceptRunAllWhitespaceComments()
885
886 c.AcceptRunAllWhitespaceNoComments()
887
888 if c.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "||"}) {
889 t.LogicalOperator = LogicalOperatorOr
890 } else if c.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "&&"}) {
891 t.LogicalOperator = LogicalOperatorAnd
892 }
893
894 if t.LogicalOperator != LogicalOperatorNone {
895 c.AcceptRunAllWhitespaceNoComments()
896 b.Score(c)
897
898 c = b.NewGoal()
899 t.Tests = new(Tests)
900
901 if err := t.Tests.parse(c); err != nil {
902 return b.Error("Tests", err)
903 }
904
905 b.Score(c)
906 } else {
907 t.Comments[4] = b.AcceptRunWhitespaceComments()
908 }
909
910 t.Tokens = b.ToTokens()
911
912 return nil
913 }
914
915 func (t *Tests) isMultiline(v bool) bool {
916 if len(t.Comments[0]) > 0 || len(t.Comments[4]) > 0 ||
917 t.Not && len(t.Comments[1]) > 0 ||
918 t.Parens != nil && (len(t.Comments[2]) > 0 || len(t.Comments[3]) > 0) ||
919 len(t.Comments[2]) > 0 && t.Word != nil && (t.Pattern != nil && t.Test >= TestOperatorStringsEqual || t.Test >= TestOperatorFileExists && t.Test <= TestOperatorVarnameIsRef) {
920 return true
921 }
922
923 if t.Parens != nil && t.Parens.isMultiline(v) {
924 return true
925 }
926
927 if t.Word != nil && t.Word.isMultiline(v) {
928 return true
929 }
930
931 if t.Pattern != nil && t.Pattern.isMultiline(v) {
932 return true
933 }
934
935 if t.Tests != nil {
936 return t.Tests.isMultiline(v)
937 }
938
939 return false
940 }
941
942 // Pattern represents a pattern being matched against in a TestCompound test.
943 //
944 // Must contain at least one WordPart.
945 type Pattern struct {
946 Parts []WordPart
947 Tokens Tokens
948 }
949
950 func (p *Pattern) parse(b *bashParser) error {
951 for nextIsPatternPart(b) {
952 c := b.NewGoal()
953
954 var pp WordPart
955
956 if err := pp.parse(c); err != nil {
957 return b.Error("Pattern", err)
958 }
959
960 p.Parts = append(p.Parts, pp)
961
962 b.Score(c)
963 }
964
965 p.Tokens = b.ToTokens()
966
967 return nil
968 }
969
970 func (p *Pattern) isMultiline(v bool) bool {
971 for _, pt := range p.Parts {
972 if pt.isMultiline(v) {
973 return true
974 }
975 }
976
977 return false
978 }
979
980 func nextIsPatternPart(b *bashParser) bool {
981 switch tk := b.Peek(); tk.Type {
982 case TokenWhitespace, TokenLineTerminator, TokenComment, TokenKeyword:
983 return false
984 case TokenPunctuator:
985 switch tk.Data {
986 case ")":
987 return false
988 }
989 }
990
991 return true
992 }
993
994 // GroupingCompound represents either a brace or parenthesized set of
995 // statements.
996 //
997 // File must contain at least one statement.
998 type GroupingCompound struct {
999 SubShell bool
1000 File
1001 Tokens Tokens
1002 }
1003
1004 func (g *GroupingCompound) parse(b *bashParser) error {
1005 if b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "("}) {
1006 g.SubShell = true
1007 } else {
1008 b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "{"})
1009 }
1010
1011 c := b.NewFileGoal()
1012
1013 if err := g.File.parse(c); err != nil {
1014 return b.Error("GroupingCompound", err)
1015 }
1016
1017 b.Score(c)
1018 b.AcceptRunAllWhitespace()
1019 b.Next()
1020
1021 g.Tokens = b.ToTokens()
1022
1023 return nil
1024 }
1025
1026 // Function compound represents a defined function, either with or without the
1027 // 'function' keyword.
1028 //
1029 // The Comments are from just before the Body.
1030 type FunctionCompound struct {
1031 HasKeyword bool
1032 Identifier *Token
1033 Body Compound
1034 Comments Comments
1035 Tokens Tokens
1036 }
1037
1038 func (f *FunctionCompound) parse(b *bashParser) error {
1039 if b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "function"}) {
1040 f.HasKeyword = true
1041
1042 b.AcceptRunWhitespace()
1043 }
1044
1045 b.Accept(TokenFunctionIdentifier)
1046
1047 f.Identifier = b.GetLastToken()
1048
1049 b.AcceptRunWhitespace()
1050
1051 if b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "("}) {
1052 b.AcceptRunWhitespace()
1053 b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: ")"})
1054 b.AcceptRunWhitespace()
1055 }
1056
1057 f.Comments = b.AcceptRunAllWhitespaceComments()
1058
1059 b.AcceptRunAllWhitespaceNoComments()
1060
1061 c := b.NewGoal()
1062
1063 if err := f.Body.parse(c); err != nil {
1064 return b.Error("FunctionCompound", err)
1065 }
1066
1067 b.Score(c)
1068
1069 f.Tokens = b.ToTokens()
1070
1071 return nil
1072 }
1073
1074 func (f *FunctionCompound) isMultiline(v bool) bool {
1075 return len(f.Comments) > 0 || f.Body.isMultiline(v)
1076 }
1077
1078 // SubstitutionType represents the type of a CommandSubstitution.
1079 type SubstitutionType uint8
1080
1081 // Substitution types.
1082 const (
1083 SubstitutionNew SubstitutionType = iota
1084 SubstitutionBacktick
1085 SubstitutionProcessInput
1086 SubstitutionProcessOutput
1087 )
1088
1089 // CommandSubstitution represents a subshell that returns some value.
1090 //
1091 // For a SubstitutionNew or SubstitutionBacktick, the Standard Out is returned;
1092 // for a SubstitutionProcessInput or SubstitutionProcessOutput a path is
1093 // return.
1094 //
1095 // For a SubstitutionBacktick, the Backtick must be set to the escaped backtick
1096 // being used for the subshell.
1097 //
1098 // The Command must contain at least one statement.
1099 type CommandSubstitution struct {
1100 SubstitutionType SubstitutionType
1101 Backtick *Token
1102 Command File
1103 Tokens Tokens
1104 }
1105
1106 func (cs *CommandSubstitution) parse(b *bashParser) error {
1107 end := parser.Token{Type: TokenPunctuator, Data: ")"}
1108
1109 if tk := b.Next(); tk.Type == TokenOpenBacktick {
1110 cs.SubstitutionType = SubstitutionBacktick
1111 end = parser.Token{Type: TokenCloseBacktick, Data: tk.Data}
1112 cs.Backtick = b.GetLastToken()
1113 } else if tk.Data == "<(" {
1114 cs.SubstitutionType = SubstitutionProcessInput
1115 } else if tk.Data == ">(" {
1116 cs.SubstitutionType = SubstitutionProcessOutput
1117 }
1118
1119 b.AcceptRunAllWhitespace()
1120
1121 c := b.NewGoal()
1122
1123 if err := cs.Command.parse(c); err != nil {
1124 return b.Error("CommandSubstitution", err)
1125 }
1126
1127 b.Score(c)
1128 b.AcceptRunAllWhitespace()
1129 b.AcceptToken(end)
1130
1131 cs.Tokens = b.ToTokens()
1132
1133 return nil
1134 }
1135
1136 func (cs *CommandSubstitution) isMultiline(v bool) bool {
1137 return cs.Command.isMultiline(v)
1138 }
1139
1140 // ArithmeticExpansion represents either an expression ('((') or a compound
1141 // ('$((').
1142 //
1143 // For the expression, the returned number is the exit code, for the compound
1144 // the returned value is a word.
1145 type ArithmeticExpansion struct {
1146 Expression bool
1147 WordsAndOperators []WordOrOperator
1148 Tokens Tokens
1149 }
1150
1151 func (a *ArithmeticExpansion) parse(b *bashParser) error {
1152 if !b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "$(("}) {
1153 b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "(("})
1154
1155 a.Expression = true
1156 }
1157
1158 b.AcceptRunAllWhitespace()
1159
1160 for !b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "))"}) {
1161 c := b.NewGoal()
1162
1163 var w WordOrOperator
1164
1165 if err := w.parse(c); err != nil {
1166 return b.Error("ArithmeticExpansion", err)
1167 }
1168
1169 a.WordsAndOperators = append(a.WordsAndOperators, w)
1170
1171 b.Score(c)
1172 b.AcceptRunAllWhitespace()
1173 }
1174
1175 a.Tokens = b.ToTokens()
1176
1177 return nil
1178 }
1179
1180 func (a *ArithmeticExpansion) isMultiline(v bool) bool {
1181 for _, w := range a.WordsAndOperators {
1182 if w.isMultiline(v) {
1183 return true
1184 }
1185 }
1186
1187 return false
1188 }
1189