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 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  		return stdoutBuf.String(), stderrBuf.String(), err
   472  	}
   473  	return wait, nil
   474  }
   475  
   476  // lookPath is (roughly) like exec.LookPath, but it uses the script's current
   477  // PATH to find the executable.
   478  func lookPath(s *State, command string) (string, error) {
   479  	var strEqual func(string, string) bool
   480  	if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
   481  		// Using GOOS as a proxy for case-insensitive file system.
   482  		// TODO(bcmills): Remove this assumption.
   483  		strEqual = strings.EqualFold
   484  	} else {
   485  		strEqual = func(a, b string) bool { return a == b }
   486  	}
   487  
   488  	var pathExt []string
   489  	var searchExt bool
   490  	var isExecutable func(os.FileInfo) bool
   491  	if runtime.GOOS == "windows" {
   492  		// Use the test process's PathExt instead of the script's.
   493  		// If PathExt is set in the command's environment, cmd.Start fails with
   494  		// "parameter is invalid". Not sure why.
   495  		// If the command already has an extension in PathExt (like "cmd.exe")
   496  		// don't search for other extensions (not "cmd.bat.exe").
   497  		pathExt = strings.Split(os.Getenv("PathExt"), string(filepath.ListSeparator))
   498  		searchExt = true
   499  		cmdExt := filepath.Ext(command)
   500  		for _, ext := range pathExt {
   501  			if strEqual(cmdExt, ext) {
   502  				searchExt = false
   503  				break
   504  			}
   505  		}
   506  		isExecutable = func(fi os.FileInfo) bool {
   507  			return fi.Mode().IsRegular()
   508  		}
   509  	} else {
   510  		isExecutable = func(fi os.FileInfo) bool {
   511  			return fi.Mode().IsRegular() && fi.Mode().Perm()&0111 != 0
   512  		}
   513  	}
   514  
   515  	pathEnv, _ := s.LookupEnv(pathEnvName())
   516  	for _, dir := range strings.Split(pathEnv, string(filepath.ListSeparator)) {
   517  		if dir == "" {
   518  			continue
   519  		}
   520  
   521  		// Determine whether dir needs a trailing path separator.
   522  		// Note: we avoid filepath.Join in this function because it cleans the
   523  		// result: we want to preserve the exact dir prefix from the environment.
   524  		sep := string(filepath.Separator)
   525  		if os.IsPathSeparator(dir[len(dir)-1]) {
   526  			sep = ""
   527  		}
   528  
   529  		if searchExt {
   530  			ents, err := os.ReadDir(dir)
   531  			if err != nil {
   532  				continue
   533  			}
   534  			for _, ent := range ents {
   535  				for _, ext := range pathExt {
   536  					if !ent.IsDir() && strEqual(ent.Name(), command+ext) {
   537  						return dir + sep + ent.Name(), nil
   538  					}
   539  				}
   540  			}
   541  		} else {
   542  			path := dir + sep + command
   543  			if fi, err := os.Stat(path); err == nil && isExecutable(fi) {
   544  				return path, nil
   545  			}
   546  		}
   547  	}
   548  	return "", &exec.Error{Name: command, Err: exec.ErrNotFound}
   549  }
   550  
   551  // pathEnvName returns the platform-specific variable used by os/exec.LookPath
   552  // to look up executable names (either "PATH" or "path").
   553  //
   554  // TODO(bcmills): Investigate whether we can instead use PATH uniformly and
   555  // rewrite it to $path when executing subprocesses.
   556  func pathEnvName() string {
   557  	switch runtime.GOOS {
   558  	case "plan9":
   559  		return "path"
   560  	default:
   561  		return "PATH"
   562  	}
   563  }
   564  
   565  // Exists checks that the named file(s) exist.
   566  func Exists() Cmd {
   567  	return Command(
   568  		CmdUsage{
   569  			Summary: "check that files exist",
   570  			Args:    "[-readonly] [-exec] file...",
   571  		},
   572  		func(s *State, args ...string) (WaitFunc, error) {
   573  			var readonly, exec bool
   574  		loop:
   575  			for len(args) > 0 {
   576  				switch args[0] {
   577  				case "-readonly":
   578  					readonly = true
   579  					args = args[1:]
   580  				case "-exec":
   581  					exec = true
   582  					args = args[1:]
   583  				default:
   584  					break loop
   585  				}
   586  			}
   587  			if len(args) == 0 {
   588  				return nil, ErrUsage
   589  			}
   590  
   591  			for _, file := range args {
   592  				file = s.Path(file)
   593  				info, err := os.Stat(file)
   594  				if err != nil {
   595  					return nil, err
   596  				}
   597  				if readonly && info.Mode()&0222 != 0 {
   598  					return nil, fmt.Errorf("%s exists but is writable", file)
   599  				}
   600  				if exec && runtime.GOOS != "windows" && info.Mode()&0111 == 0 {
   601  					return nil, fmt.Errorf("%s exists but is not executable", file)
   602  				}
   603  			}
   604  
   605  			return nil, nil
   606  		})
   607  }
   608  
   609  // Grep checks that file content matches a regexp.
   610  // Like stdout/stderr and unlike Unix grep, it accepts Go regexp syntax.
   611  //
   612  // Grep does not modify the State's stdout or stderr buffers.
   613  // (Its output goes to the script log, not stdout.)
   614  func Grep() Cmd {
   615  	return Command(
   616  		CmdUsage{
   617  			Summary: "find lines in a file that match a pattern",
   618  			Args:    matchUsage + " file",
   619  			Detail: []string{
   620  				"The command succeeds if at least one match (or the exact count, if given) is found.",
   621  				"The -q flag suppresses printing of matches.",
   622  			},
   623  			RegexpArgs: firstNonFlag,
   624  		},
   625  		func(s *State, args ...string) (WaitFunc, error) {
   626  			return nil, match(s, args, "", "grep")
   627  		})
   628  }
   629  
   630  const matchUsage = "[-count=N] [-q] 'pattern'"
   631  
   632  // match implements the Grep, Stdout, and Stderr commands.
   633  func match(s *State, args []string, text, name string) error {
   634  	n := 0
   635  	if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") {
   636  		var err error
   637  		n, err = strconv.Atoi(args[0][len("-count="):])
   638  		if err != nil {
   639  			return fmt.Errorf("bad -count=: %v", err)
   640  		}
   641  		if n < 1 {
   642  			return fmt.Errorf("bad -count=: must be at least 1")
   643  		}
   644  		args = args[1:]
   645  	}
   646  	quiet := false
   647  	if len(args) >= 1 && args[0] == "-q" {
   648  		quiet = true
   649  		args = args[1:]
   650  	}
   651  
   652  	isGrep := name == "grep"
   653  
   654  	wantArgs := 1
   655  	if isGrep {
   656  		wantArgs = 2
   657  	}
   658  	if len(args) != wantArgs {
   659  		return ErrUsage
   660  	}
   661  
   662  	pattern := `(?m)` + args[0]
   663  	re, err := regexp.Compile(pattern)
   664  	if err != nil {
   665  		return err
   666  	}
   667  
   668  	if isGrep {
   669  		name = args[1] // for error messages
   670  		data, err := os.ReadFile(s.Path(args[1]))
   671  		if err != nil {
   672  			return err
   673  		}
   674  		text = string(data)
   675  	}
   676  
   677  	if n > 0 {
   678  		count := len(re.FindAllString(text, -1))
   679  		if count != n {
   680  			return fmt.Errorf("found %d matches for %#q in %s", count, pattern, name)
   681  		}
   682  		return nil
   683  	}
   684  
   685  	if !re.MatchString(text) {
   686  		return fmt.Errorf("no match for %#q in %s", pattern, name)
   687  	}
   688  
   689  	if !quiet {
   690  		// Print the lines containing the match.
   691  		loc := re.FindStringIndex(text)
   692  		for loc[0] > 0 && text[loc[0]-1] != '\n' {
   693  			loc[0]--
   694  		}
   695  		for loc[1] < len(text) && text[loc[1]] != '\n' {
   696  			loc[1]++
   697  		}
   698  		lines := strings.TrimSuffix(text[loc[0]:loc[1]], "\n")
   699  		s.Logf("matched: %s\n", lines)
   700  	}
   701  	return nil
   702  }
   703  
   704  // Help writes command documentation to the script log.
   705  func Help() Cmd {
   706  	return Command(
   707  		CmdUsage{
   708  			Summary: "log help text for commands and conditions",
   709  			Args:    "[-v] name...",
   710  			Detail: []string{
   711  				"To display help for a specific condition, enclose it in brackets: 'help [amd64]'.",
   712  				"To display complete documentation when listing all commands, pass the -v flag.",
   713  			},
   714  		},
   715  		func(s *State, args ...string) (WaitFunc, error) {
   716  			if s.engine == nil {
   717  				return nil, errors.New("no engine configured")
   718  			}
   719  
   720  			verbose := false
   721  			if len(args) > 0 {
   722  				verbose = true
   723  				if args[0] == "-v" {
   724  					args = args[1:]
   725  				}
   726  			}
   727  
   728  			var cmds, conds []string
   729  			for _, arg := range args {
   730  				if strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]") {
   731  					conds = append(conds, arg[1:len(arg)-1])
   732  				} else {
   733  					cmds = append(cmds, arg)
   734  				}
   735  			}
   736  
   737  			out := new(strings.Builder)
   738  
   739  			if len(conds) > 0 || (len(args) == 0 && len(s.engine.Conds) > 0) {
   740  				if conds == nil {
   741  					out.WriteString("conditions:\n\n")
   742  				}
   743  				s.engine.ListConds(out, s, conds...)
   744  			}
   745  
   746  			if len(cmds) > 0 || len(args) == 0 {
   747  				if len(args) == 0 {
   748  					out.WriteString("\ncommands:\n\n")
   749  				}
   750  				s.engine.ListCmds(out, verbose, cmds...)
   751  			}
   752  
   753  			wait := func(*State) (stdout, stderr string, err error) {
   754  				return out.String(), "", nil
   755  			}
   756  			return wait, nil
   757  		})
   758  }
   759  
   760  // Mkdir creates a directory and any needed parent directories.
   761  func Mkdir() Cmd {
   762  	return Command(
   763  		CmdUsage{
   764  			Summary: "create directories, if they do not already exist",
   765  			Args:    "path...",
   766  			Detail: []string{
   767  				"Unlike Unix mkdir, parent directories are always created if needed.",
   768  			},
   769  		},
   770  		func(s *State, args ...string) (WaitFunc, error) {
   771  			if len(args) < 1 {
   772  				return nil, ErrUsage
   773  			}
   774  			for _, arg := range args {
   775  				if err := os.MkdirAll(s.Path(arg), 0777); err != nil {
   776  					return nil, err
   777  				}
   778  			}
   779  			return nil, nil
   780  		})
   781  }
   782  
   783  // Mv renames an existing file or directory to a new path.
   784  func Mv() Cmd {
   785  	return Command(
   786  		CmdUsage{
   787  			Summary: "rename a file or directory to a new path",
   788  			Args:    "old new",
   789  			Detail: []string{
   790  				"OS-specific restrictions may apply when old and new are in different directories.",
   791  			},
   792  		},
   793  		func(s *State, args ...string) (WaitFunc, error) {
   794  			if len(args) != 2 {
   795  				return nil, ErrUsage
   796  			}
   797  			return nil, os.Rename(s.Path(args[0]), s.Path(args[1]))
   798  		})
   799  }
   800  
   801  // Program returns a new command that runs the named program, found from the
   802  // host process's PATH (not looked up in the script's PATH).
   803  func Program(name string, cancel func(*exec.Cmd) error, waitDelay time.Duration) Cmd {
   804  	var (
   805  		shortName    string
   806  		summary      string
   807  		lookPathOnce sync.Once
   808  		path         string
   809  		pathErr      error
   810  	)
   811  	if filepath.IsAbs(name) {
   812  		lookPathOnce.Do(func() { path = filepath.Clean(name) })
   813  		shortName = strings.TrimSuffix(filepath.Base(path), ".exe")
   814  		summary = "run the '" + shortName + "' program provided by the script host"
   815  	} else {
   816  		shortName = name
   817  		summary = "run the '" + shortName + "' program from the script host's PATH"
   818  	}
   819  
   820  	return Command(
   821  		CmdUsage{
   822  			Summary: summary,
   823  			Args:    "[args...]",
   824  			Async:   true,
   825  		},
   826  		func(s *State, args ...string) (WaitFunc, error) {
   827  			lookPathOnce.Do(func() {
   828  				path, pathErr = pathcache.LookPath(name)
   829  			})
   830  			if pathErr != nil {
   831  				return nil, pathErr
   832  			}
   833  			return startCommand(s, shortName, path, args, cancel, waitDelay)
   834  		})
   835  }
   836  
   837  // Replace replaces all occurrences of a string in a file with another string.
   838  func Replace() Cmd {
   839  	return Command(
   840  		CmdUsage{
   841  			Summary: "replace strings in a file",
   842  			Args:    "[old new]... file",
   843  			Detail: []string{
   844  				"The 'old' and 'new' arguments are unquoted as if in quoted Go strings.",
   845  			},
   846  		},
   847  		func(s *State, args ...string) (WaitFunc, error) {
   848  			if len(args)%2 != 1 {
   849  				return nil, ErrUsage
   850  			}
   851  
   852  			oldNew := make([]string, 0, len(args)-1)
   853  			for _, arg := range args[:len(args)-1] {
   854  				s, err := strconv.Unquote(`"` + arg + `"`)
   855  				if err != nil {
   856  					return nil, err
   857  				}
   858  				oldNew = append(oldNew, s)
   859  			}
   860  
   861  			r := strings.NewReplacer(oldNew...)
   862  			file := s.Path(args[len(args)-1])
   863  
   864  			data, err := os.ReadFile(file)
   865  			if err != nil {
   866  				return nil, err
   867  			}
   868  			replaced := r.Replace(string(data))
   869  
   870  			return nil, os.WriteFile(file, []byte(replaced), 0666)
   871  		})
   872  }
   873  
   874  // Rm removes a file or directory.
   875  //
   876  // If a directory, Rm also recursively removes that directory's
   877  // contents.
   878  func Rm() Cmd {
   879  	return Command(
   880  		CmdUsage{
   881  			Summary: "remove a file or directory",
   882  			Args:    "path...",
   883  			Detail: []string{
   884  				"If the path is a directory, its contents are removed recursively.",
   885  			},
   886  		},
   887  		func(s *State, args ...string) (WaitFunc, error) {
   888  			if len(args) < 1 {
   889  				return nil, ErrUsage
   890  			}
   891  			for _, arg := range args {
   892  				if err := removeAll(s.Path(arg)); err != nil {
   893  					return nil, err
   894  				}
   895  			}
   896  			return nil, nil
   897  		})
   898  }
   899  
   900  // removeAll removes dir and all files and directories it contains.
   901  //
   902  // Unlike os.RemoveAll, removeAll attempts to make the directories writable if
   903  // needed in order to remove their contents.
   904  func removeAll(dir string) error {
   905  	// module cache has 0444 directories;
   906  	// make them writable in order to remove content.
   907  	filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error {
   908  		// chmod not only directories, but also things that we couldn't even stat
   909  		// due to permission errors: they may also be unreadable directories.
   910  		if err != nil || info.IsDir() {
   911  			os.Chmod(path, 0777)
   912  		}
   913  		return nil
   914  	})
   915  	return robustio.RemoveAll(dir)
   916  }
   917  
   918  // Sleep sleeps for the given Go duration or until the script's context is
   919  // canceled, whichever happens first.
   920  func Sleep() Cmd {
   921  	return Command(
   922  		CmdUsage{
   923  			Summary: "sleep for a specified duration",
   924  			Args:    "duration",
   925  			Detail: []string{
   926  				"The duration must be given as a Go time.Duration string.",
   927  			},
   928  			Async: true,
   929  		},
   930  		func(s *State, args ...string) (WaitFunc, error) {
   931  			if len(args) != 1 {
   932  				return nil, ErrUsage
   933  			}
   934  
   935  			d, err := time.ParseDuration(args[0])
   936  			if err != nil {
   937  				return nil, err
   938  			}
   939  
   940  			timer := time.NewTimer(d)
   941  			wait := func(s *State) (stdout, stderr string, err error) {
   942  				ctx := s.Context()
   943  				select {
   944  				case <-ctx.Done():
   945  					timer.Stop()
   946  					return "", "", ctx.Err()
   947  				case <-timer.C:
   948  					return "", "", nil
   949  				}
   950  			}
   951  			return wait, nil
   952  		})
   953  }
   954  
   955  // Stderr searches for a regular expression in the stderr buffer.
   956  func Stderr() Cmd {
   957  	return Command(
   958  		CmdUsage{
   959  			Summary: "find lines in the stderr buffer that match a pattern",
   960  			Args:    matchUsage + " file",
   961  			Detail: []string{
   962  				"The command succeeds if at least one match (or the exact count, if given) is found.",
   963  				"The -q flag suppresses printing of matches.",
   964  			},
   965  			RegexpArgs: firstNonFlag,
   966  		},
   967  		func(s *State, args ...string) (WaitFunc, error) {
   968  			return nil, match(s, args, s.Stderr(), "stderr")
   969  		})
   970  }
   971  
   972  // Stdout searches for a regular expression in the stdout buffer.
   973  func Stdout() Cmd {
   974  	return Command(
   975  		CmdUsage{
   976  			Summary: "find lines in the stdout buffer that match a pattern",
   977  			Args:    matchUsage + " file",
   978  			Detail: []string{
   979  				"The command succeeds if at least one match (or the exact count, if given) is found.",
   980  				"The -q flag suppresses printing of matches.",
   981  			},
   982  			RegexpArgs: firstNonFlag,
   983  		},
   984  		func(s *State, args ...string) (WaitFunc, error) {
   985  			return nil, match(s, args, s.Stdout(), "stdout")
   986  		})
   987  }
   988  
   989  // Stop returns a sentinel error that causes script execution to halt
   990  // and s.Execute to return with a nil error.
   991  func Stop() Cmd {
   992  	return Command(
   993  		CmdUsage{
   994  			Summary: "stop execution of the script",
   995  			Args:    "[msg]",
   996  			Detail: []string{
   997  				"The message is written to the script log, but no error is reported from the script engine.",
   998  			},
   999  		},
  1000  		func(s *State, args ...string) (WaitFunc, error) {
  1001  			if len(args) > 1 {
  1002  				return nil, ErrUsage
  1003  			}
  1004  			// TODO(bcmills): The argument passed to stop seems redundant with comments.
  1005  			// Either use it systematically or remove it.
  1006  			if len(args) == 1 {
  1007  				return nil, stopError{msg: args[0]}
  1008  			}
  1009  			return nil, stopError{}
  1010  		})
  1011  }
  1012  
  1013  // stopError is the sentinel error type returned by the Stop command.
  1014  type stopError struct {
  1015  	msg string
  1016  }
  1017  
  1018  func (s stopError) Error() string {
  1019  	if s.msg == "" {
  1020  		return "stop"
  1021  	}
  1022  	return "stop: " + s.msg
  1023  }
  1024  
  1025  // Symlink creates a symbolic link.
  1026  func Symlink() Cmd {
  1027  	return Command(
  1028  		CmdUsage{
  1029  			Summary: "create a symlink",
  1030  			Args:    "path -> target",
  1031  			Detail: []string{
  1032  				"Creates path as a symlink to target.",
  1033  				"The '->' token (like in 'ls -l' output on Unix) is required.",
  1034  			},
  1035  		},
  1036  		func(s *State, args ...string) (WaitFunc, error) {
  1037  			if len(args) != 3 || args[1] != "->" {
  1038  				return nil, ErrUsage
  1039  			}
  1040  
  1041  			// Note that the link target args[2] is not interpreted with s.Path:
  1042  			// it will be interpreted relative to the directory file is in.
  1043  			return nil, os.Symlink(filepath.FromSlash(args[2]), s.Path(args[0]))
  1044  		})
  1045  }
  1046  
  1047  // Wait waits for the completion of background commands.
  1048  //
  1049  // When Wait returns, the stdout and stderr buffers contain the concatenation of
  1050  // the background commands' respective outputs in the order in which those
  1051  // commands were started.
  1052  func Wait() Cmd {
  1053  	return Command(
  1054  		CmdUsage{
  1055  			Summary: "wait for completion of background commands",
  1056  			Args:    "",
  1057  			Detail: []string{
  1058  				"Waits for all background commands to complete.",
  1059  				"The output (and any error) from each command is printed to the log in the order in which the commands were started.",
  1060  				"After the call to 'wait', the script's stdout and stderr buffers contain the concatenation of the background commands' outputs.",
  1061  			},
  1062  		},
  1063  		func(s *State, args ...string) (WaitFunc, error) {
  1064  			if len(args) > 0 {
  1065  				return nil, ErrUsage
  1066  			}
  1067  
  1068  			var stdouts, stderrs []string
  1069  			var errs []*CommandError
  1070  			for _, bg := range s.background {
  1071  				stdout, stderr, err := bg.wait(s)
  1072  
  1073  				beforeArgs := ""
  1074  				if len(bg.args) > 0 {
  1075  					beforeArgs = " "
  1076  				}
  1077  				s.Logf("[background] %s%s%s\n", bg.name, beforeArgs, quoteArgs(bg.args))
  1078  
  1079  				if stdout != "" {
  1080  					s.Logf("[stdout]\n%s", stdout)
  1081  					stdouts = append(stdouts, stdout)
  1082  				}
  1083  				if stderr != "" {
  1084  					s.Logf("[stderr]\n%s", stderr)
  1085  					stderrs = append(stderrs, stderr)
  1086  				}
  1087  				if err != nil {
  1088  					s.Logf("[%v]\n", err)
  1089  				}
  1090  				if cmdErr := checkStatus(bg.command, err); cmdErr != nil {
  1091  					errs = append(errs, cmdErr.(*CommandError))
  1092  				}
  1093  			}
  1094  
  1095  			s.stdout = strings.Join(stdouts, "")
  1096  			s.stderr = strings.Join(stderrs, "")
  1097  			s.background = nil
  1098  			if len(errs) > 0 {
  1099  				return nil, waitError{errs: errs}
  1100  			}
  1101  			return nil, nil
  1102  		})
  1103  }
  1104  
  1105  // A waitError wraps one or more errors returned by background commands.
  1106  type waitError struct {
  1107  	errs []*CommandError
  1108  }
  1109  
  1110  func (w waitError) Error() string {
  1111  	b := new(strings.Builder)
  1112  	for i, err := range w.errs {
  1113  		if i != 0 {
  1114  			b.WriteString("\n")
  1115  		}
  1116  		b.WriteString(err.Error())
  1117  	}
  1118  	return b.String()
  1119  }
  1120  
  1121  func (w waitError) Unwrap() error {
  1122  	if len(w.errs) == 1 {
  1123  		return w.errs[0]
  1124  	}
  1125  	return nil
  1126  }
  1127  

View as plain text