Full Code of rsc/script for AI

main 334f6c18cff3 cached
28 files
83.8 KB
24.8k tokens
130 symbols
1 requests
Download .txt
Repository: rsc/script
Branch: main
Commit: 334f6c18cff3
Files: 28
Total size: 83.8 KB

Directory structure:
gitextract_2vdpsotp/

├── LICENSE
├── README.md
├── cmds.go
├── cmds_other.go
├── cmds_posix.go
├── conds.go
├── engine.go
├── errors.go
├── go.mod
├── go.sum
├── internal/
│   └── diff/
│       ├── diff.go
│       ├── diff_test.go
│       └── testdata/
│           ├── allnew.txt
│           ├── allold.txt
│           ├── basic.txt
│           ├── dups.txt
│           ├── end.txt
│           ├── eof.txt
│           ├── eof1.txt
│           ├── eof2.txt
│           ├── long.txt
│           ├── same.txt
│           ├── start.txt
│           └── triv.txt
├── scripttest/
│   ├── scripttest.go
│   ├── scripttest_test.go
│   └── testdata/
│       └── basic.txt
└── state.go

================================================
FILE CONTENTS
================================================

================================================
FILE: LICENSE
================================================
Copyright (c) 2009 The Go Authors. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

   * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
   * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
   * Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


================================================
FILE: README.md
================================================
This is a copy of cmd/go/internal/script.

See <https://pkg.go.dev/rsc.io/script> and <https://pkg.go.dev/rsc.io/script/scripttest>.
Posting it here makes it available for others to try
without us committing to officially supporting it.
We have been using it in the go command for many years now;
the code is quite stable.
Ironically, it has very few tests.

<https://github.com/rogpeppe/go-internal/testscript> is a port
of an earlier version of the go command script language.


================================================
FILE: cmds.go
================================================
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package script

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"

	"rsc.io/script/internal/diff"
)

// DefaultCmds returns a set of broadly useful script commands.
//
// Run the 'help' command within a script engine to view a list of the available
// commands.
func DefaultCmds() map[string]Cmd {
	return map[string]Cmd{
		"cat":     Cat(),
		"cd":      Cd(),
		"chmod":   Chmod(),
		"cmp":     Cmp(),
		"cmpenv":  Cmpenv(),
		"cp":      Cp(),
		"echo":    Echo(),
		"env":     Env(),
		"exec":    Exec(func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }, 100*time.Millisecond), // arbitrary grace period
		"exists":  Exists(),
		"grep":    Grep(),
		"help":    Help(),
		"mkdir":   Mkdir(),
		"mv":      Mv(),
		"rm":      Rm(),
		"replace": Replace(),
		"sleep":   Sleep(),
		"stderr":  Stderr(),
		"stdout":  Stdout(),
		"stop":    Stop(),
		"symlink": Symlink(),
		"wait":    Wait(),
	}
}

// Command returns a new Cmd with a Usage method that returns a copy of the
// given CmdUsage and a Run method calls the given function.
func Command(usage CmdUsage, run func(*State, ...string) (WaitFunc, error)) Cmd {
	return &funcCmd{
		usage: usage,
		run:   run,
	}
}

// A funcCmd implements Cmd using a function value.
type funcCmd struct {
	usage CmdUsage
	run   func(*State, ...string) (WaitFunc, error)
}

func (c *funcCmd) Run(s *State, args ...string) (WaitFunc, error) {
	return c.run(s, args...)
}

func (c *funcCmd) Usage() *CmdUsage { return &c.usage }

// firstNonFlag returns a slice containing the index of the first argument in
// rawArgs that is not a flag, or nil if all arguments are flags.
func firstNonFlag(rawArgs ...string) []int {
	for i, arg := range rawArgs {
		if !strings.HasPrefix(arg, "-") {
			return []int{i}
		}
		if arg == "--" {
			return []int{i + 1}
		}
	}
	return nil
}

// Cat writes the concatenated contents of the named file(s) to the script's
// stdout buffer.
func Cat() Cmd {
	return Command(
		CmdUsage{
			Summary: "concatenate files and print to the script's stdout buffer",
			Args:    "files...",
		},
		func(s *State, args ...string) (WaitFunc, error) {
			if len(args) == 0 {
				return nil, ErrUsage
			}

			paths := make([]string, 0, len(args))
			for _, arg := range args {
				paths = append(paths, s.Path(arg))
			}

			var buf strings.Builder
			errc := make(chan error, 1)
			go func() {
				for _, p := range paths {
					b, err := os.ReadFile(p)
					buf.Write(b)
					if err != nil {
						errc <- err
						return
					}
				}
				errc <- nil
			}()

			wait := func(*State) (stdout, stderr string, err error) {
				err = <-errc
				return buf.String(), "", err
			}
			return wait, nil
		})
}

// Cd changes the current working directory.
func Cd() Cmd {
	return Command(
		CmdUsage{
			Summary: "change the working directory",
			Args:    "dir",
		},
		func(s *State, args ...string) (WaitFunc, error) {
			if len(args) != 1 {
				return nil, ErrUsage
			}
			return nil, s.Chdir(args[0])
		})
}

// Chmod changes the permissions of a file or a directory..
func Chmod() Cmd {
	return Command(
		CmdUsage{
			Summary: "change file mode bits",
			Args:    "perm paths...",
			Detail: []string{
				"Changes the permissions of the named files or directories to be equal to perm.",
				"Only numerical permissions are supported.",
			},
		},
		func(s *State, args ...string) (WaitFunc, error) {
			if len(args) < 2 {
				return nil, ErrUsage
			}

			perm, err := strconv.ParseUint(args[0], 0, 32)
			if err != nil || perm&uint64(fs.ModePerm) != perm {
				return nil, fmt.Errorf("invalid mode: %s", args[0])
			}

			for _, arg := range args[1:] {
				err := os.Chmod(s.Path(arg), fs.FileMode(perm))
				if err != nil {
					return nil, err
				}
			}
			return nil, nil
		})
}

// Cmp compares the contents of two files, or the contents of either the
// "stdout" or "stderr" buffer and a file, returning a non-nil error if the
// contents differ.
func Cmp() Cmd {
	return Command(
		CmdUsage{
			Args:    "[-q] file1 file2",
			Summary: "compare files for differences",
			Detail: []string{
				"By convention, file1 is the actual data and file2 is the expected data.",
				"The command succeeds if the file contents are identical.",
				"File1 can be 'stdout' or 'stderr' to compare the stdout or stderr buffer from the most recent command.",
			},
		},
		func(s *State, args ...string) (WaitFunc, error) {
			return nil, doCompare(s, false, args...)
		})
}

// Cmpenv is like Compare, but also performs environment substitutions
// on the contents of both arguments.
func Cmpenv() Cmd {
	return Command(
		CmdUsage{
			Args:    "[-q] file1 file2",
			Summary: "compare files for differences, with environment expansion",
			Detail: []string{
				"By convention, file1 is the actual data and file2 is the expected data.",
				"The command succeeds if the file contents are identical after substituting variables from the script environment.",
				"File1 can be 'stdout' or 'stderr' to compare the script's stdout or stderr buffer.",
			},
		},
		func(s *State, args ...string) (WaitFunc, error) {
			return nil, doCompare(s, true, args...)
		})
}

func doCompare(s *State, env bool, args ...string) error {
	quiet := false
	if len(args) > 0 && args[0] == "-q" {
		quiet = true
		args = args[1:]
	}
	if len(args) != 2 {
		return ErrUsage
	}

	name1, name2 := args[0], args[1]
	var text1, text2 string
	switch name1 {
	case "stdout":
		text1 = s.Stdout()
	case "stderr":
		text1 = s.Stderr()
	default:
		data, err := os.ReadFile(s.Path(name1))
		if err != nil {
			return err
		}
		text1 = string(data)
	}

	data, err := os.ReadFile(s.Path(name2))
	if err != nil {
		return err
	}
	text2 = string(data)

	if env {
		text1 = s.ExpandEnv(text1, false)
		text2 = s.ExpandEnv(text2, false)
	}

	if text1 != text2 {
		if !quiet {
			diffText := diff.Diff(name1, []byte(text1), name2, []byte(text2))
			s.Logf("%s\n", diffText)
		}
		return fmt.Errorf("%s and %s differ", name1, name2)
	}
	return nil
}

// Cp copies one or more files to a new location.
func Cp() Cmd {
	return Command(
		CmdUsage{
			Summary: "copy files to a target file or directory",
			Args:    "src... dst",
			Detail: []string{
				"src can include 'stdout' or 'stderr' to copy from the script's stdout or stderr buffer.",
			},
		},
		func(s *State, args ...string) (WaitFunc, error) {
			if len(args) < 2 {
				return nil, ErrUsage
			}

			dst := s.Path(args[len(args)-1])
			info, err := os.Stat(dst)
			dstDir := err == nil && info.IsDir()
			if len(args) > 2 && !dstDir {
				return nil, &fs.PathError{Op: "cp", Path: dst, Err: errors.New("destination is not a directory")}
			}

			for _, arg := range args[:len(args)-1] {
				var (
					src  string
					data []byte
					mode fs.FileMode
				)
				switch arg {
				case "stdout":
					src = arg
					data = []byte(s.Stdout())
					mode = 0666
				case "stderr":
					src = arg
					data = []byte(s.Stderr())
					mode = 0666
				default:
					src = s.Path(arg)
					info, err := os.Stat(src)
					if err != nil {
						return nil, err
					}
					mode = info.Mode() & 0777
					data, err = os.ReadFile(src)
					if err != nil {
						return nil, err
					}
				}
				targ := dst
				if dstDir {
					targ = filepath.Join(dst, filepath.Base(src))
				}
				err := os.WriteFile(targ, data, mode)
				if err != nil {
					return nil, err
				}
			}

			return nil, nil
		})
}

// Echo writes its arguments to stdout, followed by a newline.
func Echo() Cmd {
	return Command(
		CmdUsage{
			Summary: "display a line of text",
			Args:    "string...",
		},
		func(s *State, args ...string) (WaitFunc, error) {
			var buf strings.Builder
			for i, arg := range args {
				if i > 0 {
					buf.WriteString(" ")
				}
				buf.WriteString(arg)
			}
			buf.WriteString("\n")
			out := buf.String()

			// Stuff the result into a callback to satisfy the OutputCommandFunc
			// interface, even though it isn't really asynchronous even if run in the
			// background.
			//
			// Nobody should be running 'echo' as a background command, but it's not worth
			// defining yet another interface, and also doesn't seem worth shoehorning
			// into a SimpleCommand the way we did with Wait.
			return func(*State) (stdout, stderr string, err error) {
				return out, "", nil
			}, nil
		})
}

// Env sets or logs the values of environment variables.
//
// With no arguments, Env reports all variables in the environment.
// "key=value" arguments set variables, and arguments without "="
// cause the corresponding value to be printed to the stdout buffer.
func Env() Cmd {
	return Command(
		CmdUsage{
			Summary: "set or log the values of environment variables",
			Args:    "[key[=value]...]",
			Detail: []string{
				"With no arguments, print the script environment to the log.",
				"Otherwise, add the listed key=value pairs to the environment or print the listed keys.",
			},
		},
		func(s *State, args ...string) (WaitFunc, error) {
			out := new(strings.Builder)
			if len(args) == 0 {
				for _, kv := range s.env {
					fmt.Fprintf(out, "%s\n", kv)
				}
			} else {
				for _, env := range args {
					i := strings.Index(env, "=")
					if i < 0 {
						// Display value instead of setting it.
						fmt.Fprintf(out, "%s=%s\n", env, s.envMap[env])
						continue
					}
					if err := s.Setenv(env[:i], env[i+1:]); err != nil {
						return nil, err
					}
				}
			}
			var wait WaitFunc
			if out.Len() > 0 || len(args) == 0 {
				wait = func(*State) (stdout, stderr string, err error) {
					return out.String(), "", nil
				}
			}
			return wait, nil
		})
}

// Exec runs an arbitrary executable as a subprocess.
//
// When the Script's context is canceled, Exec sends the interrupt signal, then
// waits for up to the given delay for the subprocess to flush output before
// terminating it with os.Kill.
func Exec(cancel func(*exec.Cmd) error, waitDelay time.Duration) Cmd {
	return Command(
		CmdUsage{
			Summary: "run an executable program with arguments",
			Args:    "program [args...]",
			Detail: []string{
				"Note that 'exec' does not terminate the script (unlike Unix shells).",
			},
			Async: true,
		},
		func(s *State, args ...string) (WaitFunc, error) {
			if len(args) < 1 {
				return nil, ErrUsage
			}

			// Use the script's PATH to look up the command (if it does not contain a separator)
			// instead of the test process's PATH (see lookPath).
			// Don't use filepath.Clean, since that changes "./foo" to "foo".
			name := filepath.FromSlash(args[0])
			path := name
			if !strings.Contains(name, string(filepath.Separator)) {
				var err error
				path, err = lookPath(s, name)
				if err != nil {
					return nil, err
				}
			}

			return startCommand(s, name, path, args[1:], cancel, waitDelay)
		})
}

