1
2
3
4
5 package script
6
7 import (
8 "cmd/internal/pathcache"
9 "cmd/internal/robustio"
10 "errors"
11 "fmt"
12 "internal/diff"
13 "io/fs"
14 "os"
15 "os/exec"
16 "path/filepath"
17 "regexp"
18 "runtime"
19 "strconv"
20 "strings"
21 "sync"
22 "time"
23 )
24
25
26
27
28
29 func DefaultCmds() map[string]Cmd {
30 return map[string]Cmd{
31 "cat": Cat(),
32 "cd": Cd(),
33 "chmod": Chmod(),
34 "cmp": Cmp(),
35 "cmpenv": Cmpenv(),
36 "cp": Cp(),
37 "echo": Echo(),
38 "env": Env(),
39 "exec": Exec(func(cmd *exec.Cmd) error { return InterruptCmd(cmd) }, 100*time.Millisecond),
40 "exists": Exists(),
41 "grep": Grep(),
42 "help": Help(),
43 "mkdir": Mkdir(),
44 "mv": Mv(),
45 "rm": Rm(),
46 "replace": Replace(),
47 "sleep": Sleep(),
48 "stderr": Stderr(),
49 "stdout": Stdout(),
50 "stop": Stop(),
51 "symlink": Symlink(),
52 "wait": Wait(),
53 }
54 }
55
56
57
58
59 func InterruptCmd(cmd *exec.Cmd) error {
60 if runtime.GOOS == "windows" {
61
62 return cmd.Process.Kill()
63 }
64
65
66 return cmd.Process.Signal(os.Interrupt)
67 }
68
69
70
71 func Command(usage CmdUsage, run func(*State, ...string) (WaitFunc, error)) Cmd {
72 return &funcCmd{
73 usage: usage,
74 run: run,
75 }
76 }
77
78
79 type funcCmd struct {
80 usage CmdUsage
81 run func(*State, ...string) (WaitFunc, error)
82 }
83
84 func (c *funcCmd) Run(s *State, args ...string) (WaitFunc, error) {
85 return c.run(s, args...)
86 }
87
88 func (c *funcCmd) Usage() *CmdUsage { return &c.usage }
89
90
91
92 func firstNonFlag(rawArgs ...string) []int {
93 for i, arg := range rawArgs {
94 if !strings.HasPrefix(arg, "-") {
95 return []int{i}
96 }
97 if arg == "--" {
98 return []int{i + 1}
99 }
100 }
101 return nil
102 }
103
104
105
106 func Cat() Cmd {
107 return Command(
108 CmdUsage{
109 Summary: "concatenate files and print to the script's stdout buffer",
110 Args: "files...",
111 },
112 func(s *State, args ...string) (WaitFunc, error) {
113 if len(args) == 0 {
114 return nil, ErrUsage
115 }
116
117 paths := make([]string, 0, len(args))
118 for _, arg := range args {
119 paths = append(paths, s.Path(arg))
120 }
121
122 var buf strings.Builder
123 errc := make(chan error, 1)
124 go func() {
125 for _, p := range paths {
126 b, err := os.ReadFile(p)
127 buf.Write(b)
128 if err != nil {
129 errc <- err
130 return
131 }
132 }
133 errc <- nil
134 }()
135
136 wait := func(*State) (stdout, stderr string, err error) {
137 err = <-errc
138 return buf.String(), "", err
139 }
140 return wait, nil
141 })
142 }
143
144
145 func Cd() Cmd {
146 return Command(
147 CmdUsage{
148 Summary: "change the working directory",
149 Args: "dir",
150 },
151 func(s *State, args ...string) (WaitFunc, error) {
152 if len(args) != 1 {
153 return nil, ErrUsage
154 }
155 return nil, s.Chdir(args[0])
156 })
157 }
158
159
160 func Chmod() Cmd {
161 return Command(
162 CmdUsage{
163 Summary: "change file mode bits",
164 Args: "perm paths...",
165 Detail: []string{
166 "Changes the permissions of the named files or directories to be equal to perm.",
167 "Only numerical permissions are supported.",
168 },
169 },
170 func(s *State, args ...string) (WaitFunc, error) {
171 if len(args) < 2 {
172 return nil, ErrUsage
173 }
174
175 perm, err := strconv.ParseUint(args[0], 0, 32)
176 if err != nil || perm&uint64(fs.ModePerm) != perm {
177 return nil, fmt.Errorf("invalid mode: %s", args[0])
178 }
179
180 for _, arg := range args[1:] {
181 err := os.Chmod(s.Path(arg), fs.FileMode(perm))
182 if err != nil {
183 return nil, err
184 }
185 }
186 return nil, nil
187 })
188 }
189
190
191
192
193 func Cmp() Cmd {
194 return Command(
195 CmdUsage{
196 Args: "[-q] file1 file2",
197 Summary: "compare files for differences",
198 Detail: []string{
199 "By convention, file1 is the actual data and file2 is the expected data.",
200 "The command succeeds if the file contents are identical.",
201 "File1 can be 'stdout' or 'stderr' to compare the stdout or stderr buffer from the most recent command.",
202 },
203 },
204 func(s *State, args ...string) (WaitFunc, error) {
205 return nil, doCompare(s, false, args...)
206 })
207 }
208
209
210
211 func Cmpenv() Cmd {
212 return Command(
213 CmdUsage{
214 Args: "[-q] file1 file2",
215 Summary: "compare files for differences, with environment expansion",
216 Detail: []string{
217 "By convention, file1 is the actual data and file2 is the expected data.",
218 "The command succeeds if the file contents are identical after substituting variables from the script environment.",
219 "File1 can be 'stdout' or 'stderr' to compare the script's stdout or stderr buffer.",
220 },
221 },
222 func(s *State, args ...string) (WaitFunc, error) {
223 return nil, doCompare(s, true, args...)
224 })
225 }
226
227 func doCompare(s *State, env bool, args ...string) error {
228 quiet := false
229 if len(args) > 0 && args[0] == "-q" {
230 quiet = true
231 args = args[1:]
232 }
233 if len(args) != 2 {
234 return ErrUsage
235 }
236
237 name1, name2 := args[0], args[1]
238 var text1, text2 string
239 switch name1 {
240 case "stdout":
241 text1 = s.Stdout()
242 case "stderr":
243 text1 = s.Stderr()
244 default:
245 data, err := os.ReadFile(s.Path(name1))
246 if err != nil {
247 return err
248 }
249 text1 = string(data)
250 }
251
252 data, err := os.ReadFile(s.Path(name2))
253 if err != nil {
254 return err
255 }
256 text2 = string(data)
257
258 if env {
259 text1 = s.ExpandEnv(text1, false)
260 text2 = s.ExpandEnv(text2, false)
261 }
262
263 if text1 != text2 {
264 if !quiet {
265 diffText := diff.Diff(name1, []byte(text1), name2, []byte(text2))
266 s.Logf("%s\n", diffText)
267 }
268 return fmt.Errorf("%s and %s differ", name1, name2)
269 }
270 return nil
271 }
272
273
274 func Cp() Cmd {
275 return Command(
276 CmdUsage{
277 Summary: "copy files to a target file or directory",
278 Args: "src... dst",
279 Detail: []string{
280 "src can include 'stdout' or 'stderr' to copy from the script's stdout or stderr buffer.",
281 },
282 },
283 func(s *State, args ...string) (WaitFunc, error) {
284 if len(args) < 2 {
285 return nil, ErrUsage
286 }
287
288 dst := s.Path(args[len(args)-1])
289 info, err := os.Stat(dst)
290 dstDir := err == nil && info.IsDir()
291 if len(args) > 2 && !dstDir {
292 return nil, &fs.PathError{Op: "cp", Path: dst, Err: errors.New("destination is not a directory")}
293 }
294
295 for _, arg := range args[:len(args)-1] {
296 var (
297 src string
298 data []byte
299 mode fs.FileMode
300 )
301 switch arg {
302 case "stdout":
303 src = arg
304 data = []byte(s.Stdout())
305 mode = 0666
306 case "stderr":
307 src = arg
308 data = []byte(s.Stderr())
309 mode = 0666
310 default:
311 src = s.Path(arg)
312 info, err := os.Stat(src)
313 if err != nil {
314 return nil, err
315 }
316 mode = info.Mode() & 0777
317 data, err = os.ReadFile(src)
318 if err != nil {
319 return nil, err
320 }
321 }
322 targ := dst
323 if dstDir {
324 targ = filepath.Join(dst, filepath.Base(src))
325 }
326 err := os.WriteFile(targ, data, mode)
327 if err != nil {
328 return nil, err
329 }
330 }
331
332 return nil, nil
333 })
334 }
335
336
337 func Echo() Cmd {
338 return Command(
339 CmdUsage{
340 Summary: "display a line of text",
341 Args: "string...",
342 },
343 func(s *State, args ...string) (WaitFunc, error) {
344 var buf strings.Builder
345 for i, arg := range args {
346 if i > 0 {
347 buf.WriteString(" ")
348 }
349 buf.WriteString(arg)
350 }
351 buf.WriteString("\n")
352 out := buf.String()
353
354
355
356
357
358
359
360
361 return func(*State) (stdout, stderr string, err error) {
362 return out, "", nil
363 }, nil
364 })
365 }
366
367
368
369
370
371
372 func Env() Cmd {
373 return Command(
374 CmdUsage{
375 Summary: "set or log the values of environment variables",
376 Args: "[key[=value]...]",
377 Detail: []string{
378 "With no arguments, print the script environment to the log.",
379 "Otherwise, add the listed key=value pairs to the environment or print the listed keys.",
380 },
381 },
382 func(s *State, args ...string) (WaitFunc, error) {
383 out := new(strings.Builder)
384 if len(args) == 0 {
385 for _, kv := range s.env {
386 fmt.Fprintf(out, "%s\n", kv)
387 }
388 } else {
389 for _, env := range args {
390 i := strings.Index(env, "=")
391 if i < 0 {
392
393 fmt.Fprintf(out, "%s=%s\n", env, s.envMap[env])
394 continue
395 }
396 if err := s.Setenv(env[:i], env[i+1:]); err != nil {
397 return nil, err
398 }
399 }
400 }
401 var wait WaitFunc
402 if out.Len() > 0 || len(args) == 0 {
403 wait = func(*State) (stdout, stderr string, err error) {
404 return out.String(), "", nil
405 }
406 }
407 return wait, nil
408 })
409 }
410
411
412
413
414
415
416 func Exec(cancel func(*exec.Cmd) error, waitDelay time.Duration) Cmd {
417 return Command(
418 CmdUsage{
419 Summary: "run an executable program with arguments",
420 Args: "program [args...]",
421 Detail: []string{
422 "Note that 'exec' does not terminate the script (unlike Unix shells).",
423 },
424 Async: true,
425 },
426 func(s *State, args ...string) (WaitFunc, error) {
427 if len(args) < 1 {
428 return nil, ErrUsage
429 }
430
431
432
433
434 name := filepath.FromSlash(args[0])
435 path := name
436 if !strings.Contains(name, string(filepath.Separator)) {
437 var err error
438 path, err = lookPath(s, name)
439 if err != nil {
440 return nil, err
441 }
442 }
443
444 return startCommand(s, name, path, args[1:], cancel, waitDelay)
445 })
446 }
447
448 func startCommand(s *State, name, path string, args []string, cancel func(*exec.Cmd) error, waitDelay time.Duration) (WaitFunc, error) {
449 var (
450 cmd *exec.Cmd
451 stdoutBuf, stderrBuf strings.Builder
452 )
453 for {
454 cmd = exec.CommandContext(s.Context(), path, args...)
455 if cancel == nil {
456 cmd.Cancel = nil
457 } else {
458 cmd.Cancel = func() error { return cancel(cmd) }
459 }
460 cmd.WaitDelay = waitDelay
461 cmd.Args[0] = name
462 cmd.Dir = s.Getwd()
463 cmd.Env = s.env
464 cmd.Stdout = &stdoutBuf
465 cmd.Stderr = &stderrBuf
466 err := cmd.Start()
467 if err == nil {
468 break
469 }
470 if isETXTBSY(err) {
471
472
473
474
475
476
477 } else {
478 return nil, err
479 }
480 }
481
482 wait := func(s *State) (stdout, stderr string, err error) {
483 err = cmd.Wait()
484 if errors.Is(err, exec.ErrWaitDelay) {
485 err = fmt.Errorf("%w: output pipes not closed after waiting %v", err, cmd.WaitDelay)
486 }
487 return stdoutBuf.String(), stderrBuf.String(), err
488 }
489 return wait, nil
490 }
491
492
493
494 func lookPath(s *State, command string) (string, error) {
495 var strEqual func(string, string) bool
496 if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
497
498
499 strEqual = strings.EqualFold
500 } else {
501 strEqual = func(a, b string) bool { return a == b }
502 }
503
504 var pathExt []string
505 var searchExt bool
506 var isExecutable func(os.FileInfo) bool
507 if runtime.GOOS == "windows" {
508
509
510
511
512
513 pathExt = strings.Split(os.Getenv("PathExt"), string(filepath.ListSeparator))
514 searchExt = true
515 cmdExt := filepath.Ext(command)
516 for _, ext := range pathExt {
517 if strEqual(cmdExt, ext) {
518 searchExt = false
519 break
520 }
521 }
522 isExecutable = func(fi os.FileInfo) bool {
523 return fi.Mode().IsRegular()
524 }
525 } else {
526 isExecutable = func(fi os.FileInfo) bool {
527 return fi.Mode().IsRegular() && fi.Mode().Perm()&0111 != 0
528 }
529 }
530
531 pathEnv, _ := s.LookupEnv(pathEnvName())
532 for dir := range strings.SplitSeq(pathEnv, string(filepath.ListSeparator)) {
533 if dir == "" {
534 continue
535 }
536
537
538
539
540 sep := string(filepath.Separator)
541 if os.IsPathSeparator(dir[len(dir)-1]) {
542 sep = ""
543 }
544
545 if searchExt {
546 ents, err := os.ReadDir(dir)
547 if err != nil {
548 continue
549 }
550 for _, ent := range ents {
551 for _, ext := range pathExt {
552 if !ent.IsDir() && strEqual(ent.Name(), command+ext) {
553 return dir + sep + ent.Name(), nil
554 }
555 }
556 }
557 } else {
558 path := dir + sep + command
559 if fi, err := os.Stat(path); err == nil && isExecutable(fi) {
560 return path, nil
561 }
562 }
563 }
564 return "", &exec.Error{Name: command, Err: exec.ErrNotFound}
565 }
566
567
568
569
570
571
572 func pathEnvName() string {
573 switch runtime.GOOS {
574 case "plan9":
575 return "path"
576 default:
577 return "PATH"
578 }
579 }
580
581
582 func Exists() Cmd {
583 return Command(
584 CmdUsage{
585 Summary: "check that files exist",
586 Args: "[-readonly] [-exec] file...",
587 },
588 func(s *State, args ...string) (WaitFunc, error) {
589 var readonly, exec bool
590 loop:
591 for len(args) > 0 {
592 switch args[0] {
593 case "-readonly":
594 readonly = true
595 args = args[1:]
596 case "-exec":
597 exec = true
598 args = args[1:]
599 default:
600 break loop
601 }
602 }
603 if len(args) == 0 {
604 return nil, ErrUsage
605 }
606
607 for _, file := range args {
608 file = s.Path(file)
609 info, err := os.Stat(file)
610 if err != nil {
611 return nil, err
612 }
613 if readonly && info.Mode()&0222 != 0 {
614 return nil, fmt.Errorf("%s exists but is writable", file)
615 }
616 if exec && runtime.GOOS != "windows" && info.Mode()&0111 == 0 {
617 return nil, fmt.Errorf("%s exists but is not executable", file)
618 }
619 }
620
621 return nil, nil
622 })
623 }
624
625
626
627
628
629
630 func Grep() Cmd {
631 return Command(
632 CmdUsage{
633 Summary: "find lines in a file that match a pattern",
634 Args: matchUsage + " file",
635 Detail: []string{
636 "The command succeeds if at least one match (or the exact count, if given) is found.",
637 "The -q flag suppresses printing of matches.",
638 },
639 RegexpArgs: firstNonFlag,
640 },
641 func(s *State, args ...string) (WaitFunc, error) {
642 return nil, match(s, args, "", "grep")
643 })
644 }
645
646 const matchUsage = "[-count=N] [-q] 'pattern'"
647
648
649 func match(s *State, args []string, text, name string) error {
650 n := 0
651 if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") {
652 var err error
653 n, err = strconv.Atoi(args[0][len("-count="):])
654 if err != nil {
655 return fmt.Errorf("bad -count=: %v", err)
656 }
657 if n < 1 {
658 return fmt.Errorf("bad -count=: must be at least 1")
659 }
660 args = args[1:]
661 }
662 quiet := false
663 if len(args) >= 1 && args[0] == "-q" {
664 quiet = true
665 args = args[1:]
666 }
667
668 isGrep := name == "grep"
669
670 wantArgs := 1
671 if isGrep {
672 wantArgs = 2
673 }
674 if len(args) != wantArgs {
675 return ErrUsage
676 }
677
678 pattern := `(?m)` + args[0]
679 re, err := regexp.Compile(pattern)
680 if err != nil {
681 return err
682 }
683
684 if isGrep {
685 name = args[1]
686 data, err := os.ReadFile(s.Path(args[1]))
687 if err != nil {
688 return err
689 }
690 text = string(data)
691 }
692
693 if n > 0 {
694 count := len(re.FindAllString(text, -1))
695 if count != n {
696 return fmt.Errorf("found %d matches for %#q in %s", count, pattern, name)
697 }
698 return nil
699 }
700
701 if !re.MatchString(text) {
702 return fmt.Errorf("no match for %#q in %s", pattern, name)
703 }
704
705 if !quiet {
706
707 loc := re.FindStringIndex(text)
708 for loc[0] > 0 && text[loc[0]-1] != '\n' {
709 loc[0]--
710 }
711 for loc[1] < len(text) && text[loc[1]] != '\n' {
712 loc[1]++
713 }
714 lines := strings.TrimSuffix(text[loc[0]:loc[1]], "\n")
715 s.Logf("matched: %s\n", lines)
716 }
717 return nil
718 }
719
720
721 func Help() Cmd {
722 return Command(
723 CmdUsage{
724 Summary: "log help text for commands and conditions",
725 Args: "[-v] name...",
726 Detail: []string{
727 "To display help for a specific condition, enclose it in brackets: 'help [amd64]'.",
728 "To display complete documentation when listing all commands, pass the -v flag.",
729 },
730 },
731 func(s *State, args ...string) (WaitFunc, error) {
732 if s.engine == nil {
733 return nil, errors.New("no engine configured")
734 }
735
736 verbose := false
737 if len(args) > 0 {
738 verbose = true
739 if args[0] == "-v" {
740 args = args[1:]
741 }
742 }
743
744 var cmds, conds []string
745 for _, arg := range args {
746 if strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]") {
747 conds = append(conds, arg[1:len(arg)-1])
748 } else {
749 cmds = append(cmds, arg)
750 }
751 }
752
753 out := new(strings.Builder)
754
755 if len(conds) > 0 || (len(args) == 0 && len(s.engine.Conds) > 0) {
756 if conds == nil {
757 out.WriteString("conditions:\n\n")
758 }
759 s.engine.ListConds(out, s, conds...)
760 }
761
762 if len(cmds) > 0 || len(args) == 0 {
763 if len(args) == 0 {
764 out.WriteString("\ncommands:\n\n")
765 }
766 s.engine.ListCmds(out, verbose, cmds...)
767 }
768
769 wait := func(*State) (stdout, stderr string, err error) {
770 return out.String(), "", nil
771 }
772 return wait, nil
773 })
774 }
775
776
777 func Mkdir() Cmd {
778 return Command(
779 CmdUsage{
780 Summary: "create directories, if they do not already exist",
781 Args: "path...",
782 Detail: []string{
783 "Unlike Unix mkdir, parent directories are always created if needed.",
784 },
785 },
786 func(s *State, args ...string) (WaitFunc, error) {
787 if len(args) < 1 {
788 return nil, ErrUsage
789 }
790 for _, arg := range args {
791 if err := os.MkdirAll(s.Path(arg), 0777); err != nil {
792 return nil, err
793 }
794 }
795 return nil, nil
796 })
797 }
798
799
800 func Mv() Cmd {
801 return Command(
802 CmdUsage{
803 Summary: "rename a file or directory to a new path",
804 Args: "old new",
805 Detail: []string{
806 "OS-specific restrictions may apply when old and new are in different directories.",
807 },
808 },
809 func(s *State, args ...string) (WaitFunc, error) {
810 if len(args) != 2 {
811 return nil, ErrUsage
812 }
813 return nil, os.Rename(s.Path(args[0]), s.Path(args[1]))
814 })
815 }
816
817
818
819 func Program(name string, cancel func(*exec.Cmd) error, waitDelay time.Duration) Cmd {
820 var (
821 shortName string
822 summary string
823 lookPathOnce sync.Once
824 path string
825 pathErr error
826 )
827 if filepath.IsAbs(name) {
828 lookPathOnce.Do(func() { path = filepath.Clean(name) })
829 shortName = strings.TrimSuffix(filepath.Base(path), ".exe")
830 summary = "run the '" + shortName + "' program provided by the script host"
831 } else {
832 shortName = name
833 summary = "run the '" + shortName + "' program from the script host's PATH"
834 }
835
836 return Command(
837 CmdUsage{
838 Summary: summary,
839 Args: "[args...]",
840 Async: true,
841 },
842 func(s *State, args ...string) (WaitFunc, error) {
843 lookPathOnce.Do(func() {
844 path, pathErr = pathcache.LookPath(name)
845 })
846 if pathErr != nil {
847 return nil, pathErr
848 }
849 return startCommand(s, shortName, path, args, cancel, waitDelay)
850 })
851 }
852
853
854 func Replace() Cmd {
855 return Command(
856 CmdUsage{
857 Summary: "replace strings in a file",
858 Args: "[old new]... file",
859 Detail: []string{
860 "The 'old' and 'new' arguments are unquoted as if in quoted Go strings.",
861 },
862 },
863 func(s *State, args ...string) (WaitFunc, error) {
864 if len(args)%2 != 1 {
865 return nil, ErrUsage
866 }
867
868 oldNew := make([]string, 0, len(args)-1)
869 for _, arg := range args[:len(args)-1] {
870 s, err := strconv.Unquote(`"` + arg + `"`)
871 if err != nil {
872 return nil, err
873 }
874 oldNew = append(oldNew, s)
875 }
876
877 r := strings.NewReplacer(oldNew...)
878 file := s.Path(args[len(args)-1])
879
880 data, err := os.ReadFile(file)
881 if err != nil {
882 return nil, err
883 }
884 replaced := r.Replace(string(data))
885
886 return nil, os.WriteFile(file, []byte(replaced), 0666)
887 })
888 }
889
890
891
892
893
894 func Rm() Cmd {
895 return Command(
896 CmdUsage{
897 Summary: "remove a file or directory",
898 Args: "path...",
899 Detail: []string{
900 "If the path is a directory, its contents are removed recursively.",
901 },
902 },
903 func(s *State, args ...string) (WaitFunc, error) {
904 if len(args) < 1 {
905 return nil, ErrUsage
906 }
907 for _, arg := range args {
908 if err := removeAll(s.Path(arg)); err != nil {
909 return nil, err
910 }
911 }
912 return nil, nil
913 })
914 }
915
916
917
918
919
920 func removeAll(dir string) error {
921
922
923 filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error {
924
925
926 if err != nil || info.IsDir() {
927 os.Chmod(path, 0777)
928 }
929 return nil
930 })
931 return robustio.RemoveAll(dir)
932 }
933
934
935
936 func Sleep() Cmd {
937 return Command(
938 CmdUsage{
939 Summary: "sleep for a specified duration",
940 Args: "duration",
941 Detail: []string{
942 "The duration must be given as a Go time.Duration string.",
943 },
944 Async: true,
945 },
946 func(s *State, args ...string) (WaitFunc, error) {
947 if len(args) != 1 {
948 return nil, ErrUsage
949 }
950
951 d, err := time.ParseDuration(args[0])
952 if err != nil {
953 return nil, err
954 }
955
956 timer := time.NewTimer(d)
957 wait := func(s *State) (stdout, stderr string, err error) {
958 ctx := s.Context()
959 select {
960 case <-ctx.Done():
961 timer.Stop()
962 return "", "", ctx.Err()
963 case <-timer.C:
964 return "", "", nil
965 }
966 }
967 return wait, nil
968 })
969 }
970
971
972 func Stderr() Cmd {
973 return Command(
974 CmdUsage{
975 Summary: "find lines in the stderr buffer that match a pattern",
976 Args: matchUsage + " file",
977 Detail: []string{
978 "The command succeeds if at least one match (or the exact count, if given) is found.",
979 "The -q flag suppresses printing of matches.",
980 },
981 RegexpArgs: firstNonFlag,
982 },
983 func(s *State, args ...string) (WaitFunc, error) {
984 return nil, match(s, args, s.Stderr(), "stderr")
985 })
986 }
987
988
989 func Stdout() Cmd {
990 return Command(
991 CmdUsage{
992 Summary: "find lines in the stdout buffer that match a pattern",
993 Args: matchUsage + " file",
994 Detail: []string{
995 "The command succeeds if at least one match (or the exact count, if given) is found.",
996 "The -q flag suppresses printing of matches.",
997 },
998 RegexpArgs: firstNonFlag,
999 },
1000 func(s *State, args ...string) (WaitFunc, error) {
1001 return nil, match(s, args, s.Stdout(), "stdout")
1002 })
1003 }
1004
1005
1006
1007 func Stop() Cmd {
1008 return Command(
1009 CmdUsage{
1010 Summary: "stop execution of the script",
1011 Args: "[msg]",
1012 Detail: []string{
1013 "The message is written to the script log, but no error is reported from the script engine.",
1014 },
1015 },
1016 func(s *State, args ...string) (WaitFunc, error) {
1017 if len(args) > 1 {
1018 return nil, ErrUsage
1019 }
1020
1021
1022 if len(args) == 1 {
1023 return nil, stopError{msg: args[0]}
1024 }
1025 return nil, stopError{}
1026 })
1027 }
1028
1029
1030 type stopError struct {
1031 msg string
1032 }
1033
1034 func (s stopError) Error() string {
1035 if s.msg == "" {
1036 return "stop"
1037 }
1038 return "stop: " + s.msg
1039 }
1040
1041
1042 func Symlink() Cmd {
1043 return Command(
1044 CmdUsage{
1045 Summary: "create a symlink",
1046 Args: "path -> target",
1047 Detail: []string{
1048 "Creates path as a symlink to target.",
1049 "The '->' token (like in 'ls -l' output on Unix) is required.",
1050 },
1051 },
1052 func(s *State, args ...string) (WaitFunc, error) {
1053 if len(args) != 3 || args[1] != "->" {
1054 return nil, ErrUsage
1055 }
1056
1057
1058
1059 return nil, os.Symlink(filepath.FromSlash(args[2]), s.Path(args[0]))
1060 })
1061 }
1062
1063
1064
1065
1066
1067
1068 func Wait() Cmd {
1069 return Command(
1070 CmdUsage{
1071 Summary: "wait for completion of background commands",
1072 Args: "",
1073 Detail: []string{
1074 "Waits for all background commands to complete.",
1075 "The output (and any error) from each command is printed to the log in the order in which the commands were started.",
1076 "After the call to 'wait', the script's stdout and stderr buffers contain the concatenation of the background commands' outputs.",
1077 },
1078 },
1079 func(s *State, args ...string) (WaitFunc, error) {
1080 if len(args) > 0 {
1081 return nil, ErrUsage
1082 }
1083
1084 var stdouts, stderrs []string
1085 var errs []*CommandError
1086 for _, bg := range s.background {
1087 stdout, stderr, err := bg.wait(s)
1088
1089 beforeArgs := ""
1090 if len(bg.args) > 0 {
1091 beforeArgs = " "
1092 }
1093 s.Logf("[background] %s%s%s\n", bg.name, beforeArgs, quoteArgs(bg.args))
1094
1095 if stdout != "" {
1096 s.Logf("[stdout]\n%s", stdout)
1097 stdouts = append(stdouts, stdout)
1098 }
1099 if stderr != "" {
1100 s.Logf("[stderr]\n%s", stderr)
1101 stderrs = append(stderrs, stderr)
1102 }
1103 if err != nil {
1104 s.Logf("[%v]\n", err)
1105 }
1106 if cmdErr := checkStatus(bg.command, err); cmdErr != nil {
1107 errs = append(errs, cmdErr.(*CommandError))
1108 }
1109 }
1110
1111 s.stdout = strings.Join(stdouts, "")
1112 s.stderr = strings.Join(stderrs, "")
1113 s.background = nil
1114 if len(errs) > 0 {
1115 return nil, waitError{errs: errs}
1116 }
1117 return nil, nil
1118 })
1119 }
1120
1121
1122 type waitError struct {
1123 errs []*CommandError
1124 }
1125
1126 func (w waitError) Error() string {
1127 b := new(strings.Builder)
1128 for i, err := range w.errs {
1129 if i != 0 {
1130 b.WriteString("\n")
1131 }
1132 b.WriteString(err.Error())
1133 }
1134 return b.String()
1135 }
1136
1137 func (w waitError) Unwrap() error {
1138 if len(w.errs) == 1 {
1139 return w.errs[0]
1140 }
1141 return nil
1142 }
1143
View as plain text