summaryrefslogtreecommitdiffstats
path: root/src/cmd/go/internal/script/cmds.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/go/internal/script/cmds.go')
-rw-r--r--src/cmd/go/internal/script/cmds.go1113
1 files changed, 1113 insertions, 0 deletions
diff --git a/src/cmd/go/internal/script/cmds.go b/src/cmd/go/internal/script/cmds.go
new file mode 100644
index 0000000..666d2d6
--- /dev/null
+++ b/src/cmd/go/internal/script/cmds.go
@@ -0,0 +1,1113 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package script
+
+import (
+ "cmd/go/internal/robustio"
+ "errors"
+ "fmt"
+ "internal/diff"
+ "io/fs"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+)
+
+// DefaultCmds returns a set of broadly useful script commands.
+//
+// Run the 'help' command within a script engine to view a list of the available
+// commands.
+func DefaultCmds() map[string]Cmd {
+ return map[string]Cmd{
+ "cat": Cat(),
+ "cd": Cd(),
+ "chmod": Chmod(),
+ "cmp": Cmp(),
+ "cmpenv": Cmpenv(),
+ "cp": Cp(),
+ "echo": Echo(),
+ "env": Env(),
+ "exec": Exec(func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }, 100*time.Millisecond), // arbitrary grace period
+ "exists": Exists(),
+ "grep": Grep(),
+ "help": Help(),
+ "mkdir": Mkdir(),
+ "mv": Mv(),
+ "rm": Rm(),
+ "replace": Replace(),
+ "sleep": Sleep(),
+ "stderr": Stderr(),
+ "stdout": Stdout(),
+ "stop": Stop(),
+ "symlink": Symlink(),
+ "wait": Wait(),
+ }
+}
+
+// Command returns a new Cmd with a Usage method that returns a copy of the
+// given CmdUsage and a Run method calls the given function.
+func Command(usage CmdUsage, run func(*State, ...string) (WaitFunc, error)) Cmd {
+ return &funcCmd{
+ usage: usage,
+ run: run,
+ }
+}
+
+// A funcCmd implements Cmd using a function value.
+type funcCmd struct {
+ usage CmdUsage
+ run func(*State, ...string) (WaitFunc, error)
+}
+
+func (c *funcCmd) Run(s *State, args ...string) (WaitFunc, error) {
+ return c.run(s, args...)
+}
+
+func (c *funcCmd) Usage() *CmdUsage { return &c.usage }
+
+// firstNonFlag returns a slice containing the index of the first argument in
+// rawArgs that is not a flag, or nil if all arguments are flags.
+func firstNonFlag(rawArgs ...string) []int {
+ for i, arg := range rawArgs {
+ if !strings.HasPrefix(arg, "-") {
+ return []int{i}
+ }
+ if arg == "--" {
+ return []int{i + 1}
+ }
+ }
+ return nil
+}
+
+// Cat writes the concatenated contents of the named file(s) to the script's
+// stdout buffer.
+func Cat() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "concatenate files and print to the script's stdout buffer",
+ Args: "files...",
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ if len(args) == 0 {
+ return nil, ErrUsage
+ }
+
+ paths := make([]string, 0, len(args))
+ for _, arg := range args {
+ paths = append(paths, s.Path(arg))
+ }
+
+ var buf strings.Builder
+ errc := make(chan error, 1)
+ go func() {
+ for _, p := range paths {
+ b, err := os.ReadFile(p)
+ buf.Write(b)
+ if err != nil {
+ errc <- err
+ return
+ }
+ }
+ errc <- nil
+ }()
+
+ wait := func(*State) (stdout, stderr string, err error) {
+ err = <-errc
+ return buf.String(), "", err
+ }
+ return wait, nil
+ })
+}
+
+// Cd changes the current working directory.
+func Cd() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "change the working directory",
+ Args: "dir",
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ if len(args) != 1 {
+ return nil, ErrUsage
+ }
+ return nil, s.Chdir(args[0])
+ })
+}
+
+// Chmod changes the permissions of a file or a directory..
+func Chmod() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "change file mode bits",
+ Args: "perm paths...",
+ Detail: []string{
+ "Changes the permissions of the named files or directories to be equal to perm.",
+ "Only numerical permissions are supported.",
+ },
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ if len(args) < 2 {
+ return nil, ErrUsage
+ }
+
+ perm, err := strconv.ParseUint(args[0], 0, 32)
+ if err != nil || perm&uint64(fs.ModePerm) != perm {
+ return nil, fmt.Errorf("invalid mode: %s", args[0])
+ }
+
+ for _, arg := range args[1:] {
+ err := os.Chmod(s.Path(arg), fs.FileMode(perm))
+ if err != nil {
+ return nil, err
+ }
+ }
+ return nil, nil
+ })
+}
+
+// Cmp compares the contents of two files, or the contents of either the
+// "stdout" or "stderr" buffer and a file, returning a non-nil error if the
+// contents differ.
+func Cmp() Cmd {
+ return Command(
+ CmdUsage{
+ Args: "[-q] file1 file2",
+ Summary: "compare files for differences",
+ Detail: []string{
+ "By convention, file1 is the actual data and file2 is the expected data.",
+ "The command succeeds if the file contents are identical.",
+ "File1 can be 'stdout' or 'stderr' to compare the stdout or stderr buffer from the most recent command.",
+ },
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ return nil, doCompare(s, false, args...)
+ })
+}
+
+// Cmpenv is like Compare, but also performs environment substitutions
+// on the contents of both arguments.
+func Cmpenv() Cmd {
+ return Command(
+ CmdUsage{
+ Args: "[-q] file1 file2",
+ Summary: "compare files for differences, with environment expansion",
+ Detail: []string{
+ "By convention, file1 is the actual data and file2 is the expected data.",
+ "The command succeeds if the file contents are identical after substituting variables from the script environment.",
+ "File1 can be 'stdout' or 'stderr' to compare the script's stdout or stderr buffer.",
+ },
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ return nil, doCompare(s, true, args...)
+ })
+}
+
+func doCompare(s *State, env bool, args ...string) error {
+ quiet := false
+ if len(args) > 0 && args[0] == "-q" {
+ quiet = true
+ args = args[1:]
+ }
+ if len(args) != 2 {
+ return ErrUsage
+ }
+
+ name1, name2 := args[0], args[1]
+ var text1, text2 string
+ switch name1 {
+ case "stdout":
+ text1 = s.Stdout()
+ case "stderr":
+ text1 = s.Stderr()
+ default:
+ data, err := os.ReadFile(s.Path(name1))
+ if err != nil {
+ return err
+ }
+ text1 = string(data)
+ }
+
+ data, err := os.ReadFile(s.Path(name2))
+ if err != nil {
+ return err
+ }
+ text2 = string(data)
+
+ if env {
+ text1 = s.ExpandEnv(text1, false)
+ text2 = s.ExpandEnv(text2, false)
+ }
+
+ if text1 != text2 {
+ if !quiet {
+ diffText := diff.Diff(name1, []byte(text1), name2, []byte(text2))
+ s.Logf("%s\n", diffText)
+ }
+ return fmt.Errorf("%s and %s differ", name1, name2)
+ }
+ return nil
+}
+
+// Cp copies one or more files to a new location.
+func Cp() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "copy files to a target file or directory",
+ Args: "src... dst",
+ Detail: []string{
+ "src can include 'stdout' or 'stderr' to copy from the script's stdout or stderr buffer.",
+ },
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ if len(args) < 2 {
+ return nil, ErrUsage
+ }
+
+ dst := s.Path(args[len(args)-1])
+ info, err := os.Stat(dst)
+ dstDir := err == nil && info.IsDir()
+ if len(args) > 2 && !dstDir {
+ return nil, &fs.PathError{Op: "cp", Path: dst, Err: errors.New("destination is not a directory")}
+ }
+
+ for _, arg := range args[:len(args)-1] {
+ var (
+ src string
+ data []byte
+ mode fs.FileMode
+ )
+ switch arg {
+ case "stdout":
+ src = arg
+ data = []byte(s.Stdout())
+ mode = 0666
+ case "stderr":
+ src = arg
+ data = []byte(s.Stderr())
+ mode = 0666
+ default:
+ src = s.Path(arg)
+ info, err := os.Stat(src)
+ if err != nil {
+ return nil, err
+ }
+ mode = info.Mode() & 0777
+ data, err = os.ReadFile(src)
+ if err != nil {
+ return nil, err
+ }
+ }
+ targ := dst
+ if dstDir {
+ targ = filepath.Join(dst, filepath.Base(src))
+ }
+ err := os.WriteFile(targ, data, mode)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return nil, nil
+ })
+}
+
+// Echo writes its arguments to stdout, followed by a newline.
+func Echo() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "display a line of text",
+ Args: "string...",
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ var buf strings.Builder
+ for i, arg := range args {
+ if i > 0 {
+ buf.WriteString(" ")
+ }
+ buf.WriteString(arg)
+ }
+ buf.WriteString("\n")
+ out := buf.String()
+
+ // Stuff the result into a callback to satisfy the OutputCommandFunc
+ // interface, even though it isn't really asynchronous even if run in the
+ // background.
+ //
+ // Nobody should be running 'echo' as a background command, but it's not worth
+ // defining yet another interface, and also doesn't seem worth shoehorning
+ // into a SimpleCommand the way we did with Wait.
+ return func(*State) (stdout, stderr string, err error) {
+ return out, "", nil
+ }, nil
+ })
+}
+
+// Env sets or logs the values of environment variables.
+//
+// With no arguments, Env reports all variables in the environment.
+// "key=value" arguments set variables, and arguments without "="
+// cause the corresponding value to be printed to the stdout buffer.
+func Env() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "set or log the values of environment variables",
+ Args: "[key[=value]...]",
+ Detail: []string{
+ "With no arguments, print the script environment to the log.",
+ "Otherwise, add the listed key=value pairs to the environment or print the listed keys.",
+ },
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ out := new(strings.Builder)
+ if len(args) == 0 {
+ for _, kv := range s.env {
+ fmt.Fprintf(out, "%s\n", kv)
+ }
+ } else {
+ for _, env := range args {
+ i := strings.Index(env, "=")
+ if i < 0 {
+ // Display value instead of setting it.
+ fmt.Fprintf(out, "%s=%s\n", env, s.envMap[env])
+ continue
+ }
+ if err := s.Setenv(env[:i], env[i+1:]); err != nil {
+ return nil, err
+ }
+ }
+ }
+ var wait WaitFunc
+ if out.Len() > 0 || len(args) == 0 {
+ wait = func(*State) (stdout, stderr string, err error) {
+ return out.String(), "", nil
+ }
+ }
+ return wait, nil
+ })
+}
+
+// Exec runs an arbitrary executable as a subprocess.
+//
+// When the Script's context is canceled, Exec sends the interrupt signal, then
+// waits for up to the given delay for the subprocess to flush output before
+// terminating it with os.Kill.
+func Exec(cancel func(*exec.Cmd) error, waitDelay time.Duration) Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "run an executable program with arguments",
+ Args: "program [args...]",
+ Detail: []string{
+ "Note that 'exec' does not terminate the script (unlike Unix shells).",
+ },
+ Async: true,
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ if len(args) < 1 {
+ return nil, ErrUsage
+ }
+
+ // Use the script's PATH to look up the command if it contains a separator
+ // instead of the test process's PATH (see lookPath).
+ // Don't use filepath.Clean, since that changes "./foo" to "foo".
+ name := filepath.FromSlash(args[0])
+ path := name
+ if !strings.Contains(name, string(filepath.Separator)) {
+ var err error
+ path, err = lookPath(s, name)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return startCommand(s, name, path, args[1:], cancel, waitDelay)
+ })
+}
+
+func startCommand(s *State, name, path string, args []string, cancel func(*exec.Cmd) error, waitDelay time.Duration) (WaitFunc, error) {
+ var (
+ cmd *exec.Cmd
+ stdoutBuf, stderrBuf strings.Builder
+ )
+ for {
+ cmd = exec.CommandContext(s.Context(), path, args...)
+ if cancel == nil {
+ cmd.Cancel = nil
+ } else {
+ cmd.Cancel = func() error { return cancel(cmd) }
+ }
+ cmd.WaitDelay = waitDelay
+ cmd.Args[0] = name
+ cmd.Dir = s.Getwd()
+ cmd.Env = s.env
+ cmd.Stdout = &stdoutBuf
+ cmd.Stderr = &stderrBuf
+ err := cmd.Start()
+ if err == nil {
+ break
+ }
+ if isETXTBSY(err) {
+ // If the script (or its host process) just wrote the executable we're
+ // trying to run, a fork+exec in another thread may be holding open the FD
+ // that we used to write the executable (see https://go.dev/issue/22315).
+ // Since the descriptor should have CLOEXEC set, the problem should
+ // resolve as soon as the forked child reaches its exec call.
+ // Keep retrying until that happens.
+ } else {
+ return nil, err
+ }
+ }
+
+ wait := func(s *State) (stdout, stderr string, err error) {
+ err = cmd.Wait()
+ return stdoutBuf.String(), stderrBuf.String(), err
+ }
+ return wait, nil
+}
+
+// lookPath is (roughly) like exec.LookPath, but it uses the script's current
+// PATH to find the executable.
+func lookPath(s *State, command string) (string, error) {
+ var strEqual func(string, string) bool
+ if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
+ // Using GOOS as a proxy for case-insensitive file system.
+ // TODO(bcmills): Remove this assumption.
+ strEqual = strings.EqualFold
+ } else {
+ strEqual = func(a, b string) bool { return a == b }
+ }
+
+ var pathExt []string
+ var searchExt bool
+ var isExecutable func(os.FileInfo) bool
+ if runtime.GOOS == "windows" {
+ // Use the test process's PathExt instead of the script's.
+ // If PathExt is set in the command's environment, cmd.Start fails with
+ // "parameter is invalid". Not sure why.
+ // If the command already has an extension in PathExt (like "cmd.exe")
+ // don't search for other extensions (not "cmd.bat.exe").
+ pathExt = strings.Split(os.Getenv("PathExt"), string(filepath.ListSeparator))
+ searchExt = true
+ cmdExt := filepath.Ext(command)
+ for _, ext := range pathExt {
+ if strEqual(cmdExt, ext) {
+ searchExt = false
+ break
+ }
+ }
+ isExecutable = func(fi os.FileInfo) bool {
+ return fi.Mode().IsRegular()
+ }
+ } else {
+ isExecutable = func(fi os.FileInfo) bool {
+ return fi.Mode().IsRegular() && fi.Mode().Perm()&0111 != 0
+ }
+ }
+
+ pathEnv, _ := s.LookupEnv(pathEnvName())
+ for _, dir := range strings.Split(pathEnv, string(filepath.ListSeparator)) {
+ if searchExt {
+ ents, err := os.ReadDir(dir)
+ if err != nil {
+ continue
+ }
+ for _, ent := range ents {
+ for _, ext := range pathExt {
+ if !ent.IsDir() && strEqual(ent.Name(), command+ext) {
+ return dir + string(filepath.Separator) + ent.Name(), nil
+ }
+ }
+ }
+ } else {
+ path := dir + string(filepath.Separator) + command
+ if fi, err := os.Stat(path); err == nil && isExecutable(fi) {
+ return path, nil
+ }
+ }
+ }
+ return "", &exec.Error{Name: command, Err: exec.ErrNotFound}
+}
+
+// pathEnvName returns the platform-specific variable used by os/exec.LookPath
+// to look up executable names (either "PATH" or "path").
+//
+// TODO(bcmills): Investigate whether we can instead use PATH uniformly and
+// rewrite it to $path when executing subprocesses.
+func pathEnvName() string {
+ switch runtime.GOOS {
+ case "plan9":
+ return "path"
+ default:
+ return "PATH"
+ }
+}
+
+// Exists checks that the named file(s) exist.
+func Exists() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "check that files exist",
+ Args: "[-readonly] [-exec] file...",
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ var readonly, exec bool
+ loop:
+ for len(args) > 0 {
+ switch args[0] {
+ case "-readonly":
+ readonly = true
+ args = args[1:]
+ case "-exec":
+ exec = true
+ args = args[1:]
+ default:
+ break loop
+ }
+ }
+ if len(args) == 0 {
+ return nil, ErrUsage
+ }
+
+ for _, file := range args {
+ file = s.Path(file)
+ info, err := os.Stat(file)
+ if err != nil {
+ return nil, err
+ }
+ if readonly && info.Mode()&0222 != 0 {
+ return nil, fmt.Errorf("%s exists but is writable", file)
+ }
+ if exec && runtime.GOOS != "windows" && info.Mode()&0111 == 0 {
+ return nil, fmt.Errorf("%s exists but is not executable", file)
+ }
+ }
+
+ return nil, nil
+ })
+}
+
+// Grep checks that file content matches a regexp.
+// Like stdout/stderr and unlike Unix grep, it accepts Go regexp syntax.
+//
+// Grep does not modify the State's stdout or stderr buffers.
+// (Its output goes to the script log, not stdout.)
+func Grep() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "find lines in a file that match a pattern",
+ Args: matchUsage + " file",
+ Detail: []string{
+ "The command succeeds if at least one match (or the exact count, if given) is found.",
+ "The -q flag suppresses printing of matches.",
+ },
+ RegexpArgs: firstNonFlag,
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ return nil, match(s, args, "", "grep")
+ })
+}
+
+const matchUsage = "[-count=N] [-q] 'pattern'"
+
+// match implements the Grep, Stdout, and Stderr commands.
+func match(s *State, args []string, text, name string) error {
+ n := 0
+ if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") {
+ var err error
+ n, err = strconv.Atoi(args[0][len("-count="):])
+ if err != nil {
+ return fmt.Errorf("bad -count=: %v", err)
+ }
+ if n < 1 {
+ return fmt.Errorf("bad -count=: must be at least 1")
+ }
+ args = args[1:]
+ }
+ quiet := false
+ if len(args) >= 1 && args[0] == "-q" {
+ quiet = true
+ args = args[1:]
+ }
+
+ isGrep := name == "grep"
+
+ wantArgs := 1
+ if isGrep {
+ wantArgs = 2
+ }
+ if len(args) != wantArgs {
+ return ErrUsage
+ }
+
+ pattern := `(?m)` + args[0]
+ re, err := regexp.Compile(pattern)
+ if err != nil {
+ return err
+ }
+
+ if isGrep {
+ name = args[1] // for error messages
+ data, err := os.ReadFile(s.Path(args[1]))
+ if err != nil {
+ return err
+ }
+ text = string(data)
+ }
+
+ if n > 0 {
+ count := len(re.FindAllString(text, -1))
+ if count != n {
+ return fmt.Errorf("found %d matches for %#q in %s", count, pattern, name)
+ }
+ return nil
+ }
+
+ if !re.MatchString(text) {
+ return fmt.Errorf("no match for %#q in %s", pattern, name)
+ }
+
+ if !quiet {
+ // Print the lines containing the match.
+ loc := re.FindStringIndex(text)
+ for loc[0] > 0 && text[loc[0]-1] != '\n' {
+ loc[0]--
+ }
+ for loc[1] < len(text) && text[loc[1]] != '\n' {
+ loc[1]++
+ }
+ lines := strings.TrimSuffix(text[loc[0]:loc[1]], "\n")
+ s.Logf("matched: %s\n", lines)
+ }
+ return nil
+}
+
+// Help writes command documentation to the script log.
+func Help() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "log help text for commands and conditions",
+ Args: "[-v] name...",
+ Detail: []string{
+ "To display help for a specific condition, enclose it in brackets: 'help [amd64]'.",
+ "To display complete documentation when listing all commands, pass the -v flag.",
+ },
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ if s.engine == nil {
+ return nil, errors.New("no engine configured")
+ }
+
+ verbose := false
+ if len(args) > 0 {
+ verbose = true
+ if args[0] == "-v" {
+ args = args[1:]
+ }
+ }
+
+ var cmds, conds []string
+ for _, arg := range args {
+ if strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]") {
+ conds = append(conds, arg[1:len(arg)-1])
+ } else {
+ cmds = append(cmds, arg)
+ }
+ }
+
+ out := new(strings.Builder)
+
+ if len(conds) > 0 || (len(args) == 0 && len(s.engine.Conds) > 0) {
+ if conds == nil {
+ out.WriteString("conditions:\n\n")
+ }
+ s.engine.ListConds(out, s, conds...)
+ }
+
+ if len(cmds) > 0 || len(args) == 0 {
+ if len(args) == 0 {
+ out.WriteString("\ncommands:\n\n")
+ }
+ s.engine.ListCmds(out, verbose, cmds...)
+ }
+
+ wait := func(*State) (stdout, stderr string, err error) {
+ return out.String(), "", nil
+ }
+ return wait, nil
+ })
+}
+
+// Mkdir creates a directory and any needed parent directories.
+func Mkdir() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "create directories, if they do not already exist",
+ Args: "path...",
+ Detail: []string{
+ "Unlike Unix mkdir, parent directories are always created if needed.",
+ },
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ if len(args) < 1 {
+ return nil, ErrUsage
+ }
+ for _, arg := range args {
+ if err := os.MkdirAll(s.Path(arg), 0777); err != nil {
+ return nil, err
+ }
+ }
+ return nil, nil
+ })
+}
+
+// Mv renames an existing file or directory to a new path.
+func Mv() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "rename a file or directory to a new path",
+ Args: "old new",
+ Detail: []string{
+ "OS-specific restrictions may apply when old and new are in different directories.",
+ },
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ if len(args) != 2 {
+ return nil, ErrUsage
+ }
+ return nil, os.Rename(s.Path(args[0]), s.Path(args[1]))
+ })
+}
+
+// Program returns a new command that runs the named program, found from the
+// host process's PATH (not looked up in the script's PATH).
+func Program(name string, cancel func(*exec.Cmd) error, waitDelay time.Duration) Cmd {
+ var (
+ shortName string
+ summary string
+ lookPathOnce sync.Once
+ path string
+ pathErr error
+ )
+ if filepath.IsAbs(name) {
+ lookPathOnce.Do(func() { path = filepath.Clean(name) })
+ shortName = strings.TrimSuffix(filepath.Base(path), ".exe")
+ summary = "run the '" + shortName + "' program provided by the script host"
+ } else {
+ shortName = name
+ summary = "run the '" + shortName + "' program from the script host's PATH"
+ }
+
+ return Command(
+ CmdUsage{
+ Summary: summary,
+ Args: "[args...]",
+ Async: true,
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ lookPathOnce.Do(func() {
+ path, pathErr = exec.LookPath(name)
+ })
+ if pathErr != nil {
+ return nil, pathErr
+ }
+ return startCommand(s, shortName, path, args, cancel, waitDelay)
+ })
+}
+
+// Replace replaces all occurrences of a string in a file with another string.
+func Replace() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "replace strings in a file",
+ Args: "[old new]... file",
+ Detail: []string{
+ "The 'old' and 'new' arguments are unquoted as if in quoted Go strings.",
+ },
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ if len(args)%2 != 1 {
+ return nil, ErrUsage
+ }
+
+ oldNew := make([]string, 0, len(args)-1)
+ for _, arg := range args[:len(args)-1] {
+ s, err := strconv.Unquote(`"` + arg + `"`)
+ if err != nil {
+ return nil, err
+ }
+ oldNew = append(oldNew, s)
+ }
+
+ r := strings.NewReplacer(oldNew...)
+ file := s.Path(args[len(args)-1])
+
+ data, err := os.ReadFile(file)
+ if err != nil {
+ return nil, err
+ }
+ replaced := r.Replace(string(data))
+
+ return nil, os.WriteFile(file, []byte(replaced), 0666)
+ })
+}
+
+// Rm removes a file or directory.
+//
+// If a directory, Rm also recursively removes that directory's
+// contents.
+func Rm() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "remove a file or directory",
+ Args: "path...",
+ Detail: []string{
+ "If the path is a directory, its contents are removed recursively.",
+ },
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ if len(args) < 1 {
+ return nil, ErrUsage
+ }
+ for _, arg := range args {
+ if err := removeAll(s.Path(arg)); err != nil {
+ return nil, err
+ }
+ }
+ return nil, nil
+ })
+}
+
+// removeAll removes dir and all files and directories it contains.
+//
+// Unlike os.RemoveAll, removeAll attempts to make the directories writable if
+// needed in order to remove their contents.
+func removeAll(dir string) error {
+ // module cache has 0444 directories;
+ // make them writable in order to remove content.
+ filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error {
+ // chmod not only directories, but also things that we couldn't even stat
+ // due to permission errors: they may also be unreadable directories.
+ if err != nil || info.IsDir() {
+ os.Chmod(path, 0777)
+ }
+ return nil
+ })
+ return robustio.RemoveAll(dir)
+}
+
+// Sleep sleeps for the given Go duration or until the script's context is
+// cancelled, whichever happens first.
+func Sleep() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "sleep for a specified duration",
+ Args: "duration",
+ Detail: []string{
+ "The duration must be given as a Go time.Duration string.",
+ },
+ Async: true,
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ if len(args) != 1 {
+ return nil, ErrUsage
+ }
+
+ d, err := time.ParseDuration(args[0])
+ if err != nil {
+ return nil, err
+ }
+
+ timer := time.NewTimer(d)
+ wait := func(s *State) (stdout, stderr string, err error) {
+ ctx := s.Context()
+ select {
+ case <-ctx.Done():
+ timer.Stop()
+ return "", "", ctx.Err()
+ case <-timer.C:
+ return "", "", nil
+ }
+ }
+ return wait, nil
+ })
+}
+
+// Stderr searches for a regular expression in the stderr buffer.
+func Stderr() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "find lines in the stderr buffer that match a pattern",
+ Args: matchUsage + " file",
+ Detail: []string{
+ "The command succeeds if at least one match (or the exact count, if given) is found.",
+ "The -q flag suppresses printing of matches.",
+ },
+ RegexpArgs: firstNonFlag,
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ return nil, match(s, args, s.Stderr(), "stderr")
+ })
+}
+
+// Stdout searches for a regular expression in the stdout buffer.
+func Stdout() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "find lines in the stdout buffer that match a pattern",
+ Args: matchUsage + " file",
+ Detail: []string{
+ "The command succeeds if at least one match (or the exact count, if given) is found.",
+ "The -q flag suppresses printing of matches.",
+ },
+ RegexpArgs: firstNonFlag,
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ return nil, match(s, args, s.Stdout(), "stdout")
+ })
+}
+
+// Stop returns a sentinel error that causes script execution to halt
+// and s.Execute to return with a nil error.
+func Stop() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "stop execution of the script",
+ Args: "[msg]",
+ Detail: []string{
+ "The message is written to the script log, but no error is reported from the script engine.",
+ },
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ if len(args) > 1 {
+ return nil, ErrUsage
+ }
+ // TODO(bcmills): The argument passed to stop seems redundant with comments.
+ // Either use it systematically or remove it.
+ if len(args) == 1 {
+ return nil, stopError{msg: args[0]}
+ }
+ return nil, stopError{}
+ })
+}
+
+// stopError is the sentinel error type returned by the Stop command.
+type stopError struct {
+ msg string
+}
+
+func (s stopError) Error() string {
+ if s.msg == "" {
+ return "stop"
+ }
+ return "stop: " + s.msg
+}
+
+// Symlink creates a symbolic link.
+func Symlink() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "create a symlink",
+ Args: "path -> target",
+ Detail: []string{
+ "Creates path as a symlink to target.",
+ "The '->' token (like in 'ls -l' output on Unix) is required.",
+ },
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ if len(args) != 3 || args[1] != "->" {
+ return nil, ErrUsage
+ }
+
+ // Note that the link target args[2] is not interpreted with s.Path:
+ // it will be interpreted relative to the directory file is in.
+ return nil, os.Symlink(filepath.FromSlash(args[2]), s.Path(args[0]))
+ })
+}
+
+// Wait waits for the completion of background commands.
+//
+// When Wait returns, the stdout and stderr buffers contain the concatenation of
+// the background commands' respective outputs in the order in which those
+// commands were started.
+func Wait() Cmd {
+ return Command(
+ CmdUsage{
+ Summary: "wait for completion of background commands",
+ Args: "",
+ Detail: []string{
+ "Waits for all background commands to complete.",
+ "The output (and any error) from each command is printed to the log in the order in which the commands were started.",
+ "After the call to 'wait', the script's stdout and stderr buffers contain the concatenation of the background commands' outputs.",
+ },
+ },
+ func(s *State, args ...string) (WaitFunc, error) {
+ if len(args) > 0 {
+ return nil, ErrUsage
+ }
+
+ var stdouts, stderrs []string
+ var errs []*CommandError
+ for _, bg := range s.background {
+ stdout, stderr, err := bg.wait(s)
+
+ beforeArgs := ""
+ if len(bg.args) > 0 {
+ beforeArgs = " "
+ }
+ s.Logf("[background] %s%s%s\n", bg.name, beforeArgs, quoteArgs(bg.args))
+
+ if stdout != "" {
+ s.Logf("[stdout]\n%s", stdout)
+ stdouts = append(stdouts, stdout)
+ }
+ if stderr != "" {
+ s.Logf("[stderr]\n%s", stderr)
+ stderrs = append(stderrs, stderr)
+ }
+ if err != nil {
+ s.Logf("[%v]\n", err)
+ }
+ if cmdErr := checkStatus(bg.command, err); cmdErr != nil {
+ errs = append(errs, cmdErr.(*CommandError))
+ }
+ }
+
+ s.stdout = strings.Join(stdouts, "")
+ s.stderr = strings.Join(stderrs, "")
+ s.background = nil
+ if len(errs) > 0 {
+ return nil, waitError{errs: errs}
+ }
+ return nil, nil
+ })
+}
+
+// A waitError wraps one or more errors returned by background commands.
+type waitError struct {
+ errs []*CommandError
+}
+
+func (w waitError) Error() string {
+ b := new(strings.Builder)
+ for i, err := range w.errs {
+ if i != 0 {
+ b.WriteString("\n")
+ }
+ b.WriteString(err.Error())
+ }
+ return b.String()
+}
+
+func (w waitError) Unwrap() error {
+ if len(w.errs) == 1 {
+ return w.errs[0]
+ }
+ return nil
+}