| // 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() |
| } |