func startCommand(s *State, name, path string, args []string, cancel func(*exec.Cmd) error, waitDelay time.Duration) (WaitFunc, error) {
	var (
		cmd                  *exec.Cmd
		stdoutBuf, stderrBuf strings.Builder
	)
	for {
		cmd = exec.CommandContext(s.Context(), path, args...)
		if cancel == nil {
			cmd.Cancel = nil
		} else {
			cmd.Cancel = func() error { return cancel(cmd) }
		}
		cmd.WaitDelay = waitDelay
		cmd.Args[0] = name
		cmd.Dir = s.Getwd()
		cmd.Env = s.env
		cmd.Stdout = &stdoutBuf
		cmd.Stderr = &stderrBuf
		err := cmd.Start()
		if err == nil {
			break
		}
		if isETXTBSY(err) {
			// If the script (or its host process) just wrote the executable we're
			// trying to run, a fork+exec in another thread may be holding open the FD
			// that we used to write the executable (see https://go.dev/issue/22315).
			// Since the descriptor should have CLOEXEC set, the problem should
			// resolve as soon as the forked child reaches its exec call.
			// Keep retrying until that happens.
		} else {
			return nil, err
		}
	}

	wait := func(s *State) (stdout, stderr string, err error) {
		err = cmd.Wait()
		return stdoutBuf.String(), stderrBuf.String(), err
	}
	return wait, nil
}

// lookPath is (roughly) like exec.LookPath, but it uses the script's current
// PATH to find the executable.
func lookPath(s *State, command string) (string, error) {
	var strEqual func(string, string) bool
	if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
		// Using GOOS as a proxy for case-insensitive file system.
		// TODO(bcmills): Remove this assumption.
		strEqual = strings.EqualFold
	} else {
		strEqual = func(a, b string) bool { return a == b }
	}

	var pathExt []string
	var searchExt bool
	var isExecutable func(os.FileInfo) bool
	if runtime.GOOS == "windows" {
		// Use the test process's PathExt instead of the script's.
		// If PathExt is set in the command's environment, cmd.Start fails with
		// "parameter is invalid". Not sure why.
		// If the command already has an extension in PathExt (like "cmd.exe")
		// don't search for other extensions (not "cmd.bat.exe").
		pathExt = strings.Split(os.Getenv("PathExt"), string(filepath.ListSeparator))
		searchExt = true
		cmdExt := filepath.Ext(command)
		for _, ext := range pathExt {
			if strEqual(cmdExt, ext) {
				searchExt = false
				break
			}
		}
		isExecutable = func(fi os.FileInfo) bool {
			return fi.Mode().IsRegular()
		}
	} else {
		isExecutable = func(fi os.FileInfo) bool {
			return fi.Mode().IsRegular() && fi.Mode().Perm()&0111 != 0
		}
	}

	pathEnv, _ := s.LookupEnv(pathEnvName())
	for _, dir := range strings.Split(pathEnv, string(filepath.ListSeparator)) {
		if dir == "" {
			continue
		}

		// Determine whether dir needs a trailing path separator.
		// Note: we avoid filepath.Join in this function because it cleans the
		// result: we want to preserve the exact dir prefix from the environment.
		sep := string(filepath.Separator)
		if os.IsPathSeparator(dir[len(dir)-1]) {
			sep = ""
		}

		if searchExt {
			ents, err := os.ReadDir(dir)
			if err != nil {
				continue
			}
			for _, ent := range ents {
				for _, ext := range pathExt {
					if !ent.IsDir() && strEqual(ent.Name(), command+ext) {
						return dir + sep + ent.Name(), nil
					}
				}
			}
		} else {
			path := dir + sep + command
			if fi, err := os.Stat(path); err == nil && isExecutable(fi) {
				return path, nil
			}
		}
	}
	return "", &exec.Error{Name: command, Err: exec.ErrNotFound}
}

// pathEnvName returns the platform-specific variable used by os/exec.LookPath
// to look up executable names (either "PATH" or "path").
//
// TODO(bcmills): Investigate whether we can instead use PATH uniformly and
// rewrite it to $path when executing subprocesses.
func pathEnvName() string {
	switch runtime.GOOS {
	case "plan9":
		return "path"
	default:
		return "PATH"
	}
}

// Exists checks that the named file(s) exist.
func Exists() Cmd {
	return Command(
		CmdUsage{
			Summary: "check that files exist",
			Args:    "[-readonly] [-exec] file...",
		},
		func(s *State, args ...string) (WaitFunc, error) {
			var readonly, exec bool
		loop:
			for len(args) > 0 {
				switch args[0] {
				case "-readonly":
					readonly = true
					args = args[1:]
				case "-exec":
					exec = true
					args = args[1:]
				default:
					break loop
				}
			}
			if len(args) == 0 {
				return nil, ErrUsage
			}

			for _, file := range args {
				file = s.Path(file)
				info, err := os.Stat(file)
				if err != nil {
					return nil, err
				}
				if readonly && info.Mode()&0222 != 0 {
					return nil, fmt.Errorf("%s exists but is writable", file)
				}
				if exec && runtime.GOOS != "windows" && info.Mode()&0111 == 0 {
					return nil, fmt.Errorf("%s exists but is not executable", file)
				}
			}

			return nil, nil
		})
}

// Grep checks that file content matches a regexp.
// Like stdout/stderr and unlike Unix grep, it accepts Go regexp syntax.
//
// Grep does not modify the State's stdout or stderr buffers.
// (Its output goes to the script log, not stdout.)
func Grep() Cmd {
	return Command(
		CmdUsage{
			Summary: "find lines in a file that match a pattern",
			Args:    matchUsage + " file",
			Detail: []string{
				"The command succeeds if at least one match (or the exact count, if given) is found.",
				"The -q flag suppresses printing of matches.",
			},
			RegexpArgs: firstNonFlag,
		},
		func(s *State, args ...string) (WaitFunc, error) {
			return nil, match(s, args, "", "grep")
		})
}

const matchUsage = "[-count=N] [-q] 'pattern'"

// match implements the Grep, Stdout, and Stderr commands.
func match(s *State, args []string, text, name string) error {
	n := 0
	if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") {
		var err error
		n, err = strconv.Atoi(args[0][len("-count="):])
		if err != nil {
			return fmt.Errorf("bad -count=: %v", err)
		}
		if n < 1 {
			return fmt.Errorf("bad -count=: must be at least 1")
		}
		args = args[1:]
	}
	quiet := false
	if len(args) >= 1 && args[0] == "-q" {
		quiet = true
		args = args[1:]
	}

	isGrep := name == "grep"

	wantArgs := 1
	if isGrep {
		wantArgs = 2
	}
	if len(args) != wantArgs {
		return ErrUsage
	}

	pattern := `(?m)` + args[0]
	re, err := regexp.Compile(pattern)
	if err != nil {
		return err
	}

	if isGrep {
		name = args[1] // for error messages
		data, err := os.ReadFile(s.Path(args[1]))
		if err != nil {
			return err
		}
		text = string(data)
	}

	if n > 0 {
		count := len(re.FindAllString(text, -1))
		if count != n {
			return fmt.Errorf("found %d matches for %#q in %s", count, pattern, name)
		}
		return nil
	}

	if !re.MatchString(text) {
		return fmt.Errorf("no match for %#q in %s", pattern, name)
	}

	if !quiet {
		// Print the lines containing the match.
		loc := re.FindStringIndex(text)
		for loc[0] > 0 && text[loc[0]-1] != '\n' {
			loc[0]--
		}
		for loc[1] < len(text) && text[loc[1]] != '\n' {
			loc[1]++
		}
		lines := strings.TrimSuffix(text[loc[0]:loc[1]], "\n")
		s.Logf("matched: %s\n", lines)
	}
	return nil
}

// Help writes command documentation to the script log.
func Help() Cmd {
	return Command(
		CmdUsage{
			Summary: "log help text for commands and conditions",
			Args:    "[-v] name...",
			Detail: []string{
				"To display help for a specific condition, enclose it in brackets: 'help [amd64]'.",
				"To display complete documentation when listing all commands, pass the -v flag.",
			},
		},
		func(s *State, args ...string) (WaitFunc, error) {
			if s.engine == nil {
				return nil, errors.New("no engine configured")
			}

			verbose := false
			if len(args) > 0 {
				verbose = true
				if args[0] == "-v" {
					args = args[1:]
				}
			}

			var cmds, conds []string
			for _, arg := range args {
				if strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]") {
					conds = append(conds, arg[1:len(arg)-1])
				} else {
					cmds = append(cmds, arg)
				}
			}

			out := new(strings.Builder)

			if len(conds) > 0 || (len(args) == 0 && len(s.engine.Conds) > 0) {
				if conds == nil {
					out.WriteString("conditions:\n\n")
				}
				s.engine.ListConds(out, s, conds...)
			}

			if len(cmds) > 0 || len(args) == 0 {
				if len(args) == 0 {
					out.WriteString("\ncommands:\n\n")
				}
				s.engine.ListCmds(out, verbose, cmds...)
			}

			wait := func(*State) (stdout, stderr string, err error) {
				return out.String(), "", nil
			}
			return wait, nil
		})
}

// Mkdir creates a directory and any needed parent directories.
func Mkdir() Cmd {
	return Command(
		CmdUsage{
			Summary: "create directories, if they do not already exist",
			Args:    "path...",
			Detail: []string{
				"Unlike Unix mkdir, parent directories are always created if needed.",
			},
		},
		func(s *State, args ...string) (WaitFunc, error) {
			if len(args) < 1 {
				return nil, ErrUsage
			}
			for _, arg := range args {
				if err := os.MkdirAll(s.Path(arg), 0777); err != nil {
					return nil, err
				}
			}
			return nil, nil
		})
}

// Mv renames an existing file or directory to a new path.
func Mv() Cmd {
	return Command(
		CmdUsage{
			Summary: "rename a file or directory to a new path",
			Args:    "old new",
			Detail: []string{
				"OS-specific restrictions may apply when old and new are in different directories.",
			},
		},
		func(s *State, args ...string) (WaitFunc, error) {
			if len(args) != 2 {
				return nil, ErrUsage
			}
			return nil, os.Rename(s.Path(args[0]), s.Path(args[1]))
		})
}

// Program returns a new command that runs the named program, found from the
// host process's PATH (not looked up in the script's PATH).
func Program(name string, cancel func(*exec.Cmd) error, waitDelay time.Duration) Cmd {
	var (
		shortName    string
		summary      string
		lookPathOnce sync.Once
		path         string
		pathErr      error
	)
	if filepath.IsAbs(name) {
		lookPathOnce.Do(func() { path = filepath.Clean(name) })
		shortName = strings.TrimSuffix(filepath.Base(path), ".exe")
		summary = "run the '" + shortName + "' program provided by the script host"
	} else {
		shortName = name
		summary = "run the '" + shortName + "' program from the script host's PATH"
	}

	return Command(
		CmdUsage{
			Summary: summary,
			Args:    "[args...]",
			Async:   true,
		},
		func(s *State, args ...string) (WaitFunc, error) {
			lookPathOnce.Do(func() {
				path, pathErr = exec.LookPath(name)
			})
			if pathErr != nil {
				return nil, pathErr
			}
			return startCommand(s, shortName, path, args, cancel, waitDelay)
		})
}

// Replace replaces all occurrences of a string in a file with another string.
func Replace() Cmd {
	return Command(
		CmdUsage{
			Summary: "replace strings in a file",
			Args:    "[old new]... file",
			Detail: []string{
				"The 'old' and 'new' arguments are unquoted as if in quoted Go strings.",
			},
		},
		func(s *State, args ...string) (WaitFunc, error) {
			if len(args)%2 != 1 {
				return nil, ErrUsage
			}

			oldNew := make([]string, 0, len(args)-1)
			for _, arg := range args[:len(args)-1] {
				s, err := strconv.Unquote(`"` + arg + `"`)
				if err != nil {
					return nil, err
				}
				oldNew = append(oldNew, s)
			}

			r := strings.NewReplacer(oldNew...)
			file := s.Path(args[len(args)-1])

			data, err := os.ReadFile(file)
			if err != nil {
				return nil, err
			}
			replaced := r.Replace(string(data))

			return nil, os.WriteFile(file, []byte(replaced), 0666)
		})
}

// Rm removes a file or directory.
//
// If a directory, Rm also recursively removes that directory's
// contents.
func Rm() Cmd {
	return Command(
		CmdUsage{
			Summary: "remove a file or directory",
			Args:    "path...",
			Detail: []string{
				"If the path is a directory, its contents are removed recursively.",
			},
		},
		func(s *State, args ...string) (WaitFunc, error) {
			if len(args) < 1 {
				return nil, ErrUsage
			}
			for _, arg := range args {
				if err := removeAll(s.Path(arg)); err != nil {
					return nil, err
				}
			}
			return nil, nil
		})
}

// removeAll removes dir and all files and directories it contains.
//
// Unlike os.RemoveAll, removeAll attempts to make the directories writable if
// needed in order to remove their contents.
func removeAll(dir string) error {
	// module cache has 0444 directories;
	// make them writable in order to remove content.
	filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error {
		// chmod not only directories, but also things that we couldn't even stat
		// due to permission errors: they may also be unreadable directories.
		if err != nil || info.IsDir() {
			os.Chmod(path, 0777)
		}
		return nil
	})
	return os.RemoveAll(dir)
}

