bash - ast_statement.go
1 package bash
2
3 import "vimagination.zapto.org/parser"
4
5 // LogicalOperator represents how two statements are joined.
6 type LogicalOperator uint8
7
8 // Logical Operators.
9 const (
10 LogicalOperatorNone LogicalOperator = iota
11 LogicalOperatorAnd
12 LogicalOperatorOr
13 )
14
15 // JobControl determines whether a job starts in the foreground or background.
16 type JobControl uint8
17
18 const (
19 JobControlForeground JobControl = iota
20 JobControlBackground
21 )
22
23 // Statement represents a statement or statements joined by '||' or '&&'
24 // operators.
25 //
26 // With a LogicalOperator set to either LogicalOperatorAnd or LogicalOperatorOr,
27 // the Statement must be set.
28 type Statement struct {
29 Pipeline Pipeline
30 LogicalOperator LogicalOperator
31 Statement *Statement
32 JobControl JobControl
33 Tokens
34 }
35
36 func (s *Statement) parse(b *bashParser, first bool) error {
37 c := b.NewGoal()
38
39 if err := s.Pipeline.parse(c); err != nil {
40 return b.Error("Statement", err)
41 }
42
43 b.Score(c)
44
45 c = b.NewGoal()
46
47 c.AcceptRunWhitespace()
48
49 if c.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "&&"}) {
50 s.LogicalOperator = LogicalOperatorAnd
51 } else if c.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "||"}) {
52 s.LogicalOperator = LogicalOperatorOr
53 }
54
55 if s.LogicalOperator != LogicalOperatorNone {
56 c.AcceptRunWhitespace()
57 b.Score(c)
58
59 c = b.NewGoal()
60 s.Statement = new(Statement)
61
62 if err := s.Statement.parse(c, false); err != nil {
63 return b.Error("Statement", err)
64 }
65
66 b.Score(c)
67 }
68
69 if first {
70 c = b.NewGoal()
71
72 c.AcceptRunWhitespace()
73
74 if c.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "&"}) {
75 s.JobControl = JobControlBackground
76
77 b.Score(c)
78 } else if c.AcceptToken(parser.Token{Type: TokenPunctuator, Data: ";"}) {
79 b.Score(c)
80 }
81 }
82
83 s.Tokens = b.ToTokens()
84
85 return nil
86 }
87
88 func (s *Statement) isMultiline(v bool) bool {
89 if s.Pipeline.isMultiline(v) {
90 return true
91 } else if s.Statement != nil {
92 return s.Statement.isMultiline(v)
93 }
94
95 return false
96 }
97
98 func (s *Statement) parseHeredocs(b *bashParser) error {
99 c := b.NewGoal()
100
101 if err := s.Pipeline.parseHeredocs(c); err != nil {
102 return b.Error("Statement", err)
103 }
104
105 b.Score(c)
106
107 if s.Statement != nil {
108 c = b.NewGoal()
109
110 if err := s.Statement.parseHeredocs(c); err != nil {
111 return b.Error("Statement", err)
112 }
113
114 b.Score(c)
115 }
116
117 return nil
118 }
119
120 // PipelineTime represents a potential 'time' keyword prefixed to a pipeline.
121 type PipelineTime uint8
122
123 // Pipeline Time options.
124 const (
125 PipelineTimeNone PipelineTime = iota
126 PipelineTimeBash
127 PipelineTimePosix
128 )
129
130 // Pipeline represents a command or compound, possibly connected to another
131 // pipeline by a pipe ('|').
132 type Pipeline struct {
133 PipelineTime PipelineTime
134 Not bool
135 Coproc bool
136 CoprocIdentifier *Token
137 CommandOrCompound CommandOrCompound
138 Pipeline *Pipeline
139 Tokens Tokens
140 }
141
142 func (p *Pipeline) parse(b *bashParser) error {
143 if b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "time"}) {
144 b.AcceptRunWhitespace()
145
146 if b.AcceptToken(parser.Token{Type: TokenWord, Data: "-p"}) {
147 p.PipelineTime = PipelineTimePosix
148 } else {
149 p.PipelineTime = PipelineTimeBash
150 }
151
152 b.AcceptRunWhitespace()
153 }
154
155 if p.Not = b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "!"}); p.Not {
156 b.AcceptRunWhitespace()
157 }
158
159 if b.AcceptToken(parser.Token{Type: TokenKeyword, Data: "coproc"}) {
160 p.Coproc = true
161
162 b.AcceptRunWhitespace()
163
164 if b.Accept(TokenIdentifier) {
165 p.CoprocIdentifier = b.GetLastToken()
166
167 b.AcceptRunWhitespace()
168 }
169 }
170
171 c := b.NewGoal()
172
173 if err := p.CommandOrCompound.parse(c); err != nil {
174 return b.Error("Pipeline", err)
175 }
176
177 b.Score(c)
178
179 c = b.NewGoal()
180
181 c.AcceptRunWhitespace()
182
183 if c.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "|"}) {
184 c.AcceptRunWhitespace()
185 b.Score(c)
186
187 c = b.NewGoal()
188 p.Pipeline = new(Pipeline)
189
190 if err := p.Pipeline.parse(c); err != nil {
191 return b.Error("Pipeline", err)
192 }
193
194 b.Score(c)
195 }
196
197 p.Tokens = b.ToTokens()
198
199 return nil
200 }
201
202 func (p *Pipeline) isMultiline(v bool) bool {
203 if p.CommandOrCompound.isMultiline(v) {
204 return true
205 } else if p.Pipeline != nil {
206 return p.Pipeline.isMultiline(v)
207 }
208
209 return false
210 }
211
212 func (p *Pipeline) parseHeredocs(b *bashParser) error {
213 c := b.NewGoal()
214
215 if err := p.CommandOrCompound.parseHeredoc(c); err != nil {
216 return b.Error("Pipeline", err)
217 }
218
219 b.Score(c)
220
221 if p.Pipeline != nil {
222 c = b.NewGoal()
223
224 if err := p.Pipeline.parseHeredocs(c); err != nil {
225 return b.Error("Pipeline", err)
226 }
227
228 b.Score(c)
229 }
230
231 return nil
232 }
233
234 // CommandOrCompound represents either a Command or a Compound; one, and only
235 // one of which must be set.
236 type CommandOrCompound struct {
237 Command *Command
238 Compound *Compound
239 Tokens Tokens
240 }
241
242 func (cc *CommandOrCompound) parse(b *bashParser) error {
243 var err error
244
245 c := b.NewGoal()
246
247 if isCompoundNext(b) {
248 cc.Compound = new(Compound)
249 err = cc.Compound.parse(c)
250 } else {
251 cc.Command = new(Command)
252 err = cc.Command.parse(c)
253 }
254
255 if err != nil {
256 return b.Error("CommandOrCompound", err)
257 }
258
259 b.Score(c)
260
261 cc.Tokens = b.ToTokens()
262
263 return nil
264 }
265
266 func (cc *CommandOrCompound) isMultiline(v bool) bool {
267 return cc.Command != nil && cc.Command.isMultiline(v) || cc.Compound != nil && cc.Compound.isMultiline(v)
268 }
269
270 func (cc *CommandOrCompound) parseHeredoc(b *bashParser) error {
271 var err error
272
273 c := b.NewGoal()
274
275 if cc.Command != nil {
276 err = cc.Command.parseHeredocs(c)
277 } else if cc.Compound != nil {
278 err = cc.Compound.parseHeredocs(c)
279 }
280
281 if err != nil {
282 return b.Error("CommandOrCompound", err)
283 }
284
285 b.Score(c)
286
287 return nil
288 }
289
290 func isCompoundNext(b *bashParser) bool {
291 tk := b.Peek()
292
293 return tk.Type == TokenKeyword && (tk.Data == "function" || tk.Data == "if" || tk.Data == "case" || tk.Data == "while" || tk.Data == "for" || tk.Data == "until" || tk.Data == "select" || tk.Data == "[[") || tk.Type == TokenPunctuator && (tk.Data == "((" || tk.Data == "(" || tk.Data == "{") || tk.Type == TokenFunctionIdentifier
294 }
295
296 // Command represents an assignment or a call to a command or builtin.
297 //
298 // At least one Var, Redirection, or Word must be set.
299 type Command struct {
300 Vars []Assignment
301 Redirections []Redirection
302 AssignmentsOrWords []AssignmentOrWord
303 Tokens Tokens
304 }
305
306 func (cc *Command) parse(b *bashParser) error {
307 for {
308 c := b.NewGoal()
309
310 if b.Peek().Type == TokenIdentifierAssign {
311 var a Assignment
312
313 if err := a.parse(c); err != nil {
314 return b.Error("Command", err)
315 }
316
317 cc.Vars = append(cc.Vars, a)
318 } else if isRedirection(b) {
319 var r Redirection
320
321 if err := r.parse(c); err != nil {
322 return b.Error("Command", err)
323 }
324
325 cc.Redirections = append(cc.Redirections, r)
326 } else {
327 break
328 }
329
330 b.Score(c)
331 b.AcceptRunWhitespace()
332 }
333
334 c := b.NewGoal()
335
336 for nextIsWordPart(c) || isRedirection(c) {
337 b.Score(c)
338 c = b.NewGoal()
339
340 if isRedirection(b) {
341 var r Redirection
342
343 if err := r.parse(c); err != nil {
344 return b.Error("Command", err)
345 }
346
347 cc.Redirections = append(cc.Redirections, r)
348 } else {
349 var a AssignmentOrWord
350
351 if err := a.parse(c); err != nil {
352 return b.Error("Command", err)
353 }
354
355 cc.AssignmentsOrWords = append(cc.AssignmentsOrWords, a)
356 }
357
358 b.Score(c)
359
360 c = b.NewGoal()
361
362 c.AcceptRunWhitespace()
363 }
364
365 if len(cc.AssignmentsOrWords) == 0 && len(cc.Redirections) == 0 && len(cc.Vars) == 0 {
366 return b.Error("Command", ErrMissingWord)
367 }
368
369 cc.Tokens = b.ToTokens()
370
371 return nil
372 }
373
374 func (cc *Command) isMultiline(v bool) bool {
375 for _, vs := range cc.Vars {
376 if vs.isMultiline(v) {
377 return true
378 }
379 }
380
381 for _, r := range cc.Redirections {
382 if r.isMultiline(v) {
383 return true
384 }
385 }
386
387 for _, a := range cc.AssignmentsOrWords {
388 if a.isMultiline(v) {
389 return true
390 }
391 }
392
393 return false
394 }
395
396 func (cc *Command) parseHeredocs(b *bashParser) error {
397 for n := range cc.Redirections {
398 c := b.NewGoal()
399
400 if err := cc.Redirections[n].parseHeredocs(c); err != nil {
401 return b.Error("Command", err)
402 }
403
404 b.Score(c)
405 }
406
407 return nil
408 }
409
410 func isRedirection(b *bashParser) bool {
411 c := b.NewGoal()
412
413 if c.Accept(TokenNumberLiteral, TokenBraceWord) {
414 if c.Accept(TokenPunctuator) {
415 switch c.GetLastToken().Data {
416 case "<", ">", ">|", ">>", "<<", "<<-", "<<<", "<&", ">&", "<>":
417 return true
418 }
419 }
420 } else if c.Accept(TokenPunctuator) {
421 switch c.GetLastToken().Data {
422 case "<", ">", ">|", ">>", "<<", "<<-", "<<<", "<&", ">&", "<>", "&>", "&>>":
423 return true
424 }
425 }
426
427 return false
428 }
429
430 // AssignmentType represents the type of assignment, either a simple set or and
431 // append.
432 type AssignmentType uint8
433
434 // Assignment types.
435 const (
436 AssignmentAssign AssignmentType = iota
437 AssignmentAppend
438 )
439
440 // Assignment represents a value assignment.
441 //
442 // If Assignment is AssignmentAppend, Expression should be used, otherwise
443 // Value should be set.
444 type Assignment struct {
445 Identifier ParameterAssign
446 Assignment AssignmentType
447 Expression []WordOrOperator
448 Value *Value
449 Tokens Tokens
450 }
451
452 func (a *Assignment) parse(b *bashParser) error {
453 c := b.NewGoal()
454
455 isLet := b.Peek().Type == TokenLetIdentifierAssign
456
457 if err := a.Identifier.parse(c); err != nil {
458 return b.Error("Assignment", err)
459 }
460
461 b.Score(c)
462
463 if b.AcceptToken(parser.Token{Type: TokenAssignment, Data: "="}) {
464 a.Assignment = AssignmentAssign
465 } else if b.AcceptToken(parser.Token{Type: TokenAssignment, Data: "+="}) {
466 a.Assignment = AssignmentAppend
467 }
468
469 if isLet {
470 parens := 0
471
472 for {
473 if tk := b.Peek(); parens == 0 && (tk.Type == TokenWhitespace || tk.Type == TokenLineTerminator || tk.Type == TokenComment || tk == (parser.Token{Type: TokenPunctuator, Data: ";"}) || isEnd(tk)) {
474 break
475 } else if tk == (parser.Token{Type: TokenPunctuator, Data: "("}) {
476 parens++
477 } else if tk == (parser.Token{Type: TokenPunctuator, Data: ")"}) {
478 parens--
479 }
480
481 c := b.NewGoal()
482
483 var w WordOrOperator
484
485 if err := w.parse(c); err != nil {
486 return b.Error("Assignment", err)
487 }
488
489 a.Expression = append(a.Expression, w)
490
491 b.Score(c)
492
493 if parens > 0 {
494 b.AcceptRunWhitespace()
495 }
496 }
497 } else {
498 c := b.NewGoal()
499
500 a.Value = new(Value)
501 if err := a.Value.parse(c); err != nil {
502 return b.Error("Assignment", err)
503 }
504
505 b.Score(c)
506 }
507
508 a.Tokens = b.ToTokens()
509
510 return nil
511 }
512
513 func (a *Assignment) isMultiline(v bool) bool {
514 if a.Identifier.isMultiline(v) {
515 return true
516 }
517
518 if a.Value != nil {
519 return a.Value.isMultiline(v)
520 }
521
522 for _, wo := range a.Expression {
523 if wo.isMultiline(v) {
524 return true
525 }
526 }
527
528 return false
529 }
530
531 // ParameterAssign represents an identifier being assigned to, with a possible
532 // subscript.
533 //
534 // Identifier must be set.
535 type ParameterAssign struct {
536 Identifier *Token
537 Subscript []WordOrOperator
538 Tokens Tokens
539 }
540
541 func (p *ParameterAssign) parse(b *bashParser) error {
542 b.Next()
543
544 p.Identifier = b.GetLastToken()
545
546 if b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "["}) {
547 b.AcceptRunWhitespace()
548
549 for !b.AcceptToken(parser.Token{Type: TokenPunctuator, Data: "]"}) {
550 c := b.NewGoal()
551
552 var w WordOrOperator
553
554 if err := w.parse(c); err != nil {
555 return b.Error("ParameterAssign", err)
556 }
557
558 p.Subscript = append(p.Subscript, w)
559
560 b.Score(c)
561 b.AcceptRunAllWhitespace()
562 }
563 }
564
565 p.Tokens = b.ToTokens()
566
567 return nil
568 }
569
570 func (p *ParameterAssign) isMultiline(v bool) bool {
571 for _, wo := range p.Subscript {
572 if wo.isMultiline(v) {
573 return true
574 }
575 }
576
577 return false
578 }
579
580 // Redirection presents input/output redirection.
581 //
582 // Redirector must be set to the redirection operator.
583 type Redirection struct {
584 Input *Token
585 Redirector *Token
586 Output Word
587 Heredoc *Heredoc
588 Tokens Tokens
589 }
590
591 func (r *Redirection) parse(b *bashParser) error {
592 if b.Accept(TokenNumberLiteral, TokenBraceWord) {
593 r.Input = b.GetLastToken()
594 }
595
596 b.Accept(TokenPunctuator)
597
598 r.Redirector = b.GetLastToken()
599
600 b.AcceptRunWhitespace()
601
602 c := b.NewGoal()
603
604 if err := r.Output.parse(c, false); err != nil {
605 return b.Error("Redirection", err)
606 }
607
608 b.Score(c)
609
610 r.Tokens = b.ToTokens()
611
612 return nil
613 }
614
615 func (r *Redirection) isMultiline(v bool) bool {
616 return r.Heredoc != nil || r.Output.isMultiline(v)
617 }
618
619 func (r *Redirection) isHeredoc() bool {
620 return r.Redirector != nil && (r.Redirector.Data == "<<" || r.Redirector.Data == "<<-")
621 }
622
623 func (r *Redirection) parseHeredocs(b *bashParser) error {
624 if !r.isHeredoc() {
625 return nil
626 }
627
628 b.AcceptRunWhitespace()
629
630 c := b.NewGoal()
631 r.Heredoc = new(Heredoc)
632
633 if err := r.Heredoc.parse(c); err != nil {
634 return b.Error("Redirection", err)
635 }
636
637 b.Score(c)
638
639 return nil
640 }
641
642 // Heredoc represents the parts of a Here Document.
643 type Heredoc struct {
644 HeredocPartsOrWords []HeredocPartOrWord
645 Tokens Tokens
646 }
647
648 func (h *Heredoc) parse(b *bashParser) error {
649 b.Accept(TokenHeredocIndent)
650
651 for !b.Accept(TokenHeredocEnd) {
652 c := b.NewGoal()
653
654 var hw HeredocPartOrWord
655
656 if err := hw.parse(c); err != nil {
657 return b.Error("Heredoc", err)
658 }
659
660 h.HeredocPartsOrWords = append(h.HeredocPartsOrWords, hw)
661
662 b.Score(c)
663 b.Accept(TokenHeredocIndent)
664 }
665
666 h.Tokens = b.ToTokens()
667
668 return nil
669 }
670
671 // HeredocPartOrWord represents either the string of Word part of a Here
672 // Document.
673 //
674 // One of HeredocPart or Word must be set.
675 type HeredocPartOrWord struct {
676 HeredocPart *Token
677 Word *Word
678 Tokens Tokens
679 }
680
681 func (h *HeredocPartOrWord) parse(b *bashParser) error {
682 if b.Accept(TokenHeredoc) {
683 h.HeredocPart = b.GetLastToken()
684 } else {
685 c := b.NewGoal()
686
687 h.Word = new(Word)
688
689 if err := h.Word.parse(c, false); err != nil {
690 return b.Error("HeredocPartOrWord", err)
691 }
692
693 b.Score(c)
694 }
695
696 h.Tokens = b.ToTokens()
697
698 return nil
699 }
700