| // 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" | |
| "internal/txtar" | |
| "io" | |
| "io/fs" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "regexp" | |
| "strings" | |
| ) | |
| // 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() | |
| } | |