// Sleep sleeps for the given Go duration or until the script's context is
// cancelled, whichever happens first.
func Sleep() Cmd {
	return Command(
		CmdUsage{
			Summary: "sleep for a specified duration",
			Args:    "duration",
			Detail: []string{
				"The duration must be given as a Go time.Duration string.",
			},
			Async: true,
		},
		func(s *State, args ...string) (WaitFunc, error) {
			if len(args) != 1 {
				return nil, ErrUsage
			}

			d, err := time.ParseDuration(args[0])
			if err != nil {
				return nil, err
			}

			timer := time.NewTimer(d)
			wait := func(s *State) (stdout, stderr string, err error) {
				ctx := s.Context()
				select {
				case <-ctx.Done():
					timer.Stop()
					return "", "", ctx.Err()
				case <-timer.C:
					return "", "", nil
				}
			}
			return wait, nil
		})
}

// Stderr searches for a regular expression in the stderr buffer.
func Stderr() Cmd {
	return Command(
		CmdUsage{
			Summary: "find lines in the stderr buffer that match a pattern",
			Args:    matchUsage + " file",
			Detail: []string{
				"The command succeeds if at least one match (or the exact count, if given) is found.",
				"The -q flag suppresses printing of matches.",
			},
			RegexpArgs: firstNonFlag,
		},
		func(s *State, args ...string) (WaitFunc, error) {
			return nil, match(s, args, s.Stderr(), "stderr")
		})
}

// Stdout searches for a regular expression in the stdout buffer.
func Stdout() Cmd {
	return Command(
		CmdUsage{
			Summary: "find lines in the stdout buffer that match a pattern",
			Args:    matchUsage + " file",
			Detail: []string{
				"The command succeeds if at least one match (or the exact count, if given) is found.",
				"The -q flag suppresses printing of matches.",
			},
			RegexpArgs: firstNonFlag,
		},
		func(s *State, args ...string) (WaitFunc, error) {
			return nil, match(s, args, s.Stdout(), "stdout")
		})
}

// Stop returns a sentinel error that causes script execution to halt
// and s.Execute to return with a nil error.
func Stop() Cmd {
	return Command(
		CmdUsage{
			Summary: "stop execution of the script",
			Args:    "[msg]",
			Detail: []string{
				"The message is written to the script log, but no error is reported from the script engine.",
			},
		},
		func(s *State, args ...string) (WaitFunc, error) {
			if len(args) > 1 {
				return nil, ErrUsage
			}
			// TODO(bcmills): The argument passed to stop seems redundant with comments.
			// Either use it systematically or remove it.
			if len(args) == 1 {
				return nil, stopError{msg: args[0]}
			}
			return nil, stopError{}
		})
}

// stopError is the sentinel error type returned by the Stop command.
type stopError struct {
	msg string
}

func (s stopError) Error() string {
	if s.msg == "" {
		return "stop"
	}
	return "stop: " + s.msg
}

// Symlink creates a symbolic link.
func Symlink() Cmd {
	return Command(
		CmdUsage{
			Summary: "create a symlink",
			Args:    "path -> target",
			Detail: []string{
				"Creates path as a symlink to target.",
				"The '->' token (like in 'ls -l' output on Unix) is required.",
			},
		},
		func(s *State, args ...string) (WaitFunc, error) {
			if len(args) != 3 || args[1] != "->" {
				return nil, ErrUsage
			}

			// Note that the link target args[2] is not interpreted with s.Path:
			// it will be interpreted relative to the directory file is in.
			return nil, os.Symlink(filepath.FromSlash(args[2]), s.Path(args[0]))
		})
}

// Wait waits for the completion of background commands.
//
// When Wait returns, the stdout and stderr buffers contain the concatenation of
// the background commands' respective outputs in the order in which those
// commands were started.
func Wait() Cmd {
	return Command(
		CmdUsage{
			Summary: "wait for completion of background commands",
			Args:    "",
			Detail: []string{
				"Waits for all background commands to complete.",
				"The output (and any error) from each command is printed to the log in the order in which the commands were started.",
				"After the call to 'wait', the script's stdout and stderr buffers contain the concatenation of the background commands' outputs.",
			},
		},
		func(s *State, args ...string) (WaitFunc, error) {
			if len(args) > 0 {
				return nil, ErrUsage
			}

			var stdouts, stderrs []string
			var errs []*CommandError
			for _, bg := range s.background {
				stdout, stderr, err := bg.wait(s)

				beforeArgs := ""
				if len(bg.args) > 0 {
					beforeArgs = " "
				}
				s.Logf("[background] %s%s%s\n", bg.name, beforeArgs, quoteArgs(bg.args))

				if stdout != "" {
					s.Logf("[stdout]\n%s", stdout)
					stdouts = append(stdouts, stdout)
				}
				if stderr != "" {
					s.Logf("[stderr]\n%s", stderr)
					stderrs = append(stderrs, stderr)
				}
				if err != nil {
					s.Logf("[%v]\n", err)
				}
				if cmdErr := checkStatus(bg.command, err); cmdErr != nil {
					errs = append(errs, cmdErr.(*CommandError))
				}
			}

			s.stdout = strings.Join(stdouts, "")
			s.stderr = strings.Join(stderrs, "")
			s.background = nil
			if len(errs) > 0 {
				return nil, waitError{errs: errs}
			}
			return nil, nil
		})
}

// A waitError wraps one or more errors returned by background commands.
type waitError struct {
	errs []*CommandError
}

func (w waitError) Error() string {
	b := new(strings.Builder)
	for i, err := range w.errs {
		if i != 0 {
			b.WriteString("\n")
		}
		b.WriteString(err.Error())
	}
	return b.String()
}

func (w waitError) Unwrap() error {
	if len(w.errs) == 1 {
		return w.errs[0]
	}
	return nil
}


================================================
FILE: cmds_other.go
================================================
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build !(unix || windows)

package script

func isETXTBSY(err error) bool {
	return false
}


================================================
FILE: cmds_posix.go
================================================
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build unix || windows

package script

import (
	"errors"
	"syscall"
)

func isETXTBSY(err error) bool {
	return errors.Is(err, syscall.ETXTBSY)
}


================================================
FILE: conds.go
================================================
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package script

import (
	"fmt"
	"os"
	"runtime"
	"sync"
)

// DefaultConds returns a set of broadly useful script conditions.
//
// Run the 'help' command within a script engine to view a list of the available
// conditions.
func DefaultConds() map[string]Cond {
	conds := make(map[string]Cond)

	conds["GOOS"] = PrefixCondition(
		"runtime.GOOS == <suffix>",
		func(_ *State, suffix string) (bool, error) {
			if suffix == runtime.GOOS {
				return true, nil
			}
			return false, nil
		})

	conds["GOARCH"] = PrefixCondition(
		"runtime.GOARCH == <suffix>",
		func(_ *State, suffix string) (bool, error) {
			if suffix == runtime.GOARCH {
				return true, nil
			}
			return false, nil
		})

	conds["compiler"] = PrefixCondition(
		"runtime.Compiler == <suffix>",
		func(_ *State, suffix string) (bool, error) {
			if suffix == runtime.Compiler {
				return true, nil
			}
			switch suffix {
			case "gc", "gccgo":
				return false, nil
			default:
				return false, fmt.Errorf("unrecognized compiler %q", suffix)
			}
		})

	conds["root"] = BoolCondition("os.Geteuid() == 0", os.Geteuid() == 0)

	return conds
}

// Condition returns a Cond with the given summary and evaluation function.
func Condition(summary string, eval func(*State) (bool, error)) Cond {
	return &funcCond{eval: eval, usage: CondUsage{Summary: summary}}
}

type funcCond struct {
	eval  func(*State) (bool, error)
	usage CondUsage
}

func (c *funcCond) Usage() *CondUsage { return &c.usage }

func (c *funcCond) Eval(s *State, suffix string) (bool, error) {
	if suffix != "" {
		return false, ErrUsage
	}
	return c.eval(s)
}

// PrefixCondition returns a Cond with the given summary and evaluation function.
func PrefixCondition(summary string, eval func(*State, string) (bool, error)) Cond {
	return &prefixCond{eval: eval, usage: CondUsage{Summary: summary, Prefix: true}}
}

type prefixCond struct {
	eval  func(*State, string) (bool, error)
	usage CondUsage
}

func (c *prefixCond) Usage() *CondUsage { return &c.usage }

func (c *prefixCond) Eval(s *State, suffix string) (bool, error) {
	return c.eval(s, suffix)
}

// BoolCondition returns a Cond with the given truth value and summary.
// The Cond rejects the use of condition suffixes.
func BoolCondition(summary string, v bool) Cond {
	return &boolCond{v: v, usage: CondUsage{Summary: summary}}
}

type boolCond struct {
	v     bool
	usage CondUsage
}

func (b *boolCond) Usage() *CondUsage { return &b.usage }

func (b *boolCond) Eval(s *State, suffix string) (bool, error) {
	if suffix != "" {
		return false, ErrUsage
	}
	return b.v, nil
}

// OnceCondition returns a Cond that calls eval the first time the condition is
// evaluated. Future calls reuse the same result.
//
// The eval function is not passed a *State because the condition is cached
// across all execution states and must not vary by state.
func OnceCondition(summary string, eval func() (bool, error)) Cond {
	return &onceCond{eval: eval, usage: CondUsage{Summary: summary}}
}

type onceCond struct {
	once  sync.Once
	v     bool
	err   error
	eval  func() (bool, error)
	usage CondUsage
}

func (l *onceCond) Usage() *CondUsage { return &l.usage }

func (l *onceCond) Eval(s *State, suffix string) (bool, error) {
	if suffix != "" {
		return false, ErrUsage
	}
	l.once.Do(func() { l.v, l.err = l.eval() })
	return l.v, l.err
}

// CachedCondition is like Condition but only calls eval the first time the
// condition is evaluated for a given suffix.
// Future calls with the same suffix reuse the earlier result.
//
// The eval function is not passed a *State because the condition is cached
// across all execution states and must not vary by state.
func CachedCondition(summary string, eval func(string) (bool, error)) Cond {
	return &cachedCond{eval: eval, usage: CondUsage{Summary: summary, Prefix: true}}
}

type cachedCond struct {
	m     sync.Map
	eval  func(string) (bool, error)
	usage CondUsage
}

func (c *cachedCond) Usage() *CondUsage { return &c.usage }

func (c *cachedCond) Eval(_ *State, suffix string) (bool, error) {
	for {
		var ready chan struct{}

		v, loaded := c.m.Load(suffix)
		if !loaded {
			ready = make(chan struct{})
			v, loaded = c.m.LoadOrStore(suffix, (<-chan struct{})(ready))

			if !loaded {
				inPanic := true
				defer func() {
					if inPanic {
						c.m.Delete(suffix)
					}
					close(ready)
				}()

				b, err := c.eval(suffix)
				inPanic = false

				if err == nil {
					c.m.Store(suffix, b)
					return b, nil
				} else {
					c.m.Store(suffix, err)
					return false, err
				}
			}
		}

		switch v := v.(type) {
		case bool:
			return v, nil
		case error:
			return false, v
		case <-chan struct{}:
			<-v
		}
	}
}


================================================
FILE: engine.go
================================================
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package script implements a small, customizable, platform-agnostic scripting
// language.
//
// Scripts are run by an [Engine] configured with a set of available commands
// and conditions that guard those commands. Each script has an associated
// working directory and environment, along with a buffer containing the stdout
// and stderr output of a prior command, tracked in a [State] that commands can
// inspect and modify.
//
// The default commands configured by [NewEngine] resemble a simplified Unix
// shell.
//
// # Script Language
//
// Each line of a script is parsed into a sequence of space-separated command
// words, with environment variable expansion within each word and # marking an
// end-of-line comment. Additional variables named ':' and '/' are expanded
// within script arguments (expanding to the value of os.PathListSeparator and
// os.PathSeparator respectively) but are not inherited in subprocess
// environments.
//
// Adding single quotes around text keeps spaces in that text from being treated
// as word separators and also disables environment variable expansion.
// Inside a single-quoted block of text, a repeated single quote indicates
// a literal single quote, as in:
//
//	'Don''t communicate by sharing memory.'
//
// A line beginning with # is a comment and conventionally explains what is
// being done or tested at the start of a new section of the script.
//
// Commands are executed one at a time, and errors are checked for each command;
// if any command fails unexpectedly, no subsequent commands in the script are
// executed. The command prefix ! indicates that the command on the rest of the
// line (typically go or a matching predicate) must fail instead of succeeding.
// The command prefix ? indicates that the command may or may not succeed, but
// the script should continue regardless.
//
// The command prefix [cond] indicates that the command on the rest of the line
// should only run when the condition is satisfied.
//
// A condition can be negated: [!root] means to run the rest of the line only if
// the user is not root. Multiple conditions may be given for a single command,
// for example, '[linux] [amd64] skip'. The command will run if all conditions
// are satisfied.
//
// Package script is particularly good for writing tests.
// Ironically, it has no tests.
package script

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"io"
	"sort"
	"strings"
	"time"
)

