1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 package script
51
52 import (
53 "bufio"
54 "context"
55 "errors"
56 "fmt"
57 "io"
58 "maps"
59 "slices"
60 "sort"
61 "strings"
62 "time"
63 )
64
65
66
67
68 type Engine struct {
69 Cmds map[string]Cmd
70 Conds map[string]Cond
71
72
73
74 Quiet bool
75 }
76
77
78 func NewEngine() *Engine {
79 return &Engine{
80 Cmds: DefaultCmds(),
81 Conds: DefaultConds(),
82 }
83 }
84
85
86 type Cmd interface {
87
88
89
90
91
92
93
94
95
96
97
98 Run(s *State, args ...string) (WaitFunc, error)
99
100
101 Usage() *CmdUsage
102 }
103
104
105 type WaitFunc func(*State) (stdout, stderr string, err error)
106
107
108
109 type CmdUsage struct {
110 Summary string
111 Args string
112 Detail []string
113
114
115
116 Async bool
117
118
119
120
121
122
123
124
125 RegexpArgs func(rawArgs ...string) []int
126 }
127
128
129 type Cond interface {
130
131
132
133
134
135 Eval(s *State, suffix string) (bool, error)
136
137
138 Usage() *CondUsage
139 }
140
141
142
143 type CondUsage struct {
144 Summary string
145
146
147
148
149 Prefix bool
150 }
151
152
153
154
155
156
157
158
159
160
161
162
163 func (e *Engine) Execute(s *State, file string, script *bufio.Reader, log io.Writer) (err error) {
164 defer func(prev *Engine) { s.engine = prev }(s.engine)
165 s.engine = e
166
167 var sectionStart time.Time
168
169
170 endSection := func(ok bool) error {
171 var err error
172 if sectionStart.IsZero() {
173
174
175 if s.log.Len() > 0 {
176 err = s.flushLog(log)
177 }
178 } else if s.log.Len() == 0 {
179
180 _, err = io.WriteString(log, "\n")
181 } else {
182
183 _, err = fmt.Fprintf(log, " (%.3fs)\n", time.Since(sectionStart).Seconds())
184
185 if err == nil && (!ok || !e.Quiet) {
186 err = s.flushLog(log)
187 } else {
188 s.log.Reset()
189 }
190 }
191
192 sectionStart = time.Time{}
193 return err
194 }
195
196 var lineno int
197 lineErr := func(err error) error {
198 if errors.As(err, new(*CommandError)) {
199 return err
200 }
201 return fmt.Errorf("%s:%d: %w", file, lineno, err)
202 }
203
204
205 defer func() {
206 if sErr := endSection(false); sErr != nil && err == nil {
207 err = lineErr(sErr)
208 }
209 }()
210
211 for {
212 if err := s.ctx.Err(); err != nil {
213
214
215 return lineErr(err)
216 }
217
218 line, err := script.ReadString('\n')
219 if err == io.EOF {
220 if line == "" {
221 break
222 }
223
224 } else if err != nil {
225 return lineErr(err)
226 }
227 line = strings.TrimSuffix(line, "\n")
228 lineno++
229
230
231
232 if strings.HasPrefix(line, "#") {
233
234
235
236
237
238
239 if err := endSection(true); err != nil {
240 return lineErr(err)
241 }
242
243
244
245 _, err = fmt.Fprintf(log, "%s", line)
246 sectionStart = time.Now()
247 if err != nil {
248 return lineErr(err)
249 }
250 continue
251 }
252
253 cmd, err := parse(file, lineno, line)
254 if cmd == nil && err == nil {
255 continue
256 }
257 s.Logf("> %s\n", line)
258 if err != nil {
259 return lineErr(err)
260 }
261
262
263 ok, err := e.conditionsActive(s, cmd.conds)
264 if err != nil {
265 return lineErr(err)
266 }
267 if !ok {
268 s.Logf("[condition not met]\n")
269 continue
270 }
271
272 impl := e.Cmds[cmd.name]
273
274
275 var regexpArgs []int
276 if impl != nil {
277 usage := impl.Usage()
278 if usage.RegexpArgs != nil {
279
280 rawArgs := make([]string, 0, len(cmd.rawArgs))
281 for _, frags := range cmd.rawArgs {
282 var b strings.Builder
283 for _, frag := range frags {
284 b.WriteString(frag.s)
285 }
286 rawArgs = append(rawArgs, b.String())
287 }
288 regexpArgs = usage.RegexpArgs(rawArgs...)
289 }
290 }
291 cmd.args = expandArgs(s, cmd.rawArgs, regexpArgs)
292
293
294 err = e.runCommand(s, cmd, impl)
295 if err != nil {
296 if stop := (stopError{}); errors.As(err, &stop) {
297
298
299 err = endSection(true)
300 s.Logf("%v\n", stop)
301 if err == nil {
302 return nil
303 }
304 }
305 return lineErr(err)
306 }
307 }
308
309 if err := endSection(true); err != nil {
310 return lineErr(err)
311 }
312 return nil
313 }
314
315
316 type command struct {
317 file string
318 line int
319 want expectedStatus
320 conds []condition
321 name string
322 rawArgs [][]argFragment
323 args []string
324 background bool
325 }
326
327
328
329 type expectedStatus string
330
331 const (
332 success expectedStatus = ""
333 failure expectedStatus = "!"
334 successOrFailure expectedStatus = "?"
335 )
336
337 type argFragment struct {
338 s string
339 quoted bool
340 }
341
342 type condition struct {
343 want bool
344 tag string
345 }
346
347 const argSepChars = " \t\r\n#"
348
349
350
351
352
353
354
355 func parse(filename string, lineno int, line string) (cmd *command, err error) {
356 cmd = &command{file: filename, line: lineno}
357 var (
358 rawArg []argFragment
359 start = -1
360 quoted = false
361 )
362
363 flushArg := func() error {
364 if len(rawArg) == 0 {
365 return nil
366 }
367 defer func() { rawArg = nil }()
368
369 if cmd.name == "" && len(rawArg) == 1 && !rawArg[0].quoted {
370 arg := rawArg[0].s
371
372
373
374
375 switch want := expectedStatus(arg); want {
376 case failure, successOrFailure:
377 if cmd.want != "" {
378 return errors.New("duplicated '!' or '?' token")
379 }
380 cmd.want = want
381 return nil
382 }
383
384
385 if strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]") {
386 want := true
387 arg = strings.TrimSpace(arg[1 : len(arg)-1])
388 if strings.HasPrefix(arg, "!") {
389 want = false
390 arg = strings.TrimSpace(arg[1:])
391 }
392 if arg == "" {
393 return errors.New("empty condition")
394 }
395 cmd.conds = append(cmd.conds, condition{want: want, tag: arg})
396 return nil
397 }
398
399 if arg == "" {
400 return errors.New("empty command")
401 }
402 cmd.name = arg
403 return nil
404 }
405
406 cmd.rawArgs = append(cmd.rawArgs, rawArg)
407 return nil
408 }
409
410 for i := 0; ; i++ {
411 if !quoted && (i >= len(line) || strings.ContainsRune(argSepChars, rune(line[i]))) {
412
413 if start >= 0 {
414 rawArg = append(rawArg, argFragment{s: line[start:i], quoted: false})
415 start = -1
416 }
417 if err := flushArg(); err != nil {
418 return nil, err
419 }
420 if i >= len(line) || line[i] == '#' {
421 break
422 }
423 continue
424 }
425 if i >= len(line) {
426 return nil, errors.New("unterminated quoted argument")
427 }
428 if line[i] == '\'' {
429 if !quoted {
430
431 if start >= 0 {
432 rawArg = append(rawArg, argFragment{s: line[start:i], quoted: false})
433 }
434 start = i + 1
435 quoted = true
436 continue
437 }
438
439 if i+1 < len(line) && line[i+1] == '\'' {
440 rawArg = append(rawArg, argFragment{s: line[start:i], quoted: true})
441 start = i + 1
442 i++
443 continue
444 }
445
446 rawArg = append(rawArg, argFragment{s: line[start:i], quoted: true})
447 start = i + 1
448 quoted = false
449 continue
450 }
451
452 if start < 0 {
453 start = i
454 }
455 }
456
457 if cmd.name == "" {
458 if cmd.want != "" || len(cmd.conds) > 0 || len(cmd.rawArgs) > 0 || cmd.background {
459
460 return nil, errors.New("missing command")
461 }
462
463
464 return nil, nil
465 }
466
467 if n := len(cmd.rawArgs); n > 0 {
468 last := cmd.rawArgs[n-1]
469 if len(last) == 1 && !last[0].quoted && last[0].s == "&" {
470 cmd.background = true
471 cmd.rawArgs = cmd.rawArgs[:n-1]
472 }
473 }
474 return cmd, nil
475 }
476
477
478
479 func expandArgs(s *State, rawArgs [][]argFragment, regexpArgs []int) []string {
480 args := make([]string, 0, len(rawArgs))
481 for i, frags := range rawArgs {
482 isRegexp := false
483 for _, j := range regexpArgs {
484 if i == j {
485 isRegexp = true
486 break
487 }
488 }
489
490 var b strings.Builder
491 for _, frag := range frags {
492 if frag.quoted {
493 b.WriteString(frag.s)
494 } else {
495 b.WriteString(s.ExpandEnv(frag.s, isRegexp))
496 }
497 }
498 args = append(args, b.String())
499 }
500 return args
501 }
502
503
504
505
506 func quoteArgs(args []string) string {
507 var b strings.Builder
508 for i, arg := range args {
509 if i > 0 {
510 b.WriteString(" ")
511 }
512 if strings.ContainsAny(arg, "'"+argSepChars) {
513
514 b.WriteString("'")
515 b.WriteString(strings.ReplaceAll(arg, "'", "''"))
516 b.WriteString("'")
517 } else {
518 b.WriteString(arg)
519 }
520 }
521 return b.String()
522 }
523
524 func (e *Engine) conditionsActive(s *State, conds []condition) (bool, error) {
525 for _, cond := range conds {
526 var impl Cond
527 prefix, suffix, ok := strings.Cut(cond.tag, ":")
528 if ok {
529 impl = e.Conds[prefix]
530 if impl == nil {
531 return false, fmt.Errorf("unknown condition prefix %q; known: %v", prefix, slices.Collect(maps.Keys(e.Conds)))
532 }
533 if !impl.Usage().Prefix {
534 return false, fmt.Errorf("condition %q cannot be used with a suffix", prefix)
535 }
536 } else {
537 impl = e.Conds[cond.tag]
538 if impl == nil {
539 return false, fmt.Errorf("unknown condition %q", cond.tag)
540 }
541 if impl.Usage().Prefix {
542 return false, fmt.Errorf("condition %q requires a suffix", cond.tag)
543 }
544 }
545 active, err := impl.Eval(s, suffix)
546
547 if err != nil {
548 return false, fmt.Errorf("evaluating condition %q: %w", cond.tag, err)
549 }
550 if active != cond.want {
551 return false, nil
552 }
553 }
554
555 return true, nil
556 }
557
558 func (e *Engine) runCommand(s *State, cmd *command, impl Cmd) error {
559 if impl == nil {
560 return cmdError(cmd, errors.New("unknown command"))
561 }
562
563 async := impl.Usage().Async
564 if cmd.background && !async {
565 return cmdError(cmd, errors.New("command cannot be run in background"))
566 }
567
568 wait, runErr := impl.Run(s, cmd.args...)
569 if wait == nil {
570 if async && runErr == nil {
571 return cmdError(cmd, errors.New("internal error: async command returned a nil WaitFunc"))
572 }
573 return checkStatus(cmd, runErr)
574 }
575 if runErr != nil {
576 return cmdError(cmd, errors.New("internal error: command returned both an error and a WaitFunc"))
577 }
578
579 if cmd.background {
580 s.background = append(s.background, backgroundCmd{
581 command: cmd,
582 wait: wait,
583 })
584
585
586 s.stdout = ""
587 s.stderr = ""
588 return nil
589 }
590
591 if wait != nil {
592 stdout, stderr, waitErr := wait(s)
593 s.stdout = stdout
594 s.stderr = stderr
595 if stdout != "" {
596 s.Logf("[stdout]\n%s", stdout)
597 }
598 if stderr != "" {
599 s.Logf("[stderr]\n%s", stderr)
600 }
601 if cmdErr := checkStatus(cmd, waitErr); cmdErr != nil {
602 return cmdErr
603 }
604 if waitErr != nil {
605
606 s.Logf("[%v]\n", waitErr)
607 }
608 }
609 return nil
610 }
611
612 func checkStatus(cmd *command, err error) error {
613 if err == nil {
614 if cmd.want == failure {
615 return cmdError(cmd, ErrUnexpectedSuccess)
616 }
617 return nil
618 }
619
620 if s := (stopError{}); errors.As(err, &s) {
621
622
623 return cmdError(cmd, err)
624 }
625
626 if w := (waitError{}); errors.As(err, &w) {
627
628
629
630
631
632 return cmdError(cmd, err)
633 }
634
635 if cmd.want == success {
636 return cmdError(cmd, err)
637 }
638
639 if cmd.want == failure && (errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled)) {
640
641
642
643
644 return cmdError(cmd, err)
645 }
646
647 return nil
648 }
649
650
651
652
653
654
655
656
657 func (e *Engine) ListCmds(w io.Writer, verbose bool, names ...string) error {
658 if names == nil {
659 names = make([]string, 0, len(e.Cmds))
660 for name := range e.Cmds {
661 names = append(names, name)
662 }
663 sort.Strings(names)
664 }
665
666 for _, name := range names {
667 cmd := e.Cmds[name]
668 usage := cmd.Usage()
669
670 suffix := ""
671 if usage.Async {
672 suffix = " [&]"
673 }
674
675 _, err := fmt.Fprintf(w, "%s %s%s\n\t%s\n", name, usage.Args, suffix, usage.Summary)
676 if err != nil {
677 return err
678 }
679
680 if verbose {
681 if _, err := io.WriteString(w, "\n"); err != nil {
682 return err
683 }
684 for _, line := range usage.Detail {
685 if err := wrapLine(w, line, 60, "\t"); err != nil {
686 return err
687 }
688 }
689 if _, err := io.WriteString(w, "\n"); err != nil {
690 return err
691 }
692 }
693 }
694
695 return nil
696 }
697
698 func wrapLine(w io.Writer, line string, cols int, indent string) error {
699 line = strings.TrimLeft(line, " ")
700 for len(line) > cols {
701 bestSpace := -1
702 for i, r := range line {
703 if r == ' ' {
704 if i <= cols || bestSpace < 0 {
705 bestSpace = i
706 }
707 if i > cols {
708 break
709 }
710 }
711 }
712 if bestSpace < 0 {
713 break
714 }
715
716 if _, err := fmt.Fprintf(w, "%s%s\n", indent, line[:bestSpace]); err != nil {
717 return err
718 }
719 line = line[bestSpace+1:]
720 }
721
722 _, err := fmt.Fprintf(w, "%s%s\n", indent, line)
723 return err
724 }
725
726
727
728
729
730
731
732
733
734 func (e *Engine) ListConds(w io.Writer, s *State, tags ...string) error {
735 if tags == nil {
736 tags = make([]string, 0, len(e.Conds))
737 for name := range e.Conds {
738 tags = append(tags, name)
739 }
740 sort.Strings(tags)
741 }
742
743 for _, tag := range tags {
744 if prefix, suffix, ok := strings.Cut(tag, ":"); ok {
745 cond := e.Conds[prefix]
746 if cond == nil {
747 return fmt.Errorf("unknown condition prefix %q", prefix)
748 }
749 usage := cond.Usage()
750 if !usage.Prefix {
751 return fmt.Errorf("condition %q cannot be used with a suffix", prefix)
752 }
753
754 activeStr := ""
755 if s != nil {
756 if active, _ := cond.Eval(s, suffix); active {
757 activeStr = " (active)"
758 }
759 }
760 _, err := fmt.Fprintf(w, "[%s]%s\n\t%s\n", tag, activeStr, usage.Summary)
761 if err != nil {
762 return err
763 }
764 continue
765 }
766
767 cond := e.Conds[tag]
768 if cond == nil {
769 return fmt.Errorf("unknown condition %q", tag)
770 }
771 var err error
772 usage := cond.Usage()
773 if usage.Prefix {
774 _, err = fmt.Fprintf(w, "[%s:*]\n\t%s\n", tag, usage.Summary)
775 } else {
776 activeStr := ""
777 if s != nil {
778 if ok, _ := cond.Eval(s, ""); ok {
779 activeStr = " (active)"
780 }
781 }
782 _, err = fmt.Fprintf(w, "[%s]%s\n\t%s\n", tag, activeStr, usage.Summary)
783 }
784 if err != nil {
785 return err
786 }
787 }
788
789 return nil
790 }
791
View as plain text