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  }