// An Engine stores the configuration for executing a set of scripts.
//
// The same Engine may execute multiple scripts concurrently.
type Engine struct {
	Cmds  map[string]Cmd
	Conds map[string]Cond

	// If Quiet is true, Execute deletes log prints from the previous
	// section when starting a new section.
	Quiet bool
}

// NewEngine returns an Engine configured with a basic set of commands and conditions.
func NewEngine() *Engine {
	return &Engine{
		Cmds:  DefaultCmds(),
		Conds: DefaultConds(),
	}
}

// A Cmd is a command that is available to a script.
type Cmd interface {
	// Run begins running the command.
	//
	// If the command produces output or can be run in the background, run returns
	// a WaitFunc that will be called to obtain the result of the command and
	// update the engine's stdout and stderr buffers.
	//
	// Run itself and the returned WaitFunc may inspect and/or modify the State,
	// but the State's methods must not be called concurrently after Run has
	// returned.
	//
	// Run may retain and access the args slice until the WaitFunc has returned.
	Run(s *State, args ...string) (WaitFunc, error)

	// Usage returns the usage for the command, which the caller must not modify.
	Usage() *CmdUsage
}

// A WaitFunc is a function called to retrieve the results of a Cmd.
type WaitFunc func(*State) (stdout, stderr string, err error)

// A CmdUsage describes the usage of a Cmd, independent of its name
// (which can change based on its registration).
type CmdUsage struct {
	Summary string   // in the style of the Name section of a Unix 'man' page, omitting the name
	Args    string   // a brief synopsis of the command's arguments (only)
	Detail  []string // zero or more sentences in the style of the Description section of a Unix 'man' page

	// If Async is true, the Cmd is meaningful to run in the background, and its
	// Run method must return either a non-nil WaitFunc or a non-nil error.
	Async bool

	// RegexpArgs reports which arguments, if any, should be treated as regular
	// expressions. It takes as input the raw, unexpanded arguments and returns
	// the list of argument indices that will be interpreted as regular
	// expressions.
	//
	// If RegexpArgs is nil, all arguments are assumed not to be regular
	// expressions.
	RegexpArgs func(rawArgs ...string) []int
}

// A Cond is a condition deciding whether a command should be run.
type Cond interface {
	// Eval reports whether the condition applies to the given State.
	//
	// If the condition's usage reports that it is a prefix,
	// the condition must be used with a suffix.
	// Otherwise, the passed-in suffix argument is always the empty string.
	Eval(s *State, suffix string) (bool, error)

	// Usage returns the usage for the condition, which the caller must not modify.
	Usage() *CondUsage
}

// A CondUsage describes the usage of a Cond, independent of its name
// (which can change based on its registration).
type CondUsage struct {
	Summary string // a single-line summary of when the condition is true

	// If Prefix is true, the condition is a prefix and requires a
	// colon-separated suffix (like "[GOOS:linux]" for the "GOOS" condition).
	// The suffix may be the empty string (like "[prefix:]").
	Prefix bool
}

// Execute reads and executes script, writing the output to log.
//
// Execute stops and returns an error at the first command that does not succeed.
// The returned error's text begins with "file:line: ".
//
// If the script runs to completion or ends by a 'stop' command,
// Execute returns nil.
//
// Execute does not stop background commands started by the script
// before returning. To stop those, use [State.CloseAndWait] or the
// [Wait] command.
func (e *Engine) Execute(s *State, file string, script *bufio.Reader, log io.Writer) (err error) {
	defer func(prev *Engine) { s.engine = prev }(s.engine)
	s.engine = e

	var sectionStart time.Time
	// endSection flushes the logs for the current section from s.log to log.
	// ok indicates whether all commands in the section succeeded.
	endSection := func(ok bool) error {
		var err error
		if sectionStart.IsZero() {
			// We didn't write a section header or record a timestamp, so just dump the
			// whole log without those.
			if s.log.Len() > 0 {
				err = s.flushLog(log)
			}
		} else if s.log.Len() == 0 {
			// Adding elapsed time for doing nothing is meaningless, so don't.
			_, err = io.WriteString(log, "\n")
		} else {
			// Insert elapsed time for section at the end of the section's comment.
			_, err = fmt.Fprintf(log, " (%.3fs)\n", time.Since(sectionStart).Seconds())

			if err == nil && (!ok || !e.Quiet) {
				err = s.flushLog(log)
			} else {
				s.log.Reset()
			}
		}

		sectionStart = time.Time{}
		return err
	}

	var lineno int
	lineErr := func(err error) error {
		if errors.As(err, new(*CommandError)) {
			return err
		}
		return fmt.Errorf("%s:%d: %w", file, lineno, err)
	}

	// In case of failure or panic, flush any pending logs for the section.
	defer func() {
		if sErr := endSection(false); sErr != nil && err == nil {
			err = lineErr(sErr)
		}
	}()

	for {
		if err := s.ctx.Err(); err != nil {
			// This error wasn't produced by any particular command,
			// so don't wrap it in a CommandError.
			return lineErr(err)
		}

		line, err := script.ReadString('\n')
		if err == io.EOF {
			if line == "" {
				break // Reached the end of the script.
			}
			// If the script doesn't end in a newline, interpret the final line.
		} else if err != nil {
			return lineErr(err)
		}
		line = strings.TrimSuffix(line, "\n")
		lineno++

		// The comment character "#" at the start of the line delimits a section of
		// the script.
		if strings.HasPrefix(line, "#") {
			// If there was a previous section, the fact that we are starting a new
			// one implies the success of the previous one.
			//
			// At the start of the script, the state may also contain accumulated logs
			// from commands executed on the State outside of the engine in order to
			// set it up; flush those logs too.
			if err := endSection(true); err != nil {
				return lineErr(err)
			}

			// Log the section start without a newline so that we can add
			// a timestamp for the section when it ends.
			_, err = fmt.Fprintf(log, "%s", line)
			sectionStart = time.Now()
			if err != nil {
				return lineErr(err)
			}
			continue
		}

		cmd, err := parse(file, lineno, line)
		if cmd == nil && err == nil {
			continue // Ignore blank lines.
		}
		s.Logf("> %s\n", line)
		if err != nil {
			return lineErr(err)
		}

		// Evaluate condition guards.
		ok, err := e.conditionsActive(s, cmd.conds)
		if err != nil {
			return lineErr(err)
		}
		if !ok {
			s.Logf("[condition not met]\n")
			continue
		}

		impl := e.Cmds[cmd.name]

		// Expand variables in arguments.
		var regexpArgs []int
		if impl != nil {
			usage := impl.Usage()
			if usage.RegexpArgs != nil {
				// First join rawArgs without expansion to pass to RegexpArgs.
				rawArgs := make([]string, 0, len(cmd.rawArgs))
				for _, frags := range cmd.rawArgs {
					var b strings.Builder
					for _, frag := range frags {
						b.WriteString(frag.s)
					}
					rawArgs = append(rawArgs, b.String())
				}
				regexpArgs = usage.RegexpArgs(rawArgs...)
			}
		}
		cmd.args = expandArgs(s, cmd.rawArgs, regexpArgs)

		// Run the command.
		err = e.runCommand(s, cmd, impl)
		if err != nil {
			if stop := (stopError{}); errors.As(err, &stop) {
				// Since the 'stop' command halts execution of the entire script,
				// log its message separately from the section in which it appears.
				err = endSection(true)
				s.Logf("%v\n", stop)
				if err == nil {
					return nil
				}
			}
			return lineErr(err)
		}
	}

	if err := endSection(true); err != nil {
		return lineErr(err)
	}
	return nil
}

// A command is a complete command parsed from a script.
type command struct {
	file       string
	line       int
	want       expectedStatus
	conds      []condition // all must be satisfied
	name       string      // the name of the command; must be non-empty
	rawArgs    [][]argFragment
	args       []string // shell-expanded arguments following name
	background bool     // command should run in background (ends with a trailing &)
}

// An expectedStatus describes the expected outcome of a command.
// Script execution halts when a command does not match its expected status.
type expectedStatus string

const (
	success          expectedStatus = ""
	failure          expectedStatus = "!"
	successOrFailure expectedStatus = "?"
)

type argFragment struct {
	s      string
	quoted bool // if true, disable variable expansion for this fragment
}

type condition struct {
	want bool
	tag  string
}

const argSepChars = " \t\r\n#"

// parse parses a single line as a list of space-separated arguments.
// subject to environment variable expansion (but not resplitting).
// Single quotes around text disable splitting and expansion.
// To embed a single quote, double it:
//
//	'Don''t communicate by sharing memory.'
func parse(filename string, lineno int, line string) (cmd *command, err error) {
	cmd = &command{file: filename, line: lineno}
	var (
		rawArg []argFragment // text fragments of current arg so far (need to add line[start:i])
		start  = -1          // if >= 0, position where current arg text chunk starts
		quoted = false       // currently processing quoted text
	)

	flushArg := func() error {
		if len(rawArg) == 0 {
			return nil // Nothing to flush.
		}
		defer func() { rawArg = nil }()

		if cmd.name == "" && len(rawArg) == 1 && !rawArg[0].quoted {
			arg := rawArg[0].s

			// Command prefix ! means negate the expectations about this command:
			// go command should fail, match should not be found, etc.
			// Prefix ? means allow either success or failure.
			switch want := expectedStatus(arg); want {
			case failure, successOrFailure:
				if cmd.want != "" {
					return errors.New("duplicated '!' or '?' token")
				}
				cmd.want = want
				return nil
			}

			// Command prefix [cond] means only run this command if cond is satisfied.
			if strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]") {
				want := true
				arg = strings.TrimSpace(arg[1 : len(arg)-1])
				if strings.HasPrefix(arg, "!") {
					want = false
					arg = strings.TrimSpace(arg[1:])
				}
				if arg == "" {
					return errors.New("empty condition")
				}
				cmd.conds = append(cmd.conds, condition{want: want, tag: arg})
				return nil
			}

			if arg == "" {
				return errors.New("empty command")
			}
			cmd.name = arg
			return nil
		}

		cmd.rawArgs = append(cmd.rawArgs, rawArg)
		return nil
	}

	for i := 0; ; i++ {
		if !quoted && (i >= len(line) || strings.ContainsRune(argSepChars, rune(line[i]))) {
			// Found arg-separating space.
			if start >= 0 {
				rawArg = append(rawArg, argFragment{s: line[start:i], quoted: false})
				start = -1
			}
			if err := flushArg(); err != nil {
				return nil, err
			}
			if i >= len(line) || line[i] == '#' {
				break
			}
			continue
		}
		if i >= len(line) {
			return nil, errors.New("unterminated quoted argument")
		}
		if line[i] == '\'' {
			if !quoted {
				// starting a quoted chunk
				if start >= 0 {
					rawArg = append(rawArg, argFragment{s: line[start:i], quoted: false})
				}
				start = i + 1
				quoted = true
				continue
			}
			// 'foo''bar' means foo'bar, like in rc shell and Pascal.
			if i+1 < len(line) && line[i+1] == '\'' {
				rawArg = append(rawArg, argFragment{s: line[start:i], quoted: true})
				start = i + 1
				i++ // skip over second ' before next iteration
				continue
			}
			// ending a quoted chunk
			rawArg = append(rawArg, argFragment{s: line[start:i], quoted: true})
			start = i + 1
			quoted = false
			continue
		}
		// found character worth saving; make sure we're saving
		if start < 0 {
			start = i
		}
	}

	if cmd.name == "" {
		if cmd.want != "" || len(cmd.conds) > 0 || len(cmd.rawArgs) > 0 || cmd.background {
			// The line contains a command prefix or suffix, but no actual command.
			return nil, errors.New("missing command")
		}

		// The line is blank, or contains only a comment.
		return nil, nil
	}

	if n := len(cmd.rawArgs); n > 0 {
		last := cmd.rawArgs[n-1]
		if len(last) == 1 && !last[0].quoted && last[0].s == "&" {
			cmd.background = true
			cmd.rawArgs = cmd.rawArgs[:n-1]
		}
	}
	return cmd, nil
}

// expandArgs expands the shell variables in rawArgs and joins them to form the
// final arguments to pass to a command.
func expandArgs(s *State, rawArgs [][]argFragment, regexpArgs []int) []string {
	args := make([]string, 0, len(rawArgs))
	for i, frags := range rawArgs {
		isRegexp := false
		for _, j := range regexpArgs {
			if i == j {
				isRegexp = true
				break
			}
		}

		var b strings.Builder
		for _, frag := range frags {
			if frag.quoted {
				b.WriteString(frag.s)
			} else {
				b.WriteString(s.ExpandEnv(frag.s, isRegexp))
			}
		}
		args = append(args, b.String())
	}
	return args
}

// quoteArgs returns a string that parse would parse as args when passed to a command.
//
// TODO(bcmills): This function should have a fuzz test.
func quoteArgs(args []string) string {
	var b strings.Builder
	for i, arg := range args {
		if i > 0 {
			b.WriteString(" ")
		}
		if strings.ContainsAny(arg, "'"+argSepChars) {
			// Quote the argument to a form that would be parsed as a single argument.
			b.WriteString("'")
			b.WriteString(strings.ReplaceAll(arg, "'", "''"))
			b.WriteString("'")
		} else {
			b.WriteString(arg)
		}
	}
	return b.String()
}

