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 if errors.Is(err, exec.ErrWaitDelay) {
472 err = fmt.Errorf("%w: output pipes not closed after waiting %v", err, cmd.WaitDelay)
473 }
474 return stdoutBuf.String(), stderrBuf.String(), err
475 }
476 return wait, nil
477 }
478
479
480
481 func lookPath(s *State, command string) (string, error) {
482 var strEqual func(string, string) bool
483 if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
484
485
486 strEqual = strings.EqualFold
487 } else {
488 strEqual = func(a, b string) bool { return a == b }
489 }
490
491 var pathExt []string
492 var searchExt bool
493 var isExecutable func(os.FileInfo) bool
494 if runtime.GOOS == "windows" {
495
496
497
498
499
500 pathExt = strings.Split(os.Getenv("PathExt"), string(filepath.ListSeparator))
501 searchExt = true
502 cmdExt := filepath.Ext(command)
503 for _, ext := range pathExt {
504 if strEqual(cmdExt, ext) {
505 searchExt = false
506 break
507 }
508 }
509 isExecutable = func(fi os.FileInfo) bool {
510 return fi.Mode().IsRegular()
511 }
512 } else {
513 isExecutable = func(fi os.FileInfo) bool {
514 return fi.Mode().IsRegular() && fi.Mode().Perm()&0111 != 0
515 }
516 }
517
518 pathEnv, _ := s.LookupEnv(pathEnvName())
519 for dir := range strings.SplitSeq(pathEnv, string(filepath.ListSeparator)) {
520 if dir == "" {
521 continue
522 }
523
524
525
526
527 sep := string(filepath.Separator)
528 if os.IsPathSeparator(dir[len(dir)-1]) {
529 sep = ""
530 }
531
532 if searchExt {
533 ents, err := os.ReadDir(dir)
534 if err != nil {
535 continue
536 }
537 for _, ent := range ents {
538 for _, ext := range pathExt {
539 if !ent.IsDir() && strEqual(ent.Name(), command+ext) {
540 return dir + sep + ent.Name(), nil
541 }
542 }
543 }
544 } else {
545 path := dir + sep + command
546 if fi, err := os.Stat(path); err == nil && isExecutable(fi) {
547 return path, nil
548 }
549 }
550 }
551 return "", &exec.Error{Name: command, Err: exec.ErrNotFound}
552 }
553
554
555
556
557
558
559 func pathEnvName() string {
560 switch runtime.GOOS {
561 case "plan9":
562 return "path"
563 default:
564 return "PATH"
565 }
566 }
567
568
569 func Exists() Cmd {
570 return Command(
571 CmdUsage{
572 Summary: "check that files exist",
573 Args: "[-readonly] [-exec] file...",
574 },
575 func(s *State, args ...string) (WaitFunc, error) {
576 var readonly, exec bool
577 loop:
578 for len(args) > 0 {
579 switch args[0] {
580 case "-readonly":
581 readonly = true
582 args = args[1:]
583 case "-exec":
584 exec = true
585 args = args[1:]
586 default:
587 break loop
588 }
589 }
590 if len(args) == 0 {
591 return nil, ErrUsage
592 }
593
594 for _, file := range args {
595 file = s.Path(file)
596 info, err := os.Stat(file)
597 if err != nil {
598 return nil, err
599 }
600 if readonly && info.Mode()&0222 != 0 {
601 return nil, fmt.Errorf("%s exists but is writable", file)
602 }
603 if exec && runtime.GOOS != "windows" && info.Mode()&0111 == 0 {
604 return nil, fmt.Errorf("%s exists but is not executable", file)
605 }
606 }
607
608 return nil, nil
609 })
610 }
611
612
613
614
615
616
617 func Grep() Cmd {
618 return Command(
619 CmdUsage{
620 Summary: "find lines in a file that match a pattern",
621 Args: matchUsage + " file",
622 Detail: []string{
623 "The command succeeds if at least one match (or the exact count, if given) is found.",
624 "The -q flag suppresses printing of matches.",
625 },
626 RegexpArgs: firstNonFlag,
627 },
628 func(s *State, args ...string) (WaitFunc, error) {
629 return nil, match(s, args, "", "grep")
630 })
631 }
632
633 const matchUsage = "[-count=N] [-q] 'pattern'"
634
635
636 func match(s *State, args []string, text, name string) error {
637 n := 0
638 if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") {
639 var err error
640 n, err = strconv.Atoi(args[0][len("-count="):])
641 if err != nil {
642 return fmt.Errorf("bad -count=: %v", err)
643 }
644 if n < 1 {
645 return fmt.Errorf("bad -count=: must be at least 1")
646 }
647 args = args[1:]
648 }
649 quiet := false
650 if len(args) >= 1 && args[0] == "-q" {
651 quiet = true
652 args = args[1:]
653 }
654
655 isGrep := name == "grep"
656
657 wantArgs := 1
658 if isGrep {
659 wantArgs = 2
660 }
661 if len(args) != wantArgs {
662 return ErrUsage
663 }
664
665 pattern := `(?m)` + args[0]
666 re, err := regexp.Compile(pattern)
667 if err != nil {
668 return err
669 }
670
671 if isGrep {
672 name = args[1]
673 data, err := os.ReadFile(s.Path(args[1]))
674 if err != nil {
675 return err
676 }
677 text = string(data)
678 }
679
680 if n > 0 {
681 count := len(re.FindAllString(text, -1))
682 if count != n {
683 return fmt.Errorf("found %d matches for %#q in %s", count, pattern, name)
684 }
685 return nil
686 }
687
688 if !re.MatchString(text) {
689 return fmt.Errorf("no match for %#q in %s", pattern, name)
690 }
691
692 if !quiet {
693
694 loc := re.FindStringIndex(text)
695 for loc[0] > 0 && text[loc[0]-1] != '\n' {
696 loc[0]--
697 }
698 for loc[1] < len(text) && text[loc[1]] != '\n' {
699 loc[1]++
700 }
701 lines := strings.TrimSuffix(text[loc[0]:loc[1]], "\n")
702 s.Logf("matched: %s\n", lines)
703 }
704 return nil
705 }
706
707
708 func Help() Cmd {
709 return Command(
710 CmdUsage{
711 Summary: "log help text for commands and conditions",
712 Args: "[-v] name...",
713 Detail: []string{
714 "To display help for a specific condition, enclose it in brackets: 'help [amd64]'.",
715 "To display complete documentation when listing all commands, pass the -v flag.",
716 },
717 },
718 func(s *State, args ...string) (WaitFunc, error) {
719 if s.engine == nil {
720 return nil, errors.New("no engine configured")
721 }
722
723 verbose := false
724 if len(args) > 0 {
725 verbose = true
726 if args[0] == "-v" {
727 args = args[1:]
728 }
729 }
730
731 var cmds, conds []string
732 for _, arg := range args {
733 if strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]") {
734 conds = append(conds, arg[1:len(arg)-1])
735 } else {
736 cmds = append(cmds, arg)
737 }
738 }
739
740 out := new(strings.Builder)
741
742 if len(conds) > 0 || (len(args) == 0 && len(s.engine.Conds) > 0) {
743 if conds == nil {
744 out.WriteString("conditions:\n\n")
745 }
746 s.engine.ListConds(out, s, conds...)
747 }
748
749 if len(cmds) > 0 || len(args) == 0 {
750 if len(args) == 0 {
751 out.WriteString("\ncommands:\n\n")
752 }
753 s.engine.ListCmds(out, verbose, cmds...)
754 }
755
756 wait := func(*State) (stdout, stderr string, err error) {
757 return out.String(), "", nil
758 }
759 return wait, nil
760 })
761 }
762
763
764 func Mkdir() Cmd {
765 return Command(
766 CmdUsage{
767 Summary: "create directories, if they do not already exist",
768 Args: "path...",
769 Detail: []string{
770 "Unlike Unix mkdir, parent directories are always created if needed.",
771 },
772 },
773 func(s *State, args ...string) (WaitFunc, error) {
774 if len(args) < 1 {
775 return nil, ErrUsage
776 }
777 for _, arg := range args {
778 if err := os.MkdirAll(s.Path(arg), 0777); err != nil {
779 return nil, err
780 }
781 }
782 return nil, nil
783 })
784 }
785
786
787 func Mv() Cmd {
788 return Command(
789 CmdUsage{
790 Summary: "rename a file or directory to a new path",
791 Args: "old new",
792 Detail: []string{
793 "OS-specific restrictions may apply when old and new are in different directories.",
794 },
795 },
796 func(s *State, args ...string) (WaitFunc, error) {
797 if len(args) != 2 {
798 return nil, ErrUsage
799 }
800 return nil, os.Rename(s.Path(args[0]), s.Path(args[1]))
801 })
802 }
803
804
805
806 func Program(name string, cancel func(*exec.Cmd) error, waitDelay time.Duration) Cmd {
807 var (
808 shortName string
809 summary string
810 lookPathOnce sync.Once
811 path string
812 pathErr error
813 )
814 if filepath.IsAbs(name) {
815 lookPathOnce.Do(func() { path = filepath.Clean(name) })
816 shortName = strings.TrimSuffix(filepath.Base(path), ".exe")
817 summary = "run the '" + shortName + "' program provided by the script host"
818 } else {
819 shortName = name
820 summary = "run the '" + shortName + "' program from the script host's PATH"
821 }
822
823 return Command(
824 CmdUsage{
825 Summary: summary,
826 Args: "[args...]",
827 Async: true,
828 },
829 func(s *State, args ...string) (WaitFunc, error) {
830 lookPathOnce.Do(func() {
831 path, pathErr = pathcache.LookPath(name)
832 })
833 if pathErr != nil {
834 return nil, pathErr
835 }
836 return startCommand(s, shortName, path, args, cancel, waitDelay)
837 })
838 }
839
840
841 func Replace() Cmd {
842 return Command(
843 CmdUsage{
844 Summary: "replace strings in a file",
845 Args: "[old new]... file",
846 Detail: []string{
847 "The 'old' and 'new' arguments are unquoted as if in quoted Go strings.",
848 },
849 },
850 func(s *State, args ...string) (WaitFunc, error) {
851 if len(args)%2 != 1 {
852 return nil, ErrUsage
853 }
854
855 oldNew := make([]string, 0, len(args)-1)
856 for _, arg := range args[:len(args)-1] {
857 s, err := strconv.Unquote(`"` + arg + `"`)
858 if err != nil {
859 return nil, err
860 }
861 oldNew = append(oldNew, s)
862 }
863
864 r := strings.NewReplacer(oldNew...)
865 file := s.Path(args[len(args)-1])
866
867 data, err := os.ReadFile(file)
868 if err != nil {
869 return nil, err
870 }
871 replaced := r.Replace(string(data))
872
873 return nil, os.WriteFile(file, []byte(replaced), 0666)
874 })
875 }
876
877
878
879
880
881 func Rm() Cmd {
882 return Command(
883 CmdUsage{
884 Summary: "remove a file or directory",
885 Args: "path...",
886 Detail: []string{
887 "If the path is a directory, its contents are removed recursively.",
888 },
889 },
890 func(s *State, args ...string) (WaitFunc, error) {
891 if len(args) < 1 {
892 return nil, ErrUsage
893 }
894 for _, arg := range args {
895 if err := removeAll(s.Path(arg)); err != nil {
896 return nil, err
897 }
898 }
899 return nil, nil
900 })
901 }
902
903
904
905
906
907 func removeAll(dir string) error {
908
909
910 filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error {
911
912
913 if err != nil || info.IsDir() {
914 os.Chmod(path, 0777)
915 }
916 return nil
917 })
918 return robustio.RemoveAll(dir)
919 }
920
921
922
923 func Sleep() Cmd {
924 return Command(
925 CmdUsage{
926 Summary: "sleep for a specified duration",
927 Args: "duration",
928 Detail: []string{
929 "The duration must be given as a Go time.Duration string.",
930 },
931 Async: true,
932 },
933 func(s *State, args ...string) (WaitFunc, error) {
934 if len(args) != 1 {
935 return nil, ErrUsage
936 }
937
938 d, err := time.ParseDuration(args[0])
939 if err != nil {
940 return nil, err
941 }
942
943 timer := time.NewTimer(d)
944 wait := func(s *State) (stdout, stderr string, err error) {
945 ctx := s.Context()
946 select {
947 case <-ctx.Done():
948 timer.Stop()
949 return "", "", ctx.Err()
950 case <-timer.C:
951 return "", "", nil
952 }
953 }
954 return wait, nil
955 })
956 }
957
958
959 func Stderr() Cmd {
960 return Command(
961 CmdUsage{
962 Summary: "find lines in the stderr buffer that match a pattern",
963 Args: matchUsage + " file",
964 Detail: []string{
965 "The command succeeds if at least one match (or the exact count, if given) is found.",
966 "The -q flag suppresses printing of matches.",
967 },
968 RegexpArgs: firstNonFlag,
969 },
970 func(s *State, args ...string) (WaitFunc, error) {
971 return nil, match(s, args, s.Stderr(), "stderr")
972 })
973 }
974
975
976 func Stdout() Cmd {
977 return Command(
978 CmdUsage{
979 Summary: "find lines in the stdout buffer that match a pattern",
980 Args: matchUsage + " file",
981 Detail: []string{
982 "The command succeeds if at least one match (or the exact count, if given) is found.",
983 "The -q flag suppresses printing of matches.",
984 },
985 RegexpArgs: firstNonFlag,
986 },
987 func(s *State, args ...string) (WaitFunc, error) {
988 return nil, match(s, args, s.Stdout(), "stdout")
989 })
990 }
991
992
993
994 func Stop() Cmd {
995 return Command(
996 CmdUsage{
997 Summary: "stop execution of the script",
998 Args: "[msg]",
999 Detail: []string{
1000 "The message is written to the script log, but no error is reported from the script engine.",
1001 },
1002 },
1003 func(s *State, args ...string) (WaitFunc, error) {
1004 if len(args) > 1 {
1005 return nil, ErrUsage
1006 }
1007
1008
1009 if len(args) == 1 {
1010 return nil, stopError{msg: args[0]}
1011 }
1012 return nil, stopError{}
1013 })
1014 }
1015
1016
1017 type stopError struct {
1018 msg string
1019 }
1020
1021 func (s stopError) Error() string {
1022 if s.msg == "" {
1023 return "stop"
1024 }
1025 return "stop: " + s.msg
1026 }
1027
1028
1029 func Symlink() Cmd {
1030 return Command(
1031 CmdUsage{
1032 Summary: "create a symlink",
1033 Args: "path -> target",
1034 Detail: []string{
1035 "Creates path as a symlink to target.",
1036 "The '->' token (like in 'ls -l' output on Unix) is required.",
1037 },
1038 },
1039 func(s *State, args ...string) (WaitFunc, error) {
1040 if len(args) != 3 || args[1] != "->" {
1041 return nil, ErrUsage
1042 }
1043
1044
1045
1046 return nil, os.Symlink(filepath.FromSlash(args[2]), s.Path(args[0]))
1047 })
1048 }
1049
1050
1051
1052
1053
1054
1055 func Wait() Cmd {
1056 return Command(
1057 CmdUsage{
1058 Summary: "wait for completion of background commands",
1059 Args: "",
1060 Detail: []string{
1061 "Waits for all background commands to complete.",
1062 "The output (and any error) from each command is printed to the log in the order in which the commands were started.",
1063 "After the call to 'wait', the script's stdout and stderr buffers contain the concatenation of the background commands' outputs.",
1064 },
1065 },
1066 func(s *State, args ...string) (WaitFunc, error) {
1067 if len(args) > 0 {
1068 return nil, ErrUsage
1069 }
1070
1071 var stdouts, stderrs []string
1072 var errs []*CommandError
1073 for _, bg := range s.background {
1074 stdout, stderr, err := bg.wait(s)
1075
1076 beforeArgs := ""
1077 if len(bg.args) > 0 {
1078 beforeArgs = " "
1079 }
1080 s.Logf("[background] %s%s%s\n", bg.name, beforeArgs, quoteArgs(bg.args))
1081
1082 if stdout != "" {
1083 s.Logf("[stdout]\n%s", stdout)
1084 stdouts = append(stdouts, stdout)
1085 }
1086 if stderr != "" {
1087 s.Logf("[stderr]\n%s", stderr)
1088 stderrs = append(stderrs, stderr)
1089 }
1090 if err != nil {
1091 s.Logf("[%v]\n", err)
1092 }
1093 if cmdErr := checkStatus(bg.command, err); cmdErr != nil {
1094 errs = append(errs, cmdErr.(*CommandError))
1095 }
1096 }
1097
1098 s.stdout = strings.Join(stdouts, "")
1099 s.stderr = strings.Join(stderrs, "")
1100 s.background = nil
1101 if len(errs) > 0 {
1102 return nil, waitError{errs: errs}
1103 }
1104 return nil, nil
1105 })
1106 }
1107
1108
1109 type waitError struct {
1110 errs []*CommandError
1111 }
1112
1113 func (w waitError) Error() string {
1114 b := new(strings.Builder)
1115 for i, err := range w.errs {
1116 if i != 0 {
1117 b.WriteString("\n")
1118 }
1119 b.WriteString(err.Error())
1120 }
1121 return b.String()
1122 }
1123
1124 func (w waitError) Unwrap() error {
1125 if len(w.errs) == 1 {
1126 return w.errs[0]
1127 }
1128 return nil
1129 }
1130
View as plain text