Skip to content

Commit 33916e1

Browse files
committed
interp: add stmt, subshell, builtin, func-call, and var-lookup handlers
ExecHandlers already lets external code wrap external command execution. Apply the same middleware-chain pattern to five more dispatch points so the interpreter can be extended from outside without patching its internals. StmtHandlers fires around every syntax.Stmt before dispatch. SubshellHandlers fires around each subshell creation site, with a SubshellKind to distinguish background, paren, cmd subst, proc subst, and pipeline. BuiltinHandler registers a name-keyed override for a builtin; BuiltinHandlers wraps all builtin dispatch with middleware. FuncCallHandlers wraps the body of a declared shell function. LookupVarHandlers wraps lookupVar on top of the runner's own special-variable and environment resolution, so middlewares can add or override variables without losing the existing behaviour. Each chain follows ExecHandlers' shape: middlewares are chained first-to-last, the bottom of the chain is the runner's own behaviour, and a middleware may short-circuit by not calling next. Chain bottoms recover the active runner from ctx, so the chains are built once in Reset and inherited by subshell copies the same way execHandler is. Also add RunStmts, a top-level helper for dispatching stmts on the runner attached to ctx, useful for handlers that need to execute shell code in the runner's current scope (e.g. a trap body from a Go-side signal handler).
1 parent bb64dc5 commit 33916e1

6 files changed

Lines changed: 704 additions & 67 deletions

File tree

interp/api.go

Lines changed: 195 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,29 @@ type Runner struct {
8585
// The slice is needed to preserve the relative order of middlewares.
8686
execMiddlewares []func(ExecHandlerFunc) ExecHandlerFunc
8787

88+
// stmtHandler is built from stmtMiddlewares when Reset is first called.
89+
stmtHandler StmtHandlerFunc
90+
stmtMiddlewares []func(StmtHandlerFunc) StmtHandlerFunc
91+
92+
// subshellHandler is built from subshellMiddlewares when Reset is first called.
93+
subshellHandler SubshellHandlerFunc
94+
subshellMiddlewares []func(SubshellHandlerFunc) SubshellHandlerFunc
95+
96+
// builtinOverrides is the registry populated by [BuiltinHandler]. It may be nil.
97+
builtinOverrides map[string]BuiltinHandlerFunc
98+
99+
// builtinHandler is built from builtinMiddlewares when Reset is first called.
100+
builtinHandler BuiltinHandlerFunc
101+
builtinMiddlewares []func(BuiltinHandlerFunc) BuiltinHandlerFunc
102+
103+
// lookupVarHandler is built from lookupVarMiddlewares when Reset is first called.
104+
lookupVarHandler LookupVarHandlerFunc
105+
lookupVarMiddlewares []func(LookupVarHandlerFunc) LookupVarHandlerFunc
106+
107+
// funcCallHandler is built from funcCallMiddlewares when Reset is first called.
108+
funcCallHandler FuncCallHandlerFunc
109+
funcCallMiddlewares []func(FuncCallHandlerFunc) FuncCallHandlerFunc
110+
88111
// openHandler is a function responsible for opening files. It must not be nil.
89112
openHandler OpenHandlerFunc
90113

@@ -168,6 +191,48 @@ type Runner struct {
168191
callbackExit string
169192
}
170193

194+
// buildHandlerChains constructs the handler middleware chains.
195+
// Each chain bottom recovers its runner from ctx, so subshells inherit them.
196+
func (r *Runner) buildHandlerChains() {
197+
r.stmtHandler = func(ctx context.Context, stmt *syntax.Stmt) {
198+
runnerFromCtx(ctx).dispatchStmt(ctx, stmt)
199+
}
200+
for _, mw := range slices.Backward(r.stmtMiddlewares) {
201+
r.stmtHandler = mw(r.stmtHandler)
202+
}
203+
r.subshellHandler = func(ctx context.Context, kind SubshellKind, run func(ctx context.Context) int) int {
204+
return run(ctx)
205+
}
206+
for _, mw := range slices.Backward(r.subshellMiddlewares) {
207+
r.subshellHandler = mw(r.subshellHandler)
208+
}
209+
r.builtinHandler = func(ctx context.Context, name string, args []string) ExitStatus {
210+
r := HandlerCtx(ctx).runner
211+
if fn, ok := r.builtinOverrides[name]; ok {
212+
code := fn(ctx, name, args)
213+
r.exit.code = uint8(code)
214+
return code
215+
}
216+
r.exit = r.internalBuiltin(ctx, name, args)
217+
return ExitStatus(r.exit.code)
218+
}
219+
for _, mw := range slices.Backward(r.builtinMiddlewares) {
220+
r.builtinHandler = mw(r.builtinHandler)
221+
}
222+
r.funcCallHandler = func(ctx context.Context, name string, args []string) {
223+
runnerFromCtx(ctx).dispatchFuncCall(ctx, name, args)
224+
}
225+
for _, mw := range slices.Backward(r.funcCallMiddlewares) {
226+
r.funcCallHandler = mw(r.funcCallHandler)
227+
}
228+
r.lookupVarHandler = func(ctx context.Context, name string) expand.Variable {
229+
return runnerFromCtx(ctx).defaultLookupVar(ctx, name)
230+
}
231+
for _, mw := range slices.Backward(r.lookupVarMiddlewares) {
232+
r.lookupVarHandler = mw(r.lookupVarHandler)
233+
}
234+
}
235+
171236
// exitStatus holds the state of the shell after running one command.
172237
// Beyond the exit status code, it also holds whether the shell should return or exit,
173238
// as well as any Go error values that should be given back to the user.
@@ -508,6 +573,83 @@ func StatHandler(f StatHandlerFunc) RunnerOption {
508573
}
509574
}
510575