func (e *Engine) conditionsActive(s *State, conds []condition) (bool, error) {
	for _, cond := range conds {
		var impl Cond
		prefix, suffix, ok := strings.Cut(cond.tag, ":")
		if ok {
			impl = e.Conds[prefix]
			if impl == nil {
				return false, fmt.Errorf("unknown condition prefix %q", prefix)
			}
			if !impl.Usage().Prefix {
				return false, fmt.Errorf("condition %q cannot be used with a suffix", prefix)
			}
		} else {
			impl = e.Conds[cond.tag]
			if impl == nil {
				return false, fmt.Errorf("unknown condition %q", cond.tag)
			}
			if impl.Usage().Prefix {
				return false, fmt.Errorf("condition %q requires a suffix", cond.tag)
			}
		}
		active, err := impl.Eval(s, suffix)

		if err != nil {
			return false, fmt.Errorf("evaluating condition %q: %w", cond.tag, err)
		}
		if active != cond.want {
			return false, nil
		}
	}

	return true, nil
}

func (e *Engine) runCommand(s *State, cmd *command, impl Cmd) error {
	if impl == nil {
		return cmdError(cmd, errors.New("unknown command"))
	}

	async := impl.Usage().Async
	if cmd.background && !async {
		return cmdError(cmd, errors.New("command cannot be run in background"))
	}

	wait, runErr := impl.Run(s, cmd.args...)
	if wait == nil {
		if async && runErr == nil {
			return cmdError(cmd, errors.New("internal error: async command returned a nil WaitFunc"))
		}
		return checkStatus(cmd, runErr)
	}
	if runErr != nil {
		return cmdError(cmd, errors.New("internal error: command returned both an error and a WaitFunc"))
	}

	if cmd.background {
		s.background = append(s.background, backgroundCmd{
			command: cmd,
			wait:    wait,
		})
		// Clear stdout and stderr, since they no longer correspond to the last
		// command executed.
		s.stdout = ""
		s.stderr = ""
		return nil
	}

	if wait != nil {
		stdout, stderr, waitErr := wait(s)
		s.stdout = stdout
		s.stderr = stderr
		if stdout != "" {
			s.Logf("[stdout]\n%s", stdout)
		}
		if stderr != "" {
			s.Logf("[stderr]\n%s", stderr)
		}
		if cmdErr := checkStatus(cmd, waitErr); cmdErr != nil {
			return cmdErr
		}
		if waitErr != nil {
			// waitErr was expected (by cmd.want), so log it instead of returning it.
			s.Logf("[%v]\n", waitErr)
		}
	}
	return nil
}

func checkStatus(cmd *command, err error) error {
	if err == nil {
		if cmd.want == failure {
			return cmdError(cmd, ErrUnexpectedSuccess)
		}
		return nil
	}

	if s := (stopError{}); errors.As(err, &s) {
		// This error originated in the Stop command.
		// Propagate it as-is.
		return cmdError(cmd, err)
	}

	if w := (waitError{}); errors.As(err, &w) {
		// This error was surfaced from a background process by a call to Wait.
		// Add a call frame for Wait itself, but ignore its "want" field.
		// (Wait itself cannot fail to wait on commands or else it would leak
		// processes and/or goroutines — so a negative assertion for it would be at
		// best ambiguous.)
		return cmdError(cmd, err)
	}

	if cmd.want == success {
		return cmdError(cmd, err)
	}

	if cmd.want == failure && (errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled)) {
		// The command was terminated because the script is no longer interested in
		// its output, so we don't know what it would have done had it run to
		// completion — for all we know, it could have exited without error if it
		// ran just a smidge faster.
		return cmdError(cmd, err)
	}

	return nil
}

// ListCmds prints to w a list of the named commands,
// annotating each with its arguments and a short usage summary.
// If verbose is true, ListCmds prints full details for each command.
//
// Each of the name arguments should be a command name.
// If no names are passed as arguments, ListCmds lists all the
// commands registered in e.
func (e *Engine) ListCmds(w io.Writer, verbose bool, names ...string) error {
	if names == nil {
		names = make([]string, 0, len(e.Cmds))
		for name := range e.Cmds {
			names = append(names, name)
		}
		sort.Strings(names)
	}

	for _, name := range names {
		cmd := e.Cmds[name]
		usage := cmd.Usage()

		suffix := ""
		if usage.Async {
			suffix = " [&]"
		}

		_, err := fmt.Fprintf(w, "%s %s%s\n\t%s\n", name, usage.Args, suffix, usage.Summary)
		if err != nil {
			return err
		}

		if verbose {
			if _, err := io.WriteString(w, "\n"); err != nil {
				return err
			}
			for _, line := range usage.Detail {
				if err := wrapLine(w, line, 60, "\t"); err != nil {
					return err
				}
			}
			if _, err := io.WriteString(w, "\n"); err != nil {
				return err
			}
		}
	}

	return nil
}

func wrapLine(w io.Writer, line string, cols int, indent string) error {
	line = strings.TrimLeft(line, " ")
	for len(line) > cols {
		bestSpace := -1
		for i, r := range line {
			if r == ' ' {
				if i <= cols || bestSpace < 0 {
					bestSpace = i
				}
				if i > cols {
					break
				}
			}
		}
		if bestSpace < 0 {
			break
		}

		if _, err := fmt.Fprintf(w, "%s%s\n", indent, line[:bestSpace]); err != nil {
			return err
		}
		line = line[bestSpace+1:]
	}

	_, err := fmt.Fprintf(w, "%s%s\n", indent, line)
	return err
}

// ListConds prints to w a list of conditions, one per line,
// annotating each with a description and whether the condition
// is true in the state s (if s is non-nil).
//
// Each of the tag arguments should be a condition string of
// the form "name" or "name:suffix". If no tags are passed as
// arguments, ListConds lists all conditions registered in
// the engine e.
func (e *Engine) ListConds(w io.Writer, s *State, tags ...string) error {
	if tags == nil {
		tags = make([]string, 0, len(e.Conds))
		for name := range e.Conds {
			tags = append(tags, name)
		}
		sort.Strings(tags)
	}

	for _, tag := range tags {
		if prefix, suffix, ok := strings.Cut(tag, ":"); ok {
			cond := e.Conds[prefix]
			if cond == nil {
				return fmt.Errorf("unknown condition prefix %q", prefix)
			}
			usage := cond.Usage()
			if !usage.Prefix {
				return fmt.Errorf("condition %q cannot be used with a suffix", prefix)
			}

			activeStr := ""
			if s != nil {
				if active, _ := cond.Eval(s, suffix); active {
					activeStr = " (active)"
				}
			}
			_, err := fmt.Fprintf(w, "[%s]%s\n\t%s\n", tag, activeStr, usage.Summary)
			if err != nil {
				return err
			}
			continue
		}

		cond := e.Conds[tag]
		if cond == nil {
			return fmt.Errorf("unknown condition %q", tag)
		}
		var err error
		usage := cond.Usage()
		if usage.Prefix {
			_, err = fmt.Fprintf(w, "[%s:*]\n\t%s\n", tag, usage.Summary)
		} else {
			activeStr := ""
			if s != nil {
				if ok, _ := cond.Eval(s, ""); ok {
					activeStr = " (active)"
				}
			}
			_, err = fmt.Fprintf(w, "[%s]%s\n\t%s\n", tag, activeStr, usage.Summary)
		}
		if err != nil {
			return err
		}
	}

	return nil
}


================================================
FILE: errors.go
================================================
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package script

import (
	"errors"
	"fmt"
)

// ErrUnexpectedSuccess indicates that a script command that was expected to
// fail (as indicated by a "!" prefix) instead completed successfully.
var ErrUnexpectedSuccess = errors.New("unexpected success")

// A CommandError describes an error resulting from attempting to execute a
// specific command.
type CommandError struct {
	File string
	Line int
	Op   string
	Args []string
	Err  error
}

func cmdError(cmd *command, err error) *CommandError {
	return &CommandError{
		File: cmd.file,
		Line: cmd.line,
		Op:   cmd.name,
		Args: cmd.args,
		Err:  err,
	}
}

func (e *CommandError) Error() string {
	if len(e.Args) == 0 {
		return fmt.Sprintf("%s:%d: %s: %v", e.File, e.Line, e.Op, e.Err)
	}
	return fmt.Sprintf("%s:%d: %s %s: %v", e.File, e.Line, e.Op, quoteArgs(e.Args), e.Err)
}

func (e *CommandError) Unwrap() error { return e.Err }

// A UsageError reports the valid arguments for a command.
//
// It may be returned in response to invalid arguments.
type UsageError struct {
	Name    string
	Command Cmd
}

func (e *UsageError) Error() string {
	usage := e.Command.Usage()
	suffix := ""
	if usage.Async {
		suffix = " [&]"
	}
	return fmt.Sprintf("usage: %s %s%s", e.Name, usage.Args, suffix)
}

// ErrUsage may be returned by a Command to indicate that it was called with
// invalid arguments; its Usage method may be called to obtain details.
var ErrUsage = errors.New("invalid usage")


================================================
FILE: go.mod
================================================
module rsc.io/script

go 1.21

require golang.org/x/tools v0.14.0


================================================
FILE: go.sum
================================================
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=


================================================
FILE: internal/diff/diff.go
================================================
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package diff

import (
	"bytes"
	"fmt"
	"sort"
	"strings"
)

// A pair is a pair of values tracked for both the x and y side of a diff.
// It is typically a pair of line indexes.
type pair struct{ x, y int }

// Diff returns an anchored diff of the two texts old and new
// in the “unified diff” format. If old and new are identical,
// Diff returns a nil slice (no output).
//
// Unix diff implementations typically look for a diff with
// the smallest number of lines inserted and removed,
// which can in the worst case take time quadratic in the
// number of lines in the texts. As a result, many implementations
// either can be made to run for a long time or cut off the search
// after a predetermined amount of work.
//
// In contrast, this implementation looks for a diff with the
// smallest number of “unique” lines inserted and removed,
// where unique means a line that appears just once in both old and new.
// We call this an “anchored diff” because the unique lines anchor
// the chosen matching regions. An anchored diff is usually clearer
// than a standard diff, because the algorithm does not try to
// reuse unrelated blank lines or closing braces.
// The algorithm also guarantees to run in O(n log n) time
// instead of the standard O(n²) time.
//
// Some systems call this approach a “patience diff,” named for
// the “patience sorting” algorithm, itself named for a solitaire card game.
// We avoid that name for two reasons. First, the name has been used
// for a few different variants of the algorithm, so it is imprecise.
// Second, the name is frequently interpreted as meaning that you have
// to wait longer (to be patient) for the diff, meaning that it is a slower algorithm,
// when in fact the algorithm is faster than the standard one.
func Diff(oldName string, old []byte, newName string, new []byte) []byte {
	if bytes.Equal(old, new) {
		return nil
	}
	x := lines(old)
	y := lines(new)

	// Print diff header.
	var out bytes.Buffer
	fmt.Fprintf(&out, "diff %s %s\n", oldName, newName)
	fmt.Fprintf(&out, "--- %s\n", oldName)
	fmt.Fprintf(&out, "+++ %s\n", newName)

	// Loop over matches to consider,
	// expanding each match to include surrounding lines,
	// and then printing diff chunks.
	// To avoid setup/teardown cases outside the loop,
	// tgs returns a leading {0,0} and trailing {len(x), len(y)} pair
	// in the sequence of matches.
	var (
		done  pair     // printed up to x[:done.x] and y[:done.y]
		chunk pair     // start lines of current chunk
		count pair     // number of lines from each side in current chunk
		ctext []string // lines for current chunk
	)
	for _, m := range tgs(x, y) {
		if m.x < done.x {
			// Already handled scanning forward from earlier match.
			continue
		}

		// Expand matching lines as far possible,
		// establishing that x[start.x:end.x] == y[start.y:end.y].
		// Note that on the first (or last) iteration we may (or definitely do)
		// have an empty match: start.x==end.x and start.y==end.y.
		start := m
		for start.x > done.x && start.y > done.y && x[start.x-1] == y[start.y-1] {
			start.x--
			start.y--
		}
		end := m
		for end.x < len(x) && end.y < len(y) && x[end.x] == y[end.y] {
			end.x++
			end.y++
		}

		// Emit the mismatched lines before start into this chunk.
		// (No effect on first sentinel iteration, when start = {0,0}.)
		for _, s := range x[done.x:start.x] {
			ctext = append(ctext, "-"+s)
			count.x++
		}
		for _, s := range y[done.y:start.y] {
			ctext = append(ctext, "+"+s)
			count.y++
		}

		// If we're not at EOF and have too few common lines,
		// the chunk includes all the common lines and continues.
		const C = 3 // number of context lines
		if (end.x < len(x) || end.y < len(y)) &&
			(end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) {
			for _, s := range x[start.x:end.x] {
				ctext = append(ctext, " "+s)
				count.x++
				count.y++
			}
			done = end
			continue
		}

		// End chunk with common lines for context.
		if len(ctext) > 0 {
			n := end.x - start.x
			if n > C {
				n = C
			}
			for _, s := range x[start.x : start.x+n] {
				ctext = append(ctext, " "+s)
				count.x++
				count.y++
			}
			done = pair{start.x + n, start.y + n}

			// Format and emit chunk.
			// Convert line numbers to 1-indexed.
			// Special case: empty file shows up as 0,0 not 1,0.
			if count.x > 0 {
				chunk.x++
			}
			if count.y > 0 {
				chunk.y++
			}
			fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y)
			for _, s := range ctext {
				out.WriteString(s)
			}
			count.x = 0
			count.y = 0
			ctext = ctext[:0]
		}

		// If we reached EOF, we're done.
		if end.x >= len(x) && end.y >= len(y) {
			break
		}

		// Otherwise start a new chunk.
		chunk = pair{end.x - C, end.y - C}
		for _, s := range x[chunk.x:end.x] {
			ctext = append(ctext, " "+s)
			count.x++
			count.y++
		}
		done = end
	}

	return out.Bytes()
}

