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
497
498 func quoteArgs(args []string) string {
499 var b strings.Builder
500 for i, arg := range args {
501 if i > 0 {
502 b.WriteString(" ")
503 }
504 if strings.ContainsAny(arg, "'"+argSepChars) {
505
506 b.WriteString("'")
507 b.WriteString(strings.ReplaceAll(arg, "'", "''"))
508 b.WriteString("'")
509 } else {
510 b.WriteString(arg)
511 }
512 }
513 return b.String()
514 }
515
516 func (e *Engine) conditionsActive(s *State, conds []condition) (bool, error) {
517 for _, cond := range conds {
518 var impl Cond
519 prefix, suffix, ok := strings.Cut(cond.tag, ":")
520 if ok {
521 impl = e.Conds[prefix]
522 if impl == nil {
523 return false, fmt.Errorf("unknown condition prefix %q; known: %v", prefix, slices.Collect(maps.Keys(e.Conds)))
524 }
525 if !impl.Usage().Prefix {
526 return false, fmt.Errorf("condition %q cannot be used with a suffix", prefix)
527 }
528 } else {
529 impl = e.Conds[cond.tag]
530 if impl == nil {
531 return false, fmt.Errorf("unknown condition %q", cond.tag)
532 }
533 if impl.Usage().Prefix {
534 return false, fmt.Errorf("condition %q requires a suffix", cond.tag)
535 }
536 }
537 active, err := impl.Eval(s, suffix)
538
539 if err != nil {
540 return false, fmt.Errorf("evaluating condition %q: %w", cond.tag, err)
541 }
542 if active != cond.want {
543 return false, nil
544 }
545 }
546
547 return true, nil
548 }
549
550 func (e *Engine) runCommand(s *State, cmd *command, impl Cmd) error {
551 if impl == nil {
552 return cmdError(cmd, errors.New("unknown command"))
553 }
554
555 async := impl.Usage().Async
556 if cmd.background && !async {
557 return cmdError(cmd, errors.New("command cannot be run in background"))
558 }
559
560 wait, runErr := impl.Run(s, cmd.args...)
561 if wait == nil {
562 if async && runErr == nil {
563 return cmdError(cmd, errors.New("internal error: async command returned a nil WaitFunc"))
564 }
565 return checkStatus(cmd, runErr)
566 }
567 if runErr != nil {
568 return cmdError(cmd, errors.New("internal error: command returned both an error and a WaitFunc"))
569 }
570
571 if cmd.background {
572 s.background = append(s.background, backgroundCmd{
573 command: cmd,
574 wait: wait,
575 })
576
577
578 s.stdout = ""
579 s.stderr = ""
580 return nil
581 }
582
583 if wait != nil {
584 stdout, stderr, waitErr := wait(s)
585 s.stdout = stdout
586 s.stderr = stderr
587 if stdout != "" {
588 s.Logf("[stdout]\n%s", stdout)
589 }
590 if stderr != "" {
591 s.Logf("[stderr]\n%s", stderr)
592 }
593 if cmdErr := checkStatus(cmd, waitErr); cmdErr != nil {
594 return cmdErr
595 }
596 if waitErr != nil {
597
598 s.Logf("[%v]\n", waitErr)
599 }
600 }
601 return nil
602 }
603
604 func checkStatus(cmd *command, err error) error {
605 if err == nil {
606 if cmd.want == failure {
607 return cmdError(cmd, ErrUnexpectedSuccess)
608 }
609 return nil
610 }
611
612 if _, ok := errors.AsType[stopError](err); ok {
613
614
615 return cmdError(cmd, err)
616 }
617
618 if _, ok := errors.AsType[waitError](err); ok {
619
620
621
622
623
624 return cmdError(cmd, err)
625 }
626
627 if cmd.want == success {
628 return cmdError(cmd, err)
629 }
630
631 if cmd.want == failure && (errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled)) {
632
633
634
635
636 return cmdError(cmd, err)
637 }
638
639 return nil
640 }
641
642
643
644
645
646
647
648
649 func (e *Engine) ListCmds(w io.Writer, verbose bool, names ...string) error {
650 if names == nil {
651 names = make([]string, 0, len(e.Cmds))
652 for name := range e.Cmds {
653 names = append(names, name)
654 }
655 sort.Strings(names)
656 }
657
658 for _, name := range names {
659 cmd := e.Cmds[name]
660 usage := cmd.Usage()
661
662 suffix := ""
663 if usage.Async {
664 suffix = " [&]"
665 }
666
667 _, err := fmt.Fprintf(w, "%s %s%s\n\t%s\n", name, usage.Args, suffix, usage.Summary)
668 if err != nil {
669 return err
670 }
671
672 if verbose {
673 if _, err := io.WriteString(w, "\n"); err != nil {
674 return err
675 }
676 for _, line := range usage.Detail {
677 if err := wrapLine(w, line, 60, "\t"); err != nil {
678 return err
679 }
680 }
681 if _, err := io.WriteString(w, "\n"); err != nil {
682 return err
683 }
684 }
685 }
686
687 return nil
688 }
689
690 func wrapLine(w io.Writer, line string, cols int, indent string) error {
691 line = strings.TrimLeft(line, " ")
692 for len(line) > cols {
693 bestSpace := -1
694 for i, r := range line {
695 if r == ' ' {
696 if i <= cols || bestSpace < 0 {
697 bestSpace = i
698 }
699 if i > cols {
700 break
701 }
702 }
703 }
704 if bestSpace < 0 {
705 break
706 }
707
708 if _, err := fmt.Fprintf(w, "%s%s\n", indent, line[:bestSpace]); err != nil {
709 return err
710 }
711 line = line[bestSpace+1:]
712 }
713
714 _, err := fmt.Fprintf(w, "%s%s\n", indent, line)
715 return err
716 }
717
718
719
720
721
722
723
724
725
726 func (e *Engine) ListConds(w io.Writer, s *State, tags ...string) error {
727 if tags == nil {
728 tags = make([]string, 0, len(e.Conds))
729 for name := range e.Conds {
730 tags = append(tags, name)
731 }
732 sort.Strings(tags)
733 }
734
735 for _, tag := range tags {
736 if prefix, suffix, ok := strings.Cut(tag, ":"); ok {
737 cond := e.Conds[prefix]
738 if cond == nil {
739 return fmt.Errorf("unknown condition prefix %q", prefix)
740 }
741 usage := cond.Usage()
742 if !usage.Prefix {
743 return fmt.Errorf("condition %q cannot be used with a suffix", prefix)
744 }
745
746 activeStr := ""
747 if s != nil {
748 if active, _ := cond.Eval(s, suffix); active {
749 activeStr = " (active)"
750 }
751 }
752 _, err := fmt.Fprintf(w, "[%s]%s\n\t%s\n", tag, activeStr, usage.Summary)
753 if err != nil {
754 return err
755 }
756 continue
757 }
758
759 cond := e.Conds[tag]
760 if cond == nil {
761 return fmt.Errorf("unknown condition %q", tag)
762 }
763 var err error
764 usage := cond.Usage()
765 if usage.Prefix {
766 _, err = fmt.Fprintf(w, "[%s:*]\n\t%s\n", tag, usage.Summary)
767 } else {
768 activeStr := ""
769 if s != nil {
770 if ok, _ := cond.Eval(s, ""); ok {
771 activeStr = " (active)"
772 }
773 }
774 _, err = fmt.Fprintf(w, "[%s]%s\n\t%s\n", tag, activeStr, usage.Summary)
775 }
776 if err != nil {
777 return err
778 }
779 }
780
781 return nil
782 }
783
View as plain text