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