api.go

  1// Copyright (c) 2017, Daniel MartΓ­ <mvdan@mvdan.cc>
  2// See LICENSE for licensing information
  3
  4// Package interp implements an interpreter that executes shell
  5// programs. It aims to support POSIX, but its support is not complete
  6// yet. It also supports some Bash features.
  7//
  8// The interpreter generally aims to behave like Bash,
  9// but it does not support all of its features.
 10//
 11// The interpreter currently aims to behave like a non-interactive shell,
 12// which is how most shells run scripts, and is more useful to machines.
 13// In the future, it may gain an option to behave like an interactive shell.
 14package interp
 15
 16import (
 17	"context"
 18	"errors"
 19	"fmt"
 20	"io"
 21	"io/fs"
 22	"maps"
 23	"math/rand"
 24	"os"
 25	"path/filepath"
 26	"slices"
 27	"strconv"
 28	"sync"
 29	"time"
 30
 31	"mvdan.cc/sh/v3/expand"
 32	"mvdan.cc/sh/v3/syntax"
 33)
 34
 35// A Runner interprets shell programs. It can be reused, but it is not safe for
 36// concurrent use. Use [New] to build a new Runner.
 37//
 38// Note that writes to Stdout and Stderr may be concurrent if background
 39// commands are used. If you plan on using an [io.Writer] implementation that
 40// isn't safe for concurrent use, consider a workaround like hiding writes
 41// behind a mutex.
 42//
 43// Runner's exported fields are meant to be configured via [RunnerOption];
 44// once a Runner has been created, the fields should be treated as read-only.
 45type Runner struct {
 46	// Env specifies the initial environment for the interpreter, which must
 47	// not be nil. It can only be set via [Env].
 48	//
 49	// If it includes a TMPDIR variable describing an absolute directory,
 50	// it is used as the directory in which to create temporary files needed
 51	// for the interpreter's use, such as named pipes for process substitutions.
 52	// Otherwise, [os.TempDir] is used.
 53	Env expand.Environ
 54
 55	writeEnv expand.WriteEnviron
 56
 57	// Dir specifies the working directory of the command, which must be an
 58	// absolute path. It can only be set via [Dir].
 59	Dir string
 60
 61	// tempDir is either $TMPDIR from [Runner.Env], or [os.TempDir].
 62	tempDir string
 63
 64	// Params are the current shell parameters, e.g. from running a shell
 65	// file or calling a function. Accessible via the $@/$* family of vars.
 66	// It can only be set via [Params].
 67	Params []string
 68
 69	// Separate maps - note that bash allows a name to be both a var and a
 70	// func simultaneously.
 71	// Vars is mostly superseded by Env at this point.
 72	// TODO(v4): remove these
 73
 74	Vars  map[string]expand.Variable
 75	Funcs map[string]*syntax.Stmt
 76
 77	alias map[string]alias
 78
 79	// callHandler is a function allowing to replace a simple command's
 80	// arguments. It may be nil.
 81	callHandler CallHandlerFunc
 82
 83	// execHandler is responsible for executing programs. It must not be nil.
 84	execHandler ExecHandlerFunc
 85
 86	// execMiddlewares grows with calls to [ExecHandlers],
 87	// and is used to construct execHandler when Reset is first called.
 88	// The slice is needed to preserve the relative order of middlewares.
 89	execMiddlewares []func(ExecHandlerFunc) ExecHandlerFunc
 90
 91	// openHandler is a function responsible for opening files. It must not be nil.
 92	openHandler OpenHandlerFunc
 93
 94	// readDirHandler is a function responsible for reading directories during
 95	// glob expansion. It must be non-nil.
 96	readDirHandler ReadDirHandlerFunc2
 97
 98	// statHandler is a function responsible for getting file stat. It must be non-nil.
 99	statHandler StatHandlerFunc
100
101	stdin  *os.File // e.g. the read end of a pipe
102	stdout io.Writer
103	stderr io.Writer
104
105	ecfg *expand.Config
106	ectx context.Context // just so that Runner.Subshell can use it again
107
108	lastExpandExit int // used to surface exit codes while expanding fields
109
110	// didReset remembers whether the runner has ever been reset. This is
111	// used so that Reset is automatically called when running any program
112	// or node for the first time on a Runner.
113	didReset bool
114
115	usedNew bool
116
117	// rand is used mainly to generate temporary files.
118	rand *rand.Rand
119
120	filename string // only if Node was a File
121
122	// >0 to break or continue out of N enclosing loops
123	breakEnclosing, contnEnclosing int
124
125	inLoop    bool
126	inFunc    bool
127	inSource  bool
128	noErrExit bool
129
130	// track if a sourced script set positional parameters
131	sourceSetParams bool
132
133	err          error // current shell exit code or fatal error
134	handlingTrap bool  // whether we're currently in a trap callback
135	shellExited  bool  // whether the shell needs to exit
136
137	// The current and last exit status code. They can only be different if
138	// the interpreter is in the middle of running a statement. In that
139	// scenario, 'exit' is the status code for the statement being run, and
140	// 'lastExit' corresponds to the previous statement that was run.
141	exit     int
142	lastExit int
143
144	bgShells sync.WaitGroup
145
146	opts runnerOpts
147
148	origDir    string
149	origParams []string
150	origOpts   runnerOpts
151	origStdin  *os.File
152	origStdout io.Writer
153	origStderr io.Writer
154
155	// Most scripts don't use pushd/popd, so make space for the initial PWD
156	// without requiring an extra allocation.
157	dirStack     []string
158	dirBootstrap [1]string
159
160	optState getopts
161
162	// keepRedirs is used so that "exec" can make any redirections
163	// apply to the current shell, and not just the command.
164	keepRedirs bool
165
166	// Fake signal callbacks
167	callbackErr  string
168	callbackExit string
169}
170
171type alias struct {
172	args  []*syntax.Word
173	blank bool
174}
175
176func (r *Runner) optByFlag(flag byte) *bool {
177	for i, opt := range &shellOptsTable {
178		if opt.flag == flag {
179			return &r.opts[i]
180		}
181	}
182	return nil
183}
184
185// New creates a new Runner, applying a number of options. If applying any of
186// the options results in an error, it is returned.
187//
188// Any unset options fall back to their defaults. For example, not supplying the
189// environment falls back to the process's environment, and not supplying the
190// standard output writer means that the output will be discarded.
191func New(opts ...RunnerOption) (*Runner, error) {
192	r := &Runner{
193		usedNew:        true,
194		openHandler:    DefaultOpenHandler(),
195		readDirHandler: DefaultReadDirHandler2(),
196		statHandler:    DefaultStatHandler(),
197	}
198	r.dirStack = r.dirBootstrap[:0]
199	// turn "on" the default Bash options
200	for i, opt := range bashOptsTable {
201		r.opts[len(shellOptsTable)+i] = opt.defaultState
202	}
203
204	for _, opt := range opts {
205		if err := opt(r); err != nil {
206			return nil, err
207		}
208	}
209
210	// Set the default fallbacks, if necessary.
211	if r.Env == nil {
212		Env(nil)(r)
213	}
214	if r.Dir == "" {
215		if err := Dir("")(r); err != nil {
216			return nil, err
217		}
218	}
219	if r.stdout == nil || r.stderr == nil {
220		StdIO(r.stdin, r.stdout, r.stderr)(r)
221	}
222	return r, nil
223}
224
225// RunnerOption can be passed to [New] to alter a [Runner]'s behaviour.
226// It can also be applied directly on an existing Runner,
227// such as interp.Params("-e")(runner).
228// Note that options cannot be applied once Run or Reset have been called.
229type RunnerOption func(*Runner) error
230
231// TODO: enforce the rule above via didReset.
232
233// Env sets the interpreter's environment. If nil, a copy of the current
234// process's environment is used.
235func Env(env expand.Environ) RunnerOption {
236	return func(r *Runner) error {
237		if env == nil {
238			env = expand.ListEnviron(os.Environ()...)
239		}
240		r.Env = env
241		return nil
242	}
243}
244
245// Dir sets the interpreter's working directory. If empty, the process's current
246// directory is used.
247func Dir(path string) RunnerOption {
248	return func(r *Runner) error {
249		if path == "" {
250			path, err := os.Getwd()
251			if err != nil {
252				return fmt.Errorf("could not get current dir: %w", err)
253			}
254			r.Dir = path
255			return nil
256		}
257		path, err := filepath.Abs(path)
258		if err != nil {
259			return fmt.Errorf("could not get absolute dir: %w", err)
260		}
261		info, err := os.Stat(path)
262		if err != nil {
263			return fmt.Errorf("could not stat: %w", err)
264		}
265		if !info.IsDir() {
266			return fmt.Errorf("%s is not a directory", path)
267		}
268		r.Dir = path
269		return nil
270	}
271}
272
273// Interactive configures the interpreter to behave like an interactive shell,
274// akin to Bash. Currently, this only enables the expansion of aliases,
275// but later on it should also change other behavior.
276func Interactive(enabled bool) RunnerOption {
277	return func(r *Runner) error {
278		r.opts[optExpandAliases] = enabled
279		return nil
280	}
281}
282
283// Params populates the shell options and parameters. For example, Params("-e",
284// "--", "foo") will set the "-e" option and the parameters ["foo"], and
285// Params("+e") will unset the "-e" option and leave the parameters untouched.
286//
287// This is similar to what the interpreter's "set" builtin does.
288func Params(args ...string) RunnerOption {
289	return func(r *Runner) error {
290		fp := flagParser{remaining: args}
291		for fp.more() {
292			flag := fp.flag()
293			if flag == "-" {
294				// TODO: implement "The -x and -v options are turned off."
295				if args := fp.args(); len(args) > 0 {
296					r.Params = args
297				}
298				return nil
299			}
300			enable := flag[0] == '-'
301			if flag[1] != 'o' {
302				opt := r.optByFlag(flag[1])
303				if opt == nil {
304					return fmt.Errorf("invalid option: %q", flag)
305				}
306				*opt = enable
307				continue
308			}
309			value := fp.value()
310			if value == "" && enable {
311				for i, opt := range &shellOptsTable {
312					r.printOptLine(opt.name, r.opts[i], true)
313				}
314				continue
315			}
316			if value == "" && !enable {
317				for i, opt := range &shellOptsTable {
318					setFlag := "+o"
319					if r.opts[i] {
320						setFlag = "-o"
321					}
322					r.outf("set %s %s\n", setFlag, opt.name)
323				}
324				continue
325			}
326			_, opt := r.optByName(value, false)
327			if opt == nil {
328				return fmt.Errorf("invalid option: %q", value)
329			}
330			*opt = enable
331		}
332		if args := fp.args(); args != nil {
333			// If "--" wasn't given and there were zero arguments,
334			// we don't want to override the current parameters.
335			r.Params = args
336
337			// Record whether a sourced script sets the parameters.
338			if r.inSource {
339				r.sourceSetParams = true
340			}
341		}
342		return nil
343	}
344}
345
346// CallHandler sets the call handler. See [CallHandlerFunc] for more info.
347func CallHandler(f CallHandlerFunc) RunnerOption {
348	return func(r *Runner) error {
349		r.callHandler = f
350		return nil
351	}
352}
353
354// ExecHandler sets one command execution handler,
355// which replaces DefaultExecHandler(2 * time.Second).
356//
357// Deprecated: use [ExecHandlers] instead, which allows for middleware handlers.
358func ExecHandler(f ExecHandlerFunc) RunnerOption {
359	return func(r *Runner) error {
360		r.execHandler = f
361		return nil
362	}
363}
364
365// ExecHandlers appends middlewares to handle command execution.
366// The middlewares are chained from first to last, and the first is called by the runner.
367// Each middleware is expected to call the "next" middleware at most once.
368//
369// For example, a middleware may implement only some commands.
370// For those commands, it can run its logic and avoid calling "next".
371// For any other commands, it can call "next" with the original parameters.
372//
373// Another common example is a middleware which always calls "next",
374// but runs custom logic either before or after that call.
375// For instance, a middleware could change the arguments to the "next" call,
376// or it could print log lines before or after the call to "next".
377//
378// The last exec handler is DefaultExecHandler(2 * time.Second).
379func ExecHandlers(middlewares ...func(next ExecHandlerFunc) ExecHandlerFunc) RunnerOption {
380	return func(r *Runner) error {
381		r.execMiddlewares = append(r.execMiddlewares, middlewares...)
382		return nil
383	}
384}
385
386// TODO: consider porting the middleware API in [ExecHandlers] to [OpenHandler],
387// ReadDirHandler, and StatHandler.
388
389// TODO(v4): now that [ExecHandlers] allows calling a next handler with changed
390// arguments, one of the two advantages of [CallHandler] is gone. The other is the
391// ability to work with builtins; if we make [ExecHandlers] work with builtins, we
392// could join both APIs.
393
394// OpenHandler sets file open handler. See [OpenHandlerFunc] for more info.
395func OpenHandler(f OpenHandlerFunc) RunnerOption {
396	return func(r *Runner) error {
397		r.openHandler = f
398		return nil
399	}
400}
401
402// ReadDirHandler sets the read directory handler. See [ReadDirHandlerFunc] for more info.
403//
404// Deprecated: use [ReadDirHandler2].
405func ReadDirHandler(f ReadDirHandlerFunc) RunnerOption {
406	return func(r *Runner) error {
407		r.readDirHandler = func(ctx context.Context, path string) ([]fs.DirEntry, error) {
408			infos, err := f(ctx, path)
409			if err != nil {
410				return nil, err
411			}
412			entries := make([]fs.DirEntry, len(infos))
413			for i, info := range infos {
414				entries[i] = fs.FileInfoToDirEntry(info)
415			}
416			return entries, nil
417		}
418		return nil
419	}
420}
421
422// ReadDirHandler2 sets the read directory handler. See [ReadDirHandlerFunc2] for more info.
423func ReadDirHandler2(f ReadDirHandlerFunc2) RunnerOption {
424	return func(r *Runner) error {
425		r.readDirHandler = f
426		return nil
427	}
428}
429
430// StatHandler sets the stat handler. See [StatHandlerFunc] for more info.
431func StatHandler(f StatHandlerFunc) RunnerOption {
432	return func(r *Runner) error {
433		r.statHandler = f
434		return nil
435	}
436}
437
438func stdinFile(r io.Reader) (*os.File, error) {
439	switch r := r.(type) {
440	case *os.File:
441		return r, nil
442	case nil:
443		return nil, nil
444	default:
445		pr, pw, err := os.Pipe()
446		if err != nil {
447			return nil, err
448		}
449		go func() {
450			io.Copy(pw, r)
451			pw.Close()
452		}()
453		return pr, nil
454	}
455}
456
457// StdIO configures an interpreter's standard input, standard output, and
458// standard error. If out or err are nil, they default to a writer that discards
459// the output.
460//
461// Note that providing a non-nil standard input other than [*os.File] will require
462// an [os.Pipe] and spawning a goroutine to copy into it,
463// as an [os.File] is the only way to share a reader with subprocesses.
464// This may cause the interpreter to consume the entire reader.
465// See [os/exec.Cmd.Stdin].
466//
467// When providing an [*os.File] as standard input, consider using an [os.Pipe]
468// as it has the best chance to support cancellable reads via [os.File.SetReadDeadline],
469// so that cancelling the runner's context can stop a blocked standard input read.
470func StdIO(in io.Reader, out, err io.Writer) RunnerOption {
471	return func(r *Runner) error {
472		stdin, _err := stdinFile(in)
473		if _err != nil {
474			return _err
475		}
476		r.stdin = stdin
477		if out == nil {
478			out = io.Discard
479		}
480		r.stdout = out
481		if err == nil {
482			err = io.Discard
483		}
484		r.stderr = err
485		return nil
486	}
487}
488
489// optByName returns the matching runner's option index and status
490func (r *Runner) optByName(name string, bash bool) (index int, status *bool) {
491	if bash {
492		for i, opt := range bashOptsTable {
493			if opt.name == name {
494				index = len(shellOptsTable) + i
495				return index, &r.opts[index]
496			}
497		}
498	}
499	for i, opt := range &shellOptsTable {
500		if opt.name == name {
501			return i, &r.opts[i]
502		}
503	}
504	return 0, nil
505}
506
507type runnerOpts [len(shellOptsTable) + len(bashOptsTable)]bool
508
509type shellOpt struct {
510	flag byte
511	name string
512}
513
514type bashOpt struct {
515	name         string
516	defaultState bool // Bash's default value for this option
517	supported    bool // whether we support the option's non-default state
518}
519
520var shellOptsTable = [...]shellOpt{
521	// sorted alphabetically by name; use a space for the options
522	// that have no flag form
523	{'a', "allexport"},
524	{'e', "errexit"},
525	{'n', "noexec"},
526	{'f', "noglob"},
527	{'u', "nounset"},
528	{'x', "xtrace"},
529	{' ', "pipefail"},
530}
531
532var bashOptsTable = [...]bashOpt{
533	// supported options, sorted alphabetically by name
534	{
535		name:         "expand_aliases",
536		defaultState: false,
537		supported:    true,
538	},
539	{
540		name:         "globstar",
541		defaultState: false,
542		supported:    true,
543	},
544	{
545		name:         "nocaseglob",
546		defaultState: false,
547		supported:    true,
548	},
549	{
550		name:         "nullglob",
551		defaultState: false,
552		supported:    true,
553	},
554	// unsupported options, sorted alphabetically by name
555	{name: "assoc_expand_once"},
556	{name: "autocd"},
557	{name: "cdable_vars"},
558	{name: "cdspell"},
559	{name: "checkhash"},
560	{name: "checkjobs"},
561	{
562		name:         "checkwinsize",
563		defaultState: true,
564	},
565	{
566		name:         "cmdhist",
567		defaultState: true,
568	},
569	{name: "compat31"},
570	{name: "compat32"},
571	{name: "compat40"},
572	{name: "compat41"},
573	{name: "compat42"},
574	{name: "compat44"},
575	{name: "compat43"},
576	{name: "compat44"},
577	{
578		name:         "complete_fullquote",
579		defaultState: true,
580	},
581	{name: "direxpand"},
582	{name: "dirspell"},
583	{name: "dotglob"},
584	{name: "execfail"},
585	{name: "extdebug"},
586	{name: "extglob"},
587	{
588		name:         "extquote",
589		defaultState: true,
590	},
591	{name: "failglob"},
592	{
593		name:         "force_fignore",
594		defaultState: true,
595	},
596	{name: "globasciiranges"},
597	{name: "gnu_errfmt"},
598	{name: "histappend"},
599	{name: "histreedit"},
600	{name: "histverify"},
601	{
602		name:         "hostcomplete",
603		defaultState: true,
604	},
605	{name: "huponexit"},
606	{
607		name:         "inherit_errexit",
608		defaultState: true,
609	},
610	{
611		name:         "interactive_comments",
612		defaultState: true,
613	},
614	{name: "lastpipe"},
615	{name: "lithist"},
616	{name: "localvar_inherit"},
617	{name: "localvar_unset"},
618	{name: "login_shell"},
619	{name: "mailwarn"},
620	{name: "no_empty_cmd_completion"},
621	{name: "nocasematch"},
622	{
623		name:         "progcomp",
624		defaultState: true,
625	},
626	{name: "progcomp_alias"},
627	{
628		name:         "promptvars",
629		defaultState: true,
630	},
631	{name: "restricted_shell"},
632	{name: "shift_verbose"},
633	{
634		name:         "sourcepath",
635		defaultState: true,
636	},
637	{name: "xpg_echo"},
638}
639
640// To access the shell options arrays without a linear search when we
641// know which option we're after at compile time. First come the shell options,
642// then the bash options.
643const (
644	// These correspond to indexes in [shellOptsTable]
645	optAllExport = iota
646	optErrExit
647	optNoExec
648	optNoGlob
649	optNoUnset
650	optXTrace
651	optPipeFail
652
653	// These correspond to indexes (offset by the above seven items) of
654	// supported options in [bashOptsTable]
655	optExpandAliases
656	optGlobStar
657	optNoCaseGlob
658	optNullGlob
659)
660
661// Reset returns a runner to its initial state, right before the first call to
662// Run or Reset.
663//
664// Typically, this function only needs to be called if a runner is reused to run
665// multiple programs non-incrementally. Not calling Reset between each run will
666// mean that the shell state will be kept, including variables, options, and the
667// current directory.
668func (r *Runner) Reset() {
669	if !r.usedNew {
670		panic("use interp.New to construct a Runner")
671	}
672	if !r.didReset {
673		r.origDir = r.Dir
674		r.origParams = r.Params
675		r.origOpts = r.opts
676		r.origStdin = r.stdin
677		r.origStdout = r.stdout
678		r.origStderr = r.stderr
679
680		if r.execHandler != nil && len(r.execMiddlewares) > 0 {
681			panic("interp.ExecHandler should be replaced with interp.ExecHandlers, not mixed")
682		}
683		if r.execHandler == nil {
684			r.execHandler = DefaultExecHandler(2 * time.Second)
685		}
686		// Middlewares are chained from first to last, and each can call the
687		// next in the chain, so we need to construct the chain backwards.
688		for _, mw := range slices.Backward(r.execMiddlewares) {
689			r.execHandler = mw(r.execHandler)
690		}
691		// Fill tempDir; only need to do this once given that Env will not change.
692		if dir := r.Env.Get("TMPDIR").String(); filepath.IsAbs(dir) {
693			r.tempDir = dir
694		} else {
695			r.tempDir = os.TempDir()
696		}
697		// Clean it as we will later do a string prefix match.
698		r.tempDir = filepath.Clean(r.tempDir)
699	}
700	// reset the internal state
701	*r = Runner{
702		Env:            r.Env,
703		tempDir:        r.tempDir,
704		callHandler:    r.callHandler,
705		execHandler:    r.execHandler,
706		openHandler:    r.openHandler,
707		readDirHandler: r.readDirHandler,
708		statHandler:    r.statHandler,
709
710		// These can be set by functions like [Dir] or [Params], but
711		// builtins can overwrite them; reset the fields to whatever the
712		// constructor set up.
713		Dir:    r.origDir,
714		Params: r.origParams,
715		opts:   r.origOpts,
716		stdin:  r.origStdin,
717		stdout: r.origStdout,
718		stderr: r.origStderr,
719
720		origDir:    r.origDir,
721		origParams: r.origParams,
722		origOpts:   r.origOpts,
723		origStdin:  r.origStdin,
724		origStdout: r.origStdout,
725		origStderr: r.origStderr,
726
727		// emptied below, to reuse the space
728		Vars:     r.Vars,
729		dirStack: r.dirStack[:0],
730		usedNew:  r.usedNew,
731	}
732	if r.Vars == nil {
733		r.Vars = make(map[string]expand.Variable)
734	} else {
735		clear(r.Vars)
736	}
737	// TODO(v4): Use the supplied Env directly if it implements enough methods.
738	r.writeEnv = &overlayEnviron{parent: r.Env}
739	if !r.writeEnv.Get("HOME").IsSet() {
740		home, _ := os.UserHomeDir()
741		r.setVarString("HOME", home)
742	}
743	if !r.writeEnv.Get("UID").IsSet() {
744		r.setVar("UID", expand.Variable{
745			Set:      true,
746			Kind:     expand.String,
747			ReadOnly: true,
748			Str:      strconv.Itoa(os.Getuid()),
749		})
750	}
751	if !r.writeEnv.Get("EUID").IsSet() {
752		r.setVar("EUID", expand.Variable{
753			Set:      true,
754			Kind:     expand.String,
755			ReadOnly: true,
756			Str:      strconv.Itoa(os.Geteuid()),
757		})
758	}
759	if !r.writeEnv.Get("GID").IsSet() {
760		r.setVar("GID", expand.Variable{
761			Set:      true,
762			Kind:     expand.String,
763			ReadOnly: true,
764			Str:      strconv.Itoa(os.Getgid()),
765		})
766	}
767	r.setVarString("PWD", r.Dir)
768	r.setVarString("IFS", " \t\n")
769	r.setVarString("OPTIND", "1")
770
771	r.dirStack = append(r.dirStack, r.Dir)
772
773	r.didReset = true
774}
775
776// exitStatus is a non-zero status code resulting from running a shell node.
777type exitStatus uint8
778
779func (s exitStatus) Error() string { return fmt.Sprintf("exit status %d", s) }
780
781// NewExitStatus creates an error which contains the specified exit status code.
782func NewExitStatus(status uint8) error {
783	return exitStatus(status)
784}
785
786// IsExitStatus checks whether error contains an exit status and returns it.
787func IsExitStatus(err error) (status uint8, ok bool) {
788	var s exitStatus
789	if errors.As(err, &s) {
790		return uint8(s), true
791	}
792	return 0, false
793}
794
795// Run interprets a node, which can be a [*File], [*Stmt], or [Command]. If a non-nil
796// error is returned, it will typically contain a command's exit status, which
797// can be retrieved with [IsExitStatus].
798//
799// Run can be called multiple times synchronously to interpret programs
800// incrementally. To reuse a [Runner] without keeping the internal shell state,
801// call Reset.
802//
803// Calling Run on an entire [*File] implies an exit, meaning that an exit trap may
804// run.
805func (r *Runner) Run(ctx context.Context, node syntax.Node) error {
806	if !r.didReset {
807		r.Reset()
808	}
809	r.fillExpandConfig(ctx)
810	r.err = nil
811	r.shellExited = false
812	r.filename = ""
813	switch node := node.(type) {
814	case *syntax.File:
815		r.filename = node.Name
816		r.stmts(ctx, node.Stmts)
817		if !r.shellExited {
818			r.exitShell(ctx, r.exit)
819		}
820	case *syntax.Stmt:
821		r.stmt(ctx, node)
822	case syntax.Command:
823		r.cmd(ctx, node)
824	default:
825		return fmt.Errorf("node can only be File, Stmt, or Command: %T", node)
826	}
827	if r.exit != 0 {
828		r.setErr(NewExitStatus(uint8(r.exit)))
829	}
830	if r.Vars != nil {
831		maps.Insert(r.Vars, r.writeEnv.Each)
832	}
833	return r.err
834}
835
836// Exited reports whether the last Run call should exit an entire shell. This
837// can be triggered by the "exit" built-in command, for example.
838//
839// Note that this state is overwritten at every Run call, so it should be
840// checked immediately after each Run call.
841func (r *Runner) Exited() bool {
842	return r.shellExited
843}
844
845// Subshell makes a copy of the given [Runner], suitable for use concurrently
846// with the original. The copy will have the same environment, including
847// variables and functions, but they can all be modified without affecting the
848// original.
849//
850// Subshell is not safe to use concurrently with [Run]. Orchestrating this is
851// left up to the caller; no locking is performed.
852//
853// To replace e.g. stdin/out/err, do StdIO(r.stdin, r.stdout, r.stderr)(r) on
854// the copy.
855func (r *Runner) Subshell() *Runner {
856	if !r.didReset {
857		r.Reset()
858	}
859	// Keep in sync with the Runner type. Manually copy fields, to not copy
860	// sensitive ones like [errgroup.Group], and to do deep copies of slices.
861	r2 := &Runner{
862		Dir:            r.Dir,
863		tempDir:        r.tempDir,
864		Params:         r.Params,
865		callHandler:    r.callHandler,
866		execHandler:    r.execHandler,
867		openHandler:    r.openHandler,
868		readDirHandler: r.readDirHandler,
869		statHandler:    r.statHandler,
870		stdin:          r.stdin,
871		stdout:         r.stdout,
872		stderr:         r.stderr,
873		filename:       r.filename,
874		opts:           r.opts,
875		usedNew:        r.usedNew,
876		exit:           r.exit,
877		lastExit:       r.lastExit,
878
879		origStdout: r.origStdout, // used for process substitutions
880	}
881	// Funcs are copied, since they might be modified.
882	// Env vars aren't copied; setVar will copy lists and maps as needed.
883	oenv := &overlayEnviron{parent: r.writeEnv}
884	r2.writeEnv = oenv
885	r2.Funcs = maps.Clone(r.Funcs)
886	r2.Vars = make(map[string]expand.Variable)
887	r2.alias = maps.Clone(r.alias)
888
889	r2.dirStack = append(r2.dirBootstrap[:0], r.dirStack...)
890	r2.fillExpandConfig(r.ectx)
891	r2.didReset = true
892	return r2
893}