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