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