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