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