...
▽

Source file src/cmd/internal/script/cmds.go

Documentation: cmd/internal/script

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

View as plain text