github.com/bir3/gocompiler@v0.9.2202/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/cfg" 9 "github.com/bir3/gocompiler/src/cmd/gocmd/internal/robustio" 10 "errors" 11 "fmt" 12 "github.com/bir3/gocompiler/src/internal/diff" 13 "io/fs" 14 "os" 15 "github.com/bir3/gocompiler/exec" 16 "path/filepath" 17 "regexp" 18 "runtime" 19 "strconv" 20 "strings" 21 "sync" 22 "time" 23 ) 24 25 // DefaultCmds returns a set of broadly useful script commands. 26 // 27 // Run the 'help' command within a script engine to view a list of the available 28 // commands. 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), // arbitrary grace period 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 // Command returns a new Cmd with a Usage method that returns a copy of the 57 // given CmdUsage and a Run method calls the given function. 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 // A funcCmd implements Cmd using a function value. 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 // firstNonFlag returns a slice containing the index of the first argument in 78 // rawArgs that is not a flag, or nil if all arguments are flags. 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 // Cat writes the concatenated contents of the named file(s) to the script's 92 // stdout buffer. 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 // Cd changes the current working directory. 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 // Chmod changes the permissions of a file or a directory.. 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 // Cmp compares the contents of two files, or the contents of either the 178 // "stdout" or "stderr" buffer and a file, returning a non-nil error if the 179 // contents differ. 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 // Cmpenv is like Compare, but also performs environment substitutions 197 // on the contents of both arguments. 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 // Cp copies one or more files to a new location. 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 // Echo writes its arguments to stdout, followed by a newline. 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 // Stuff the result into a callback to satisfy the OutputCommandFunc 342 // interface, even though it isn't really asynchronous even if run in the 343 // background. 344 // 345 // Nobody should be running 'echo' as a background command, but it's not worth 346 // defining yet another interface, and also doesn't seem worth shoehorning 347 // into a SimpleCommand the way we did with Wait. 348 return func(*State) (stdout, stderr string, err error) { 349 return out, "", nil 350 }, nil 351 }) 352 } 353 354 // Env sets or logs the values of environment variables. 355 // 356 // With no arguments, Env reports all variables in the environment. 357 // "key=value" arguments set variables, and arguments without "=" 358 // cause the corresponding value to be printed to the stdout buffer. 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 // Display value instead of setting it. 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 // Exec runs an arbitrary executable as a subprocess. 399 // 400 // When the Script's context is canceled, Exec sends the interrupt signal, then 401 // waits for up to the given delay for the subprocess to flush output before 402 // terminating it with os.Kill. 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 // Use the script's PATH to look up the command (if it does not contain a separator) 419 // instead of the test process's PATH (see lookPath). 420 // Don't use filepath.Clean, since that changes "./foo" to "foo". 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 // If the script (or its host process) just wrote the executable we're 459 // trying to run, a fork+exec in another thread may be holding open the FD 460 // that we used to write the executable (see https://go.dev/issue/22315). 461 // Since the descriptor should have CLOEXEC set, the problem should 462 // resolve as soon as the forked child reaches its exec call. 463 // Keep retrying until that happens. 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 // lookPath is (roughly) like exec.LookPath, but it uses the script's current 477 // PATH to find the executable. 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 // Using GOOS as a proxy for case-insensitive file system. 482 // TODO(bcmills): Remove this assumption. 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 // Use the test process's PathExt instead of the script's. 493 // If PathExt is set in the command's environment, cmd.Start fails with 494 // "parameter is invalid". Not sure why. 495 // If the command already has an extension in PathExt (like "cmd.exe") 496 // don't search for other extensions (not "cmd.bat.exe"). 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 // Determine whether dir needs a trailing path separator. 522 // Note: we avoid filepath.Join in this function because it cleans the 523 // result: we want to preserve the exact dir prefix from the environment. 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 // pathEnvName returns the platform-specific variable used by os/exec.LookPath 552 // to look up executable names (either "PATH" or "path"). 553 // 554 // TODO(bcmills): Investigate whether we can instead use PATH uniformly and 555 // rewrite it to $path when executing subprocesses. 556 func pathEnvName() string { 557 switch runtime.GOOS { 558 case "plan9": 559 return "path" 560 default: 561 return "PATH" 562 } 563 } 564 565 // Exists checks that the named file(s) exist. 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 // Grep checks that file content matches a regexp. 610 // Like stdout/stderr and unlike Unix grep, it accepts Go regexp syntax. 611 // 612 // Grep does not modify the State's stdout or stderr buffers. 613 // (Its output goes to the script log, not stdout.) 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 // match implements the Grep, Stdout, and Stderr commands. 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] // for error messages 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 // Print the lines containing the match. 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 // Help writes command documentation to the script log. 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 // Mkdir creates a directory and any needed parent directories. 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 // Mv renames an existing file or directory to a new path. 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 // Program returns a new command that runs the named program, found from the 802 // host process's PATH (not looked up in the script's PATH). 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 = cfg.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 // Replace replaces all occurrences of a string in a file with another string. 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 // Rm removes a file or directory. 875 // 876 // If a directory, Rm also recursively removes that directory's 877 // contents. 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 // removeAll removes dir and all files and directories it contains. 901 // 902 // Unlike os.RemoveAll, removeAll attempts to make the directories writable if 903 // needed in order to remove their contents. 904 func removeAll(dir string) error { 905 // module cache has 0444 directories; 906 // make them writable in order to remove content. 907 filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error { 908 // chmod not only directories, but also things that we couldn't even stat 909 // due to permission errors: they may also be unreadable directories. 910 if err != nil || info.IsDir() { 911 os.Chmod(path, 0777) 912 } 913 return nil 914 }) 915 return robustio.RemoveAll(dir) 916 } 917 918 // Sleep sleeps for the given Go duration or until the script's context is 919 // cancelled, whichever happens first. 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 // Stderr searches for a regular expression in the stderr buffer. 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 // Stdout searches for a regular expression in the stdout buffer. 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 // Stop returns a sentinel error that causes script execution to halt 990 // and s.Execute to return with a nil error. 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 // TODO(bcmills): The argument passed to stop seems redundant with comments. 1005 // Either use it systematically or remove it. 1006 if len(args) == 1 { 1007 return nil, stopError{msg: args[0]} 1008 } 1009 return nil, stopError{} 1010 }) 1011 } 1012 1013 // stopError is the sentinel error type returned by the Stop command. 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 // Symlink creates a symbolic link. 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 // Note that the link target args[2] is not interpreted with s.Path: 1042 // it will be interpreted relative to the directory file is in. 1043 return nil, os.Symlink(filepath.FromSlash(args[2]), s.Path(args[0])) 1044 }) 1045 } 1046 1047 // Wait waits for the completion of background commands. 1048 // 1049 // When Wait returns, the stdout and stderr buffers contain the concatenation of 1050 // the background commands' respective outputs in the order in which those 1051 // commands were started. 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 // A waitError wraps one or more errors returned by background commands. 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 }