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