576+
// StmtHandlers appends middlewares to handle statement dispatch.
577+
// See [ExecHandlers] for details on the middleware chaining.
578+
//
579+
// Unlike [ExecHandlers], this fires for every statement,
580+
// including builtins, function calls, and nested subshells.
581+
func StmtHandlers(middlewares ...func(next StmtHandlerFunc) StmtHandlerFunc) RunnerOption {
582+
return func(r *Runner) error {
583+
r.stmtMiddlewares = append(r.stmtMiddlewares, middlewares...)
584+
return nil
585+
}
586+
}
587+
588+
// SubshellHandlers appends middlewares to handle subshell creation.
589+
// See [ExecHandlers] for details on the middleware chaining.
590+
func SubshellHandlers(middlewares ...func(next SubshellHandlerFunc) SubshellHandlerFunc) RunnerOption {
591+
return func(r *Runner) error {
592+
r.subshellMiddlewares = append(r.subshellMiddlewares, middlewares...)
593+
return nil
594+
}
595+
}
596+
597+
// BuiltinHandler registers fn as the override for the named builtin.
598+
// See [BuiltinHandlerFunc] for more info.
599+
//
600+
// A nil fn removes any previous registration.
601+
// The override applies even to names that are not real builtins,
602+
// so it can also be used to add custom commands.
603+
//
604+
// Overriding "set", "unset", "cd", or "read" can disrupt the runner's internal state.
605+
// For finer-grained control, see [BuiltinHandlers].
606+
func BuiltinHandler(name string, fn BuiltinHandlerFunc) RunnerOption {
607+
return func(r *Runner) error {
608+
if fn == nil {
609+
delete(r.builtinOverrides, name)
610+
return nil
611+
}
612+
if r.builtinOverrides == nil {
613+
r.builtinOverrides = make(map[string]BuiltinHandlerFunc)
614+
}
615+
r.builtinOverrides[name] = fn
616+
return nil
617+
}
618+
}
619+
620+
// BuiltinHandlers appends middlewares around builtin execution.
621+
// See [ExecHandlers] for details on the middleware chaining.
622+
//
623+
// The bottom of the chain dispatches to a [BuiltinHandler] override
624+
// or to the runner's internal builtin implementation.
625+
func BuiltinHandlers(middlewares ...func(next BuiltinHandlerFunc) BuiltinHandlerFunc) RunnerOption {
626+
return func(r *Runner) error {
627+
r.builtinMiddlewares = append(r.builtinMiddlewares, middlewares...)
628+
return nil
629+
}
630+
}
631+
632+
// LookupVarHandlers appends middlewares around variable lookup.
633+
// See [ExecHandlers] for details on the middleware chaining.
634+
//
635+
// The bottom of the chain is the runner's own resolution,
636+
// so middlewares can wrap rather than replace it.
637+
func LookupVarHandlers(middlewares ...func(next LookupVarHandlerFunc) LookupVarHandlerFunc) RunnerOption {
638+
return func(r *Runner) error {
639+
r.lookupVarMiddlewares = append(r.lookupVarMiddlewares, middlewares...)
640+
return nil
641+
}
642+
}
643+
644+
// FuncCallHandlers appends middlewares around the execution of declared shell functions.
645+
// See [ExecHandlers] for details on the middleware chaining.
646+
func FuncCallHandlers(middlewares ...func(next FuncCallHandlerFunc) FuncCallHandlerFunc) RunnerOption {
647+
return func(r *Runner) error {
648+
r.funcCallMiddlewares = append(r.funcCallMiddlewares, middlewares...)
649+
return nil
650+
}
651+
}
652+
511653
func stdinFile(r io.Reader) (*os.File, error) {
512654
switch r := r.(type) {
513655
case *os.File:
@@ -781,6 +923,7 @@ func (r *Runner) Reset() {
781923
for _, mw := range slices.Backward(r.execMiddlewares) {
782924
r.execHandler = mw(r.execHandler)
783925
}
926+
r.buildHandlerChains()
784927
// Fill tempDir; only need to do this once given that Env will not change.
785928
if dir := r.Env.Get("TMPDIR").String(); filepath.IsAbs(dir) {
786929
r.tempDir = dir
@@ -792,13 +935,14 @@ func (r *Runner) Reset() {
792935
}
793936
// reset the internal state
794937
*r = Runner{
795-
Env: r.Env,
796-
tempDir: r.tempDir,
797-
callHandler: r.callHandler,
798-
execHandler: r.execHandler,
799-
openHandler: r.openHandler,
800-
readDirHandler: r.readDirHandler,
801-
statHandler: r.statHandler,
938+
Env: r.Env,
939+
tempDir: r.tempDir,
940+
callHandler: r.callHandler,
941+
execHandler: r.execHandler,
942+
builtinOverrides: r.builtinOverrides,
943+
openHandler: r.openHandler,
944+
readDirHandler: r.readDirHandler,
945+
statHandler: r.statHandler,
802946

803947
// These can be set by functions like [Dir] or [Params], but
804948
// builtins can overwrite them; reset the fields to whatever the
@@ -822,6 +966,19 @@ func (r *Runner) Reset() {
822966

823967
dirStack: r.dirStack[:0],
824968
usedNew: r.usedNew,
969+
970+
stmtMiddlewares: r.stmtMiddlewares,
971+
subshellMiddlewares: r.subshellMiddlewares,
972+
builtinMiddlewares: r.builtinMiddlewares,
973+
funcCallMiddlewares: r.funcCallMiddlewares,
974+
lookupVarMiddlewares: r.lookupVarMiddlewares,
975+
976+
// Built once in the first-time setup above; see [Runner.buildHandlerChains].
977+
stmtHandler: r.stmtHandler,
978+
subshellHandler: r.subshellHandler,
979+
builtinHandler: r.builtinHandler,
980+
funcCallHandler: r.funcCallHandler,
981+
lookupVarHandler: r.lookupVarHandler,
825982
}
826983
// Ensure we stop referencing any pointers before we reuse bgProcs.
827984
clear(r.bgProcs)
@@ -977,22 +1134,37 @@ func (r *Runner) subshell(background bool) *Runner {
9771134
// Keep in sync with the Runner type. Manually copy fields, to not copy
9781135
// sensitive ones like [errgroup.Group], and to do deep copies of slices.
9791136
r2 := &Runner{
980-
Dir: r.Dir,
981-
tempDir: r.tempDir,
982-
Params: r.Params,
983-
callHandler: r.callHandler,
984-
execHandler: r.execHandler,
985-
openHandler: r.openHandler,
986-
readDirHandler: r.readDirHandler,
987-
statHandler: r.statHandler,
988-
stdin: r.stdin,
989-
stdout: r.stdout,
990-
stderr: r.stderr,
991-
filename: r.filename,
992-
opts: r.opts,
993-
usedNew: r.usedNew,
994-
exit: r.exit,
995-
lastExit: r.lastExit,
1137+
Dir: r.Dir,
1138+
tempDir: r.tempDir,
1139+
Params: r.Params,
1140+
callHandler: r.callHandler,
1141+
execHandler: r.execHandler,
1142+
builtinOverrides: r.builtinOverrides,
1143+
openHandler: r.openHandler,
1144+
readDirHandler: r.readDirHandler,
1145+
statHandler: r.statHandler,
1146+
1147+
stmtMiddlewares: r.stmtMiddlewares,
1148+
subshellMiddlewares: r.subshellMiddlewares,
1149+
builtinMiddlewares: r.builtinMiddlewares,
1150+
funcCallMiddlewares: r.funcCallMiddlewares,
1151+
lookupVarMiddlewares: r.lookupVarMiddlewares,
1152+
1153+
// Inherited from the parent; see [Runner.buildHandlerChains].
1154+
stmtHandler: r.stmtHandler,
1155+
subshellHandler: r.subshellHandler,
1156+
builtinHandler: r.builtinHandler,
1157+
funcCallHandler: r.funcCallHandler,
1158+
lookupVarHandler: r.lookupVarHandler,
1159+
1160+
stdin: r.stdin,
1161+
stdout: r.stdout,
1162+
stderr: r.stderr,
1163+
filename: r.filename,
1164+
opts: r.opts,
1165+
usedNew: r.usedNew,
1166+
exit: r.exit,
1167+
lastExit: r.lastExit,
9961168

9971169
origStdout: r.origStdout, // used for process substitutions
9981170
}

interp/builtin.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,20 @@ func (hc HandlerContext) Builtin(ctx context.Context, args []string) error {
145145
return nil
146146
}
147147

148-
func (r *Runner) builtin(ctx context.Context, pos syntax.Pos, name string, args []string) (exit exitStatus) {
148+
func (r *Runner) builtin(ctx context.Context, pos syntax.Pos, name string, args []string) exitStatus {
149+
hctx := r.handlerCtx(ctx, handlerKindBuiltin, pos)
150+
r.exit = exitStatus{}
151+
code := r.builtinHandler(hctx, name, args)
152+
// A middleware may short-circuit without writing r.exit;
153+
// fall back to the returned exit code.
154+
if r.exit == (exitStatus{}) {
155+
r.exit.code = uint8(code)
156+
}
157+
return r.exit
158+
}
159+
160+
func (r *Runner) internalBuiltin(ctx context.Context, name string, args []string) (exit exitStatus) {
161+
pos := HandlerCtx(ctx).Pos
149162
failf := func(code uint8, format string, args ...any) exitStatus {
150163
r.errf(format, args...)
151164
exit.code = code

interp/handler.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package interp
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910
"io"
1011
"io/fs"
@@ -41,6 +42,7 @@ const (
4142
handlerKindCall // [CallHandlerFunc]
4243
handlerKindOpen // [OpenHandlerFunc]
4344
handlerKindReadDir // [ReadDirHandlerFunc2]
45+
handlerKindBuiltin // [BuiltinHandlerFunc]
4446
)
4547

4648
// HandlerContext is the data passed to all the handler functions via [context.WithValue].
@@ -390,3 +392,91 @@ func DefaultStatHandler() StatHandlerFunc {
390392
}
391393
}
392394
}
395+
396+
// StmtHandlerFunc is a handler which runs on every [syntax.Stmt]
397+
// before it is dispatched by the runner.
398+
type StmtHandlerFunc func(ctx context.Context, stmt *syntax.Stmt)
399+
400+
// SubshellKind identifies the kind of subshell creation site.
401+
// It is passed to [SubshellHandlerFunc] so handlers can distinguish them.
402+
type SubshellKind int
403+
404+
const (
405+
_ SubshellKind = iota
406+
SubshellKindBackground // `cmd &` and `cmd & disown`
407+
SubshellKindParen // `( ... )`
408+
SubshellKindCmdSubst // `$(...)` and the legacy backtick form
409+
SubshellKindProcSubst // `<(...)` and `>(...)`
410+
SubshellKindPipeline // a non-final pipeline element, e.g. the `a` in `a | b`
411+
)
412+
413+
func (k SubshellKind) String() string {
414+
switch k {
415+
case SubshellKindBackground:
416+
return "background"
417+
case SubshellKindParen:
418+
return "paren"
419+
case SubshellKindCmdSubst:
420+
return "cmd-subst"
421+
case SubshellKindProcSubst:
422+
return "proc-subst"
423+
case SubshellKindPipeline:
424+
return "pipeline"
425+
}
426+
return "unknown"
427+
}
428+
429+
// SubshellHandlerFunc is a handler which runs around the body of a subshell.
430+
// It is expected to call run to execute the body, and to return its exit code.
431+
//
432+
// The ctx passed into run can differ from the ctx received by the handler,
433+
// so a middleware can derive a child context with its own cancellation.
434+
//
435+
// For [SubshellKindBackground], run returns 0 immediately after launching the
436+
// goroutine; the body's eventual exit code is not available through this hook.
437+
type SubshellHandlerFunc func(ctx context.Context, kind SubshellKind, run func(ctx context.Context) int) int
438+
439+
// BuiltinHandlerFunc is a handler which replaces a named builtin.
440+
// The context includes a [HandlerContext] value.
441+
//
442+
// The returned [ExitStatus] is recorded as the runner's exit status.
443+
type BuiltinHandlerFunc func(ctx context.Context, name string, args []string) ExitStatus
444+
445+
// LookupVarHandlerFunc is a handler which produces a value for a named variable.
446+
// A returned [expand.Variable] with Set=false signals an unset variable.
447+
type LookupVarHandlerFunc func(ctx context.Context, name string) expand.Variable
448+
449+
// FuncCallHandlerFunc is a handler which runs around the body of a declared shell function.
450+
type FuncCallHandlerFunc func(ctx context.Context, name string, args []string)
451+
452+
type runnerCtxKey struct{}
453+
454+
func runnerWithCtx(ctx context.Context, r *Runner) context.Context {
455+
return context.WithValue(ctx, runnerCtxKey{}, r)
456+
}
457+
458+
func runnerFromCtx(ctx context.Context) *Runner {
459+
r, _ := ctx.Value(runnerCtxKey{}).(*Runner)
460+
return r
461+
}
462+
463+
// RunStmts dispatches stmts on the runner attached to ctx,
464+
// reusing its current scope rather than starting a fresh program.
465+
// To run a fresh program, use [Runner.Run].
466+
//
467+
// A non-zero exit status is returned as an [ExitStatus] error.
468+
// An error is also returned if ctx has no runner attached.
469+
func RunStmts(ctx context.Context, stmts []*syntax.Stmt) error {
470+
r := runnerFromCtx(ctx)
471+
if r == nil {
472+
return errors.New("interp.RunStmts: no runner in context")
473+
}
474+
r.stmts(ctx, stmts)
475+
if err := r.exit.err; err != nil {
476+
return err
477+
}
478+
if code := r.exit.code; code != 0 {
479+
return ExitStatus(code)
480+
}
481+
return nil
482+
}

0 commit comments

Comments
 (0)