Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

interp: add ExecHandlers to support exec middlewares #964

Merged
merged 1 commit into from
Jan 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 59 additions & 7 deletions interp/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,15 @@ type Runner struct {
// arguments. It may be nil.
callHandler CallHandlerFunc

// execHandler is a function responsible for executing programs. It must be non-nil.
// execHandler is responsible for executing programs. It must not be nil.
execHandler ExecHandlerFunc

// openHandler is a function responsible for opening files. It must be non-nil.
// execMiddlewares grows with calls to ExecHandlers,
// and is used to construct execHandler when Reset is first called.
// The slice is needed to preserve the relative order of middlewares.
execMiddlewares []func(ExecHandlerFunc) ExecHandlerFunc
mvdan marked this conversation as resolved.
Show resolved Hide resolved

// openHandler is a function responsible for opening files. It must not be nil.
openHandler OpenHandlerFunc

// readDirHandler is a function responsible for reading directories during
Expand Down Expand Up @@ -181,7 +186,6 @@ func (r *Runner) optByFlag(flag byte) *bool {
func New(opts ...RunnerOption) (*Runner, error) {
r := &Runner{
usedNew: true,
execHandler: DefaultExecHandler(2 * time.Second),
openHandler: DefaultOpenHandler(),
readDirHandler: DefaultReadDirHandler(),
statHandler: DefaultStatHandler(),
Expand Down Expand Up @@ -213,9 +217,11 @@ func New(opts ...RunnerOption) (*Runner, error) {
return r, nil
}

// RunnerOption is a function which can be passed to New to alter Runner behaviour.
// To apply option to existing Runner call it directly,
// for example interp.Params("-e")(runner).
// RunnerOption can be passed to New to alter a Runner's behaviour.
// It can also be applied directly on an existing Runner,
// such as interp.Params("-e")(runner).
// Note that options cannot be applied once Run or Reset have been called.
// TODO: enforce that rule via didReset.
type RunnerOption func(*Runner) error

// Env sets the interpreter's environment. If nil, a copy of the current
Expand Down Expand Up @@ -329,14 +335,46 @@ func CallHandler(f CallHandlerFunc) RunnerOption {
}
}

// ExecHandler sets the command execution handler. See ExecHandlerFunc for more info.
// ExecHandler sets one command execution handler,
// which replaces DefaultExecHandler(2 * time.Second).
//
// Deprecated: use ExecHandlers instead, which allows for middleware handlers.
func ExecHandler(f ExecHandlerFunc) RunnerOption {
return func(r *Runner) error {
r.execHandler = f
return nil
}
}

// ExecHandlers appends middlewares to handle command execution.
// The middlewares are chained from first to last, and the first is called by the runner.
// Each middleware is expected to call the "next" middleware at most once.
//
// For example, a middleware may implement only some commands.
// For those commands, it can run its logic and avoid calling "next".
// For any other commands, it can call "next" with the original parameters.
//
// Another common example is a middleware which always calls "next",
// but runs custom logic either before or after that call.
// For instance, a middleware could change the arguments to the "next" call,
// or it could print log lines before or after the call to "next".
//
// The last exec handler is DefaultExecHandler(2 * time.Second).
func ExecHandlers(middlewares ...func(next ExecHandlerFunc) ExecHandlerFunc) RunnerOption {
mvdan marked this conversation as resolved.
Show resolved Hide resolved
return func(r *Runner) error {
r.execMiddlewares = append(r.execMiddlewares, middlewares...)
return nil
}
}

// TODO: consider porting the middleware API in ExecHandlers to OpenHandler,
// ReadDirHandler, and StatHandler.

// TODO(v4): now that ExecHandlers allows calling a next handler with changed
// arguments, one of the two advantages of CallHandler is gone. The other is the
// ability to work with builtins; if we make ExecHandlers work with builtins, we
// could join both APIs.

// OpenHandler sets file open handler. See OpenHandlerFunc for more info.
func OpenHandler(f OpenHandlerFunc) RunnerOption {
return func(r *Runner) error {
Expand Down Expand Up @@ -561,6 +599,19 @@ func (r *Runner) Reset() {
r.origStdin = r.stdin
r.origStdout = r.stdout
r.origStderr = r.stderr

if r.execHandler != nil && len(r.execMiddlewares) > 0 {
panic("interp.ExecHandler should be replaced with interp.ExecHandlers, not mixed")
}
if r.execHandler == nil {
r.execHandler = DefaultExecHandler(2 * time.Second)
}
// Middlewares are chained from first to last, and each can call the
// next in the chain, so we need to construct the chain backwards.
for i := len(r.execMiddlewares) - 1; i >= 0; i-- {
middleware := r.execMiddlewares[i]
r.execHandler = middleware(r.execHandler)
}
}
// reset the internal state
*r = Runner{
Expand Down Expand Up @@ -632,6 +683,7 @@ func (r *Runner) Reset() {
r.setVarString("OPTIND", "1")

r.dirStack = append(r.dirStack, r.Dir)

r.didReset = true
}

Expand Down
34 changes: 19 additions & 15 deletions interp/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"os"
"runtime"
"strings"
"time"

"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
Expand Down Expand Up @@ -38,28 +37,33 @@ func Example() {
// global_value
}

func ExampleExecHandler() {
func ExampleExecHandlers() {
src := "echo foo; join ! foo bar baz; missing-program bar"
file, _ := syntax.NewParser().Parse(strings.NewReader(src), "")

exec := func(ctx context.Context, args []string) error {
hc := interp.HandlerCtx(ctx)

if args[0] == "join" {
fmt.Fprintln(hc.Stdout, strings.Join(args[2:], args[1]))
return nil
execJoin := func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
return func(ctx context.Context, args []string) error {
hc := interp.HandlerCtx(ctx)
if args[0] == "join" {
fmt.Fprintln(hc.Stdout, strings.Join(args[2:], args[1]))
return nil
}
return next(ctx, args)
}

if _, err := interp.LookPathDir(hc.Dir, hc.Env, args[0]); err != nil {
fmt.Printf("%s is not installed\n", args[0])
return interp.NewExitStatus(1)
}
execNotInstalled := func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
return func(ctx context.Context, args []string) error {
hc := interp.HandlerCtx(ctx)
if _, err := interp.LookPathDir(hc.Dir, hc.Env, args[0]); err != nil {
fmt.Printf("%s is not installed\n", args[0])
return interp.NewExitStatus(1)
}
return next(ctx, args)
}

return interp.DefaultExecHandler(2*time.Second)(ctx, args)
}
runner, _ := interp.New(
interp.StdIO(nil, os.Stdout, os.Stdout),
interp.ExecHandler(exec),
interp.ExecHandlers(execJoin, execNotInstalled),
)
runner.Run(context.TODO(), file)
// Output:
Expand Down
Loading