// lines returns the lines in the file x, including newlines.
// If the file does not end in a newline, one is supplied
// along with a warning about the missing newline.
func lines(x []byte) []string {
	l := strings.SplitAfter(string(x), "\n")
	if l[len(l)-1] == "" {
		l = l[:len(l)-1]
	} else {
		// Treat last line as having a message about the missing newline attached,
		// using the same text as BSD/GNU diff (including the leading backslash).
		l[len(l)-1] += "\n\\ No newline at end of file\n"
	}
	return l
}

// tgs returns the pairs of indexes of the longest common subsequence
// of unique lines in x and y, where a unique line is one that appears
// once in x and once in y.
//
// The longest common subsequence algorithm is as described in
// Thomas G. Szymanski, “A Special Case of the Maximal Common
// Subsequence Problem,” Princeton TR #170 (January 1975),
// available at https://research.swtch.com/tgs170.pdf.
func tgs(x, y []string) []pair {
	// Count the number of times each string appears in a and b.
	// We only care about 0, 1, many, counted as 0, -1, -2
	// for the x side and 0, -4, -8 for the y side.
	// Using negative numbers now lets us distinguish positive line numbers later.
	m := make(map[string]int)
	for _, s := range x {
		if c := m[s]; c > -2 {
			m[s] = c - 1
		}
	}
	for _, s := range y {
		if c := m[s]; c > -8 {
			m[s] = c - 4
		}
	}

	// Now unique strings can be identified by m[s] = -1+-4.
	//
	// Gather the indexes of those strings in x and y, building:
	//	xi[i] = increasing indexes of unique strings in x.
	//	yi[i] = increasing indexes of unique strings in y.
	//	inv[i] = index j such that x[xi[i]] = y[yi[j]].
	var xi, yi, inv []int
	for i, s := range y {
		if m[s] == -1+-4 {
			m[s] = len(yi)
			yi = append(yi, i)
		}
	}
	for i, s := range x {
		if j, ok := m[s]; ok && j >= 0 {
			xi = append(xi, i)
			inv = append(inv, j)
		}
	}

	// Apply Algorithm A from Szymanski's paper.
	// In those terms, A = J = inv and B = [0, n).
	// We add sentinel pairs {0,0}, and {len(x),len(y)}
	// to the returned sequence, to help the processing loop.
	J := inv
	n := len(xi)
	T := make([]int, n)
	L := make([]int, n)
	for i := range T {
		T[i] = n + 1
	}
	for i := 0; i < n; i++ {
		k := sort.Search(n, func(k int) bool {
			return T[k] >= J[i]
		})
		T[k] = J[i]
		L[i] = k + 1
	}
	k := 0
	for _, v := range L {
		if k < v {
			k = v
		}
	}
	seq := make([]pair, 2+k)
	seq[1+k] = pair{len(x), len(y)} // sentinel at end
	lastj := n
	for i := n - 1; i >= 0; i-- {
		if L[i] == k && J[i] < lastj {
			seq[k] = pair{xi[i], yi[J[i]]}
			k--
		}
	}
	seq[0] = pair{0, 0} // sentinel at start
	return seq
}


================================================
FILE: internal/diff/diff_test.go
================================================
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package diff

import (
	"bytes"
	"path/filepath"
	"testing"

	"golang.org/x/tools/txtar"
)

func clean(text []byte) []byte {
	text = bytes.ReplaceAll(text, []byte("$\n"), []byte("\n"))
	text = bytes.TrimSuffix(text, []byte("^D\n"))
	return text
}

func Test(t *testing.T) {
	files, _ := filepath.Glob("testdata/*.txt")
	if len(files) == 0 {
		t.Fatalf("no testdata")
	}

	for _, file := range files {
		t.Run(filepath.Base(file), func(t *testing.T) {
			a, err := txtar.ParseFile(file)
			if err != nil {
				t.Fatal(err)
			}
			if len(a.Files) != 3 || a.Files[2].Name != "diff" {
				t.Fatalf("%s: want three files, third named \"diff\"", file)
			}
			diffs := Diff(a.Files[0].Name, clean(a.Files[0].Data), a.Files[1].Name, clean(a.Files[1].Data))
			want := clean(a.Files[2].Data)
			if !bytes.Equal(diffs, want) {
				t.Fatalf("%s: have:\n%s\nwant:\n%s\n%s", file,
					diffs, want, Diff("have", diffs, "want", want))
			}
		})
	}
}


================================================
FILE: internal/diff/testdata/allnew.txt
================================================
-- old --
-- new --
a
b
c
-- diff --
diff old new
--- old
+++ new
@@ -0,0 +1,3 @@
+a
+b
+c


================================================
FILE: internal/diff/testdata/allold.txt
================================================
-- old --
a
b
c
-- new --
-- diff --
diff old new
--- old
+++ new
@@ -1,3 +0,0 @@
-a
-b
-c


================================================
FILE: internal/diff/testdata/basic.txt
================================================
Example from Hunt and McIlroy, “An Algorithm for Differential File Comparison.”
https://www.cs.dartmouth.edu/~doug/diff.pdf

-- old --
a
b
c
d
e
f
g
-- new --
w
a
b
x
y
z
e
-- diff --
diff old new
--- old
+++ new
@@ -1,7 +1,7 @@
+w
 a
 b
-c
-d
+x
+y
+z
 e
-f
-g


================================================
FILE: internal/diff/testdata/dups.txt
================================================
-- old --
a

b

c

d

e

f
-- new --
a

B

C

d

e

f
-- diff --
diff old new
--- old
+++ new
@@ -1,8 +1,8 @@
 a
 $
-b
-
-c
+B
+
+C
 $
 d
 $


================================================
FILE: internal/diff/testdata/end.txt
================================================
-- old --
1
2
3
4
5
6
7
eight
nine
ten
eleven
-- new --
1
2
3
4
5
6
7
8
9
10
-- diff --
diff old new
--- old
+++ new
@@ -5,7 +5,6 @@
 5
 6
 7
-eight
-nine
-ten
-eleven
+8
+9
+10


================================================
FILE: internal/diff/testdata/eof.txt
================================================
-- old --
a
b
c^D
-- new --
a
b
c^D
-- diff --


================================================
FILE: internal/diff/testdata/eof1.txt
================================================
-- old --
a
b
c
-- new --
a
b
c^D
-- diff --
diff old new
--- old
+++ new
@@ -1,3 +1,3 @@
 a
 b
-c
+c
\ No newline at end of file


================================================
FILE: internal/diff/testdata/eof2.txt
================================================
-- old --
a
b
c^D
-- new --
a
b
c
-- diff --
diff old new
--- old
+++ new
@@ -1,3 +1,3 @@
 a
 b
-c
\ No newline at end of file
+c


================================================
FILE: internal/diff/testdata/long.txt
================================================
-- old --
1
2
3
4
5
6
7
8
9
10
11
12
13
14
14½
15
16
17
18
19
20
-- new --
1
2
3
4
5
6
8
9
10
11
12
13
14
17
18
19
20
-- diff --
diff old new
--- old
+++ new
@@ -4,7 +4,6 @@
 4
 5
 6
-7
 8
 9
 10
@@ -12,9 +11,6 @@
 12
 13
 14
-14½
-15
-16
 17
 18
 19


================================================
FILE: internal/diff/testdata/same.txt
================================================
-- old --
hello world
-- new --
hello world
-- diff --


================================================
FILE: internal/diff/testdata/start.txt
================================================
-- old --
e
pi
4
5
6
7
8
9
10
-- new --
1
2
3
4
5
6
7
8
9
10
-- diff --
diff old new
--- old
+++ new
@@ -1,5 +1,6 @@
-e
-pi
+1
+2
+3
 4
 5
 6


================================================
FILE: internal/diff/testdata/triv.txt
================================================
Another example from Hunt and McIlroy,
“An Algorithm for Differential File Comparison.”
https://www.cs.dartmouth.edu/~doug/diff.pdf

Anchored diff gives up on finding anything,
since there are no unique lines.

-- old --
a
b
c
a
b
b
a
-- new --
c
a
b
a
b
c
-- diff --
diff old new
--- old
+++ new
@@ -1,7 +1,6 @@
-a
-b
-c
-a
-b
-b
-a
+c
+a
+b
+a
+b
+c


================================================
FILE: scripttest/scripttest.go
================================================
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package scripttest adapts the script engine for use in tests.
package scripttest

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"

	"golang.org/x/tools/txtar"
	"rsc.io/script"
)

// DefaultCmds returns a set of broadly useful script commands.
//
// This set includes all of the commands in script.DefaultCmds,
// as well as a "skip" command that halts the script and causes the
// testing.TB passed to Run to be skipped.
func DefaultCmds() map[string]script.Cmd {
	cmds := script.DefaultCmds()
	cmds["skip"] = Skip()
	return cmds
}

// DefaultConds returns a set of broadly useful script conditions.
//
// This set includes all of the conditions in script.DefaultConds,
// as well as:
//
//   - Conditions of the form "exec:foo" are active when the executable "foo" is
//     found in the test process's PATH, and inactive when the executable is
//     not found.
//
//   - "short" is active when testing.Short() is true.
//
//   - "verbose" is active when testing.Verbose() is true.
func DefaultConds() map[string]script.Cond {
	conds := script.DefaultConds()
	conds["exec"] = CachedExec()
	conds["short"] = script.BoolCondition("testing.Short()", testing.Short())
	conds["verbose"] = script.BoolCondition("testing.Verbose()", testing.Verbose())
	return conds
}

// Run runs the script from the given filename starting at the given initial state.
// When the script completes, Run closes the state.
func Run(t testing.TB, e *script.Engine, s *script.State, filename string, testScript io.Reader) {
	t.Helper()
	err := func() (err error) {
		log := new(strings.Builder)
		log.WriteString("\n") // Start output on a new line for consistent indentation.

		// Defer writing to the test log in case the script engine panics during execution,
		// but write the log before we write the final "skip" or "FAIL" line.
		t.Helper()
		defer func() {
			t.Helper()

			if closeErr := s.CloseAndWait(log); err == nil {
				err = closeErr
			}

			if log.Len() > 0 {
				t.Log(strings.TrimSuffix(log.String(), "\n"))
			}
		}()

		if testing.Verbose() {
			// Add the environment to the start of the script log.
			wait, err := script.Env().Run(s)
			if err != nil {
				t.Fatal(err)
			}
			if wait != nil {
				stdout, stderr, err := wait(s)
				if err != nil {
					t.Fatalf("env: %v\n%s", err, stderr)
				}
				if len(stdout) > 0 {
					s.Logf("%s\n", stdout)
				}
			}
		}

		return e.Execute(s, filename, bufio.NewReader(testScript), log)
	}()

	if skip := (skipError{}); errors.As(err, &skip) {
		if skip.msg == "" {
			t.Skip("SKIP")
		} else {
			t.Skipf("SKIP: %v", skip.msg)
		}
	}
	if err != nil {
		t.Errorf("FAIL: %v", err)
	}
}

// Skip returns a sentinel error that causes Run to mark the test as skipped.
func Skip() script.Cmd {
	return script.Command(
		script.CmdUsage{
			Summary: "skip the current test",
			Args:    "[msg]",
		},
		func(_ *script.State, args ...string) (script.WaitFunc, error) {
			if len(args) > 1 {
				return nil, script.ErrUsage
			}
			if len(args) == 0 {
				return nil, skipError{""}
			}
			return nil, skipError{args[0]}
		})
}

type skipError struct {
	msg string
}

func (s skipError) Error() string {
	if s.msg == "" {
		return "skip"
	}
	return s.msg
}

// CachedExec returns a Condition that reports whether the PATH of the test
// binary itself (not the script's current environment) contains the named
// executable.
func CachedExec() script.Cond {
	return script.CachedCondition(
		"<suffix> names an executable in the test binary's PATH",
		func(name string) (bool, error) {
			_, err := exec.LookPath(name)
			return err == nil, nil
		})
}

