@@ -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+
511653func 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 }
0 commit comments