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

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

View as plain text