func Test(t *testing.T, ctx context.Context, engine *script.Engine, env []string, pattern string) {
	gracePeriod := 100 * time.Millisecond
	if deadline, ok := t.Deadline(); ok {
		timeout := time.Until(deadline)

		// If time allows, increase the termination grace period to 5% of the
		// remaining time.
		if gp := timeout / 20; gp > gracePeriod {
			gracePeriod = gp
		}

		// When we run commands that execute subprocesses, we want to reserve two
		// grace periods to clean up. We will send the first termination signal when
		// the context expires, then wait one grace period for the process to
		// produce whatever useful output it can (such as a stack trace). After the
		// first grace period expires, we'll escalate to os.Kill, leaving the second
		// grace period for the test function to record its output before the test
		// process itself terminates.
		timeout -= 2 * gracePeriod

		var cancel context.CancelFunc
		ctx, cancel = context.WithTimeout(ctx, timeout)
		t.Cleanup(cancel)
	}

	files, _ := filepath.Glob(pattern)
	if len(files) == 0 {
		t.Fatal("no testdata")
	}
	for _, file := range files {
		file := file
		name := strings.TrimSuffix(filepath.Base(file), ".txt")
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			workdir := t.TempDir()
			s, err := script.NewState(ctx, workdir, env)
			if err != nil {
				t.Fatal(err)
			}

			// Unpack archive.
			a, err := txtar.ParseFile(file)
			if err != nil {
				t.Fatal(err)
			}
			initScriptDirs(t, s)
			if err := s.ExtractFiles(a); err != nil {
				t.Fatal(err)
			}

			t.Log(time.Now().UTC().Format(time.RFC3339))
			work, _ := s.LookupEnv("WORK")
			t.Logf("$WORK=%s", work)

			// Note: Do not use filepath.Base(file) here:
			// editors that can jump to file:line references in the output
			// will work better seeing the full path relative to cmd/go
			// (where the "go test" command is usually run).
			Run(t, engine, s, file, bytes.NewReader(a.Comment))
		})
	}
}

func initScriptDirs(t testing.TB, s *script.State) {
	must := func(err error) {
		if err != nil {
			t.Helper()
			t.Fatal(err)
		}
	}

	work := s.Getwd()
	must(s.Setenv("WORK", work))
	must(os.MkdirAll(filepath.Join(work, "tmp"), 0777))
	must(s.Setenv(tempEnvName(), filepath.Join(work, "tmp")))
}

func tempEnvName() string {
	switch runtime.GOOS {
	case "windows":
		return "TMP"
	case "plan9":
		return "TMPDIR" // actually plan 9 doesn't have one at all but this is fine
	default:
		return "TMPDIR"
	}
}


================================================
FILE: scripttest/scripttest_test.go
================================================
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package scripttest_test

import (
	"context"
	"os"
	"testing"

	"rsc.io/script"
	"rsc.io/script/scripttest"
)

func TestAll(t *testing.T) {
	ctx := context.Background()
	engine := &script.Engine{
		Conds: scripttest.DefaultConds(),
		Cmds:  scripttest.DefaultCmds(),
		Quiet: !testing.Verbose(),
	}
	env := os.Environ()
	scripttest.Test(t, ctx, engine, env, "testdata/*.txt")
}


================================================
FILE: scripttest/testdata/basic.txt
================================================
cat hello.txt
stdout 'hello world'
! stderr 'hello world'

-- hello.txt --
hello world


================================================
FILE: state.go
================================================
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package script

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"

	"golang.org/x/tools/txtar"
)

// A State encapsulates the current state of a running script engine,
// including the script environment and any running background commands.
type State struct {
	engine *Engine // the engine currently executing the script, if any

	ctx    context.Context
	cancel context.CancelFunc
	file   string
	log    bytes.Buffer

	workdir string            // initial working directory
	pwd     string            // current working directory during execution
	env     []string          // environment list (for os/exec)
	envMap  map[string]string // environment mapping (matches env)
	stdout  string            // standard output from last 'go' command; for 'stdout' command
	stderr  string            // standard error from last 'go' command; for 'stderr' command

	background []backgroundCmd
}

type backgroundCmd struct {
	*command
	wait WaitFunc
}

// NewState returns a new State permanently associated with ctx, with its
// initial working directory in workdir and its initial environment set to
// initialEnv (or os.Environ(), if initialEnv is nil).
//
// The new State also contains pseudo-environment-variables for
// ${/} and ${:} (for the platform's path and list separators respectively),
// but does not pass those to subprocesses.
func NewState(ctx context.Context, workdir string, initialEnv []string) (*State, error) {
	absWork, err := filepath.Abs(workdir)
	if err != nil {
		return nil, err
	}

	ctx, cancel := context.WithCancel(ctx)

	// Make a fresh copy of the env slice to avoid aliasing bugs if we ever
	// start modifying it in place; this also establishes the invariant that
	// s.env contains no duplicates.
	env := cleanEnv(initialEnv, absWork)

	envMap := make(map[string]string, len(env))

	// Add entries for ${:} and ${/} to make it easier to write platform-independent
	// paths in scripts.
	envMap["/"] = string(os.PathSeparator)
	envMap[":"] = string(os.PathListSeparator)

	for _, kv := range env {
		if k, v, ok := strings.Cut(kv, "="); ok {
			envMap[k] = v
		}
	}

	s := &State{
		ctx:     ctx,
		cancel:  cancel,
		workdir: absWork,
		pwd:     absWork,
		env:     env,
		envMap:  envMap,
	}
	s.Setenv("PWD", absWork)
	return s, nil
}

// CloseAndWait cancels the State's Context and waits for any background commands to
// finish. If any remaining background command ended in an unexpected state,
// Close returns a non-nil error.
func (s *State) CloseAndWait(log io.Writer) error {
	s.cancel()
	wait, err := Wait().Run(s)
	if wait != nil {
		panic("script: internal error: Wait unexpectedly returns its own WaitFunc")
	}
	if flushErr := s.flushLog(log); err == nil {
		err = flushErr
	}
	return err
}

// Chdir changes the State's working directory to the given path.
func (s *State) Chdir(path string) error {
	dir := s.Path(path)
	if _, err := os.Stat(dir); err != nil {
		return &fs.PathError{Op: "Chdir", Path: dir, Err: err}
	}
	s.pwd = dir
	s.Setenv("PWD", dir)
	return nil
}

// Context returns the Context with which the State was created.
func (s *State) Context() context.Context {
	return s.ctx
}

// Environ returns a copy of the current script environment,
// in the form "key=value".
func (s *State) Environ() []string {
	return append([]string(nil), s.env...)
}

// ExpandEnv replaces ${var} or $var in the string according to the values of
// the environment variables in s. References to undefined variables are
// replaced by the empty string.
func (s *State) ExpandEnv(str string, inRegexp bool) string {
	return os.Expand(str, func(key string) string {
		e := s.envMap[key]
		if inRegexp {
			// Quote to literal strings: we want paths like C:\work\go1.4 to remain
			// paths rather than regular expressions.
			e = regexp.QuoteMeta(e)
		}
		return e
	})
}

// ExtractFiles extracts the files in ar to the state's current directory,
// expanding any environment variables within each name.
//
// The files must reside within the working directory with which the State was
// originally created.
func (s *State) ExtractFiles(ar *txtar.Archive) error {
	wd := s.workdir

	// Add trailing separator to terminate wd.
	// This prevents extracting to outside paths which prefix wd,
	// e.g. extracting to /home/foobar when wd is /home/foo
	if wd == "" {
		panic("s.workdir is unexpectedly empty")
	}
	if !os.IsPathSeparator(wd[len(wd)-1]) {
		wd += string(filepath.Separator)
	}

	for _, f := range ar.Files {
		name := s.Path(s.ExpandEnv(f.Name, false))

		if !strings.HasPrefix(name, wd) {
			return fmt.Errorf("file %#q is outside working directory", f.Name)
		}

		if err := os.MkdirAll(filepath.Dir(name), 0777); err != nil {
			return err
		}
		if err := os.WriteFile(name, f.Data, 0666); err != nil {
			return err
		}
	}

	return nil
}

// Getwd returns the directory in which to run the next script command.
func (s *State) Getwd() string { return s.pwd }

// Logf writes output to the script's log without updating its stdout or stderr
// buffers. (The output log functions as a kind of meta-stderr.)
func (s *State) Logf(format string, args ...any) {
	fmt.Fprintf(&s.log, format, args...)
}

// flushLog writes the contents of the script's log to w and clears the log.
func (s *State) flushLog(w io.Writer) error {
	_, err := w.Write(s.log.Bytes())
	s.log.Reset()
	return err
}

// LookupEnv retrieves the value of the environment variable in s named by the key.
func (s *State) LookupEnv(key string) (string, bool) {
	v, ok := s.envMap[key]
	return v, ok
}

// Path returns the absolute path in the host operating system for a
// script-based (generally slash-separated and relative) path.
func (s *State) Path(path string) string {
	if filepath.IsAbs(path) {
		return filepath.Clean(path)
	}
	return filepath.Join(s.pwd, path)
}

// Setenv sets the value of the environment variable in s named by the key.
func (s *State) Setenv(key, value string) error {
	s.env = cleanEnv(append(s.env, key+"="+value), s.pwd)
	s.envMap[key] = value
	return nil
}

// Stdout returns the stdout output of the last command run,
// or the empty string if no command has been run.
func (s *State) Stdout() string { return s.stdout }

// Stderr returns the stderr output of the last command run,
// or the empty string if no command has been run.
func (s *State) Stderr() string { return s.stderr }

// cleanEnv returns a copy of env with any duplicates removed in favor of
// later values and any required system variables defined.
//
// If env is nil, cleanEnv copies the environment from os.Environ().
func cleanEnv(env []string, pwd string) []string {
	// There are some funky edge-cases in this logic, especially on Windows (with
	// case-insensitive environment variables and variables with keys like "=C:").
	// Rather than duplicating exec.dedupEnv here, cheat and use exec.Cmd directly.
	cmd := &exec.Cmd{Env: env}
	cmd.Dir = pwd
	return cmd.Environ()
}
Download .txt
gitextract_2vdpsotp/

├── LICENSE
├── README.md
├── cmds.go
├── cmds_other.go
├── cmds_posix.go
├── conds.go
├── engine.go
├── errors.go
├── go.mod
├── go.sum
├── internal/
│   └── diff/
│       ├── diff.go
│       ├── diff_test.go
│       └── testdata/
│           ├── allnew.txt
│           ├── allold.txt
│           ├── basic.txt
│           ├── dups.txt
│           ├── end.txt
│           ├── eof.txt
│           ├── eof1.txt
│           ├── eof2.txt
│           ├── long.txt
│           ├── same.txt
│           ├── start.txt
│           └── triv.txt
├── scripttest/
│   ├── scripttest.go
│   ├── scripttest_test.go
│   └── testdata/
│       └── basic.txt
└── state.go
Download .txt
SYMBOL INDEX (130 symbols across 11 files)

FILE: cmds.go
  function DefaultCmds (line 28) | func DefaultCmds() map[string]Cmd {
  function Command (line 57) | func Command(usage CmdUsage, run func(*State, ...string) (WaitFunc, erro...
  type funcCmd (line 65) | type funcCmd struct
    method Run (line 70) | func (c *funcCmd) Run(s *State, args ...string) (WaitFunc, error) {
    method Usage (line 74) | func (c *funcCmd) Usage() *CmdUsage { return &c.usage }
  function firstNonFlag (line 78) | func firstNonFlag(rawArgs ...string) []int {
  function Cat (line 92) | func Cat() Cmd {
  function Cd (line 131) | func Cd() Cmd {
  function Chmod (line 146) | func Chmod() Cmd {
  function Cmp (line 179) | func Cmp() Cmd {
  function Cmpenv (line 197) | func Cmpenv() Cmd {
  function doCompare (line 213) | func doCompare(s *State, env bool, args ...string) error {
  function Cp (line 260) | func Cp() Cmd {
  function Echo (line 323) | func Echo() Cmd {
  function Env (line 358) | func Env() Cmd {
  function Exec (line 402) | func Exec(cancel func(*exec.Cmd) error, waitDelay time.Duration) Cmd {
  function startCommand (line 434) | func startCommand(s *State, name, path string, args []string, cancel fun...
  function lookPath (line 477) | func lookPath(s *State, command string) (string, error) {
  function pathEnvName (line 555) | func pathEnvName() string {
  function Exists (line 565) | func Exists() Cmd {
  function Grep (line 613) | func Grep() Cmd {
  constant matchUsage (line 629) | matchUsage = "[-count=N] [-q] 'pattern'"
  function match (line 632) | func match(s *State, args []string, text, name string) error {
  function Help (line 704) | func Help() Cmd {
  function Mkdir (line 760) | func Mkdir() Cmd {
  function Mv (line 783) | func Mv() Cmd {
  function Program (line 802) | func Program(name string, cancel func(*exec.Cmd) error, waitDelay time.D...
  function Replace (line 837) | func Replace() Cmd {
  function Rm (line 877) | func Rm() Cmd {
  function removeAll (line 903) | func removeAll(dir string) error {
  function Sleep (line 919) | func Sleep() Cmd {
  function Stderr (line 955) | func Stderr() Cmd {
  function Stdout (line 972) | func Stdout() Cmd {
  function Stop (line 990) | func Stop() Cmd {
  type stopError (line 1013) | type stopError struct
    method Error (line 1017) | func (s stopError) Error() string {
  function Symlink (line 1025) | func Symlink() Cmd {
  function Wait (line 1051) | func Wait() Cmd {
  type waitError (line 1105) | type waitError struct
    method Error (line 1109) | func (w waitError) Error() string {
    method Unwrap (line 1120) | func (w waitError) Unwrap() error {

FILE: cmds_other.go
  function isETXTBSY (line 9) | func isETXTBSY(err error) bool {

FILE: cmds_posix.go
  function isETXTBSY (line 14) | func isETXTBSY(err error) bool {

FILE: conds.go
  function DefaultConds (line 18) | func DefaultConds() map[string]Cond {
  function Condition (line 59) | func Condition(summary string, eval func(*State) (bool, error)) Cond {
  type funcCond (line 63) | type funcCond struct
    method Usage (line 68) | func (c *funcCond) Usage() *CondUsage { return &c.usage }
    method Eval (line 70) | func (c *funcCond) Eval(s *State, suffix string) (bool, error) {
  function PrefixCondition (line 78) | func PrefixCondition(summary string, eval func(*State, string) (bool, er...
  type prefixCond (line 82) | type prefixCond struct
    method Usage (line 87) | func (c *prefixCond) Usage() *CondUsage { return &c.usage }
    method Eval (line 89) | func (c *prefixCond) Eval(s *State, suffix string) (bool, error) {
  function BoolCondition (line 95) | func BoolCondition(summary string, v bool) Cond {
  type boolCond (line 99) | type boolCond struct
    method Usage (line 104) | func (b *boolCond) Usage() *CondUsage { return &b.usage }
    method Eval (line 106) | func (b *boolCond) Eval(s *State, suffix string) (bool, error) {
  function OnceCondition (line 118) | func OnceCondition(summary string, eval func() (bool, error)) Cond {
  type onceCond (line 122) | type onceCond struct
    method Usage (line 130) | func (l *onceCond) Usage() *CondUsage { return &l.usage }
    method Eval (line 132) | func (l *onceCond) Eval(s *State, suffix string) (bool, error) {
  function CachedCondition (line 146) | func CachedCondition(summary string, eval func(string) (bool, error)) Co...
  type cachedCond (line 150) | type cachedCond struct
    method Usage (line 156) | func (c *cachedCond) Usage() *CondUsage { return &c.usage }
    method Eval (line 158) | func (c *cachedCond) Eval(_ *State, suffix string) (bool, error) {

FILE: engine.go
  type Engine (line 69) | type Engine struct
    method Execute (line 164) | func (e *Engine) Execute(s *State, file string, script *bufio.Reader, ...
    method conditionsActive (line 525) | func (e *Engine) conditionsActive(s *State, conds []condition) (bool, ...
    method runCommand (line 559) | func (e *Engine) runCommand(s *State, cmd *command, impl Cmd) error {
    method ListCmds (line 658) | func (e *Engine) ListCmds(w io.Writer, verbose bool, names ...string) ...
    method ListConds (line 735) | func (e *Engine) ListConds(w io.Writer, s *State, tags ...string) error {
  function NewEngine (line 79) | func NewEngine() *Engine {
  type Cmd (line 87) | type Cmd interface
  type WaitFunc (line 106) | type WaitFunc
  type CmdUsage (line 110) | type CmdUsage struct
  type Cond (line 130) | type Cond interface
  type CondUsage (line 144) | type CondUsage struct
  type command (line 317) | type command struct
  type expectedStatus (line 330) | type expectedStatus
  constant success (line 333) | success          expectedStatus = ""
  constant failure (line 334) | failure          expectedStatus = "!"
  constant successOrFailure (line 335) | successOrFailure expectedStatus = "?"
  type argFragment (line 338) | type argFragment struct
  type condition (line 343) | type condition struct
  constant argSepChars (line 348) | argSepChars = " \t\r\n#"
  function parse (line 356) | func parse(filename string, lineno int, line string) (cmd *command, err ...
  function expandArgs (line 480) | func expandArgs(s *State, rawArgs [][]argFragment, regexpArgs []int) []s...
  function quoteArgs (line 507) | func quoteArgs(args []string) string {
  function checkStatus (line 613) | func checkStatus(cmd *command, err error) error {
  function wrapLine (line 699) | func wrapLine(w io.Writer, line string, cols int, indent string) error {

FILE: errors.go
  type CommandError (line 18) | type CommandError struct
    method Error (line 36) | func (e *CommandError) Error() string {
    method Unwrap (line 43) | func (e *CommandError) Unwrap() error { return e.Err }
  function cmdError (line 26) | func cmdError(cmd *command, err error) *CommandError {
  type UsageError (line 48) | type UsageError struct
    method Error (line 53) | func (e *UsageError) Error() string {

FILE: internal/diff/diff.go
  type pair (line 16) | type pair struct
  function Diff (line 46) | func Diff(oldName string, old []byte, newName string, new []byte) []byte {
  function lines (line 169) | func lines(x []byte) []string {
  function tgs (line 189) | func tgs(x, y []string) []pair {

FILE: internal/diff/diff_test.go
  function clean (line 15) | func clean(text []byte) []byte {
  function Test (line 21) | func Test(t *testing.T) {

FILE: scripttest/scripttest.go
  function DefaultCmds (line 31) | func DefaultCmds() map[string]script.Cmd {
  function DefaultConds (line 49) | func DefaultConds() map[string]script.Cond {
  function Run (line 59) | func Run(t testing.TB, e *script.Engine, s *script.State, filename strin...
  function Skip (line 113) | func Skip() script.Cmd {
  type skipError (line 130) | type skipError struct
    method Error (line 134) | func (s skipError) Error() string {
  function CachedExec (line 144) | func CachedExec() script.Cond {
  function Test (line 153) | func Test(t *testing.T, ctx context.Context, engine *script.Engine, env ...
  function initScriptDirs (line 217) | func initScriptDirs(t testing.TB, s *script.State) {
  function tempEnvName (line 231) | func tempEnvName() string {

FILE: scripttest/scripttest_test.go
  function TestAll (line 16) | func TestAll(t *testing.T) {

FILE: state.go
  type State (line 24) | type State struct
    method CloseAndWait (line 95) | func (s *State) CloseAndWait(log io.Writer) error {
    method Chdir (line 108) | func (s *State) Chdir(path string) error {
    method Context (line 119) | func (s *State) Context() context.Context {
    method Environ (line 125) | func (s *State) Environ() []string {
    method ExpandEnv (line 132) | func (s *State) ExpandEnv(str string, inRegexp bool) string {
    method ExtractFiles (line 149) | func (s *State) ExtractFiles(ar *txtar.Archive) error {
    method Getwd (line 181) | func (s *State) Getwd() string { return s.pwd }
    method Logf (line 185) | func (s *State) Logf(format string, args ...any) {
    method flushLog (line 190) | func (s *State) flushLog(w io.Writer) error {
    method LookupEnv (line 197) | func (s *State) LookupEnv(key string) (string, bool) {
    method Path (line 204) | func (s *State) Path(path string) string {
    method Setenv (line 212) | func (s *State) Setenv(key, value string) error {
    method Stdout (line 220) | func (s *State) Stdout() string { return s.stdout }
    method Stderr (line 224) | func (s *State) Stderr() string { return s.stderr }
  type backgroundCmd (line 42) | type backgroundCmd struct
  function NewState (line 54) | func NewState(ctx context.Context, workdir string, initialEnv []string) ...
  function cleanEnv (line 230) | func cleanEnv(env []string, pwd string) []string {
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (97K chars).
[
  {
    "path": "LICENSE",
    "chars": 1479,
    "preview": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or with"
  },
  {
    "path": "README.md",
    "chars": 479,
    "preview": "This is a copy of cmd/go/internal/script.\n\nSee <https://pkg.go.dev/rsc.io/script> and <https://pkg.go.dev/rsc.io/script/"
  },
  {
    "path": "cmds.go",
    "chars": 29093,
    "preview": "// Copyright 2022 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
  },
  {
    "path": "cmds_other.go",
    "chars": 256,
    "preview": "// Copyright 2023 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
  },
  {
    "path": "cmds_posix.go",
    "chars": 312,
    "preview": "// Copyright 2023 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
  },
  {
    "path": "conds.go",
    "chars": 4838,
    "preview": "// Copyright 2022 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
  },
  {
    "path": "engine.go",
    "chars": 22771,
    "preview": "// Copyright 2022 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
  },
  {
    "path": "errors.go",
    "chars": 1608,
    "preview": "// Copyright 2022 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
  },
  {
    "path": "go.mod",
    "chars": 66,
    "preview": "module rsc.io/script\n\ngo 1.21\n\nrequire golang.org/x/tools v0.14.0\n"
  },
  {
    "path": "go.sum",
    "chars": 157,
    "preview": "golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=\ngolang.org/x/tools v0.14.0/go.mod h1:uYBEerGO"
  },
  {
    "path": "internal/diff/diff.go",
    "chars": 7698,
    "preview": "// Copyright 2022 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
  },
  {
    "path": "internal/diff/diff_test.go",
    "chars": 1098,
    "preview": "// Copyright 2022 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
  },
  {
    "path": "internal/diff/testdata/allnew.txt",
    "chars": 91,
    "preview": "-- old --\n-- new --\na\nb\nc\n-- diff --\ndiff old new\n--- old\n+++ new\n@@ -0,0 +1,3 @@\n+a\n+b\n+c\n"
  },
  {
    "path": "internal/diff/testdata/allold.txt",
    "chars": 91,
    "preview": "-- old --\na\nb\nc\n-- new --\n-- diff --\ndiff old new\n--- old\n+++ new\n@@ -1,3 +0,0 @@\n-a\n-b\n-c\n"
  },
  {
    "path": "internal/diff/testdata/basic.txt",
    "chars": 262,
    "preview": "Example from Hunt and McIlroy, “An Algorithm for Differential File Comparison.”\nhttps://www.cs.dartmouth.edu/~doug/diff."
  },
  {
    "path": "internal/diff/testdata/dups.txt",
    "chars": 141,
    "preview": "-- old --\na\n\nb\n\nc\n\nd\n\ne\n\nf\n-- new --\na\n\nB\n\nC\n\nd\n\ne\n\nf\n-- diff --\ndiff old new\n--- old\n+++ new\n@@ -1,8 +1,8 @@\n a\n $\n-b\n-"
  },
  {
    "path": "internal/diff/testdata/end.txt",
    "chars": 178,
    "preview": "-- old --\n1\n2\n3\n4\n5\n6\n7\neight\nnine\nten\neleven\n-- new --\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n-- diff --\ndiff old new\n--- old\n+++ new\n@@ "
  },
  {
    "path": "internal/diff/testdata/eof.txt",
    "chars": 47,
    "preview": "-- old --\na\nb\nc^D\n-- new --\na\nb\nc^D\n-- diff --\n"
  },
  {
    "path": "internal/diff/testdata/eof1.txt",
    "chars": 130,
    "preview": "-- old --\na\nb\nc\n-- new --\na\nb\nc^D\n-- diff --\ndiff old new\n--- old\n+++ new\n@@ -1,3 +1,3 @@\n a\n b\n-c\n+c\n\\ No newline at en"
  },
  {
    "path": "internal/diff/testdata/eof2.txt",
    "chars": 130,
    "preview": "-- old --\na\nb\nc^D\n-- new --\na\nb\nc\n-- diff --\ndiff old new\n--- old\n+++ new\n@@ -1,3 +1,3 @@\n a\n b\n-c\n\\ No newline at end o"
  },
  {
    "path": "internal/diff/testdata/long.txt",
    "chars": 251,
    "preview": "-- old --\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n14½\n15\n16\n17\n18\n19\n20\n-- new --\n1\n2\n3\n4\n5\n6\n8\n9\n10\n11\n12\n13\n14\n17\n18\n19\n20\n--"
  },
  {
    "path": "internal/diff/testdata/same.txt",
    "chars": 55,
    "preview": "-- old --\nhello world\n-- new --\nhello world\n-- diff --\n"
  },
  {
    "path": "internal/diff/testdata/start.txt",
    "chars": 142,
    "preview": "-- old --\ne\npi\n4\n5\n6\n7\n8\n9\n10\n-- new --\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n-- diff --\ndiff old new\n--- old\n+++ new\n@@ -1,5 +1,6 @@\n-e\n"
  },
  {
    "path": "internal/diff/testdata/triv.txt",
    "chars": 352,
    "preview": "Another example from Hunt and McIlroy,\n“An Algorithm for Differential File Comparison.”\nhttps://www.cs.dartmouth.edu/~do"
  },
  {
    "path": "scripttest/scripttest.go",
    "chars": 6302,
    "preview": "// Copyright 2022 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
  },
  {
    "path": "scripttest/scripttest_test.go",
    "chars": 538,
    "preview": "// Copyright 2023 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
  },
  {
    "path": "scripttest/testdata/basic.txt",
    "chars": 87,
    "preview": "cat hello.txt\nstdout 'hello world'\n! stderr 'hello world'\n\n-- hello.txt --\nhello world\n"
  },
  {
    "path": "state.go",
    "chars": 7116,
    "preview": "// Copyright 2022 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
  }
]

About this extraction

This page contains the full source code of the rsc/script GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (83.8 KB), approximately 24.8k tokens, and a symbol index with 130 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!