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