Documentation ¶
Overview ¶
Package interp implements an interpreter that executes shell programs. It aims to support POSIX, but its support is not complete yet. It also supports some Bash features.
Example ¶
package main import ( "context" "os" "strings" "github.com/MerSna/sh/v4/expand" "github.com/MerSna/sh/v4/interp" "github.com/MerSna/sh/v4/syntax" ) func main() { src := ` foo=abc for i in 1 2 3; do foo+=$i done let bar=(2 + 3) echo $foo $bar echo $GLOBAL ` file, _ := syntax.NewParser().Parse(strings.NewReader(src), "") runner, _ := interp.New( interp.Env(expand.ListEnviron("GLOBAL=global_value")), interp.StdIO(nil, os.Stdout, os.Stdout), ) runner.Run(context.TODO(), file) }
Output: abc123 5 global_value
Index ¶
- func IsExitStatus(err error) (status uint8, ok bool)
- func LookPath(env expand.Environ, file string) (string, error)
- func LookPathDir(cwd string, env expand.Environ, file string) (string, error)
- func NewExitStatus(status uint8) error
- type ExecHandlerFunc
- type HandlerContext
- type OpenHandlerFunc
- type Runner
- type RunnerOption
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsExitStatus ¶
IsExitStatus checks whether error contains an exit status and returns it.
func LookPathDir ¶
LookPathDir is similar to os/exec.LookPath, with the difference that it uses the provided environment. env is used to fetch relevant environment variables such as PWD and PATH.
If no error is returned, the returned path must be valid.
func NewExitStatus ¶
NewExitStatus creates an error which contains the specified exit status code.
Types ¶
type ExecHandlerFunc ¶
ExecHandlerFunc is a handler which executes simple command. It is called for all CallExpr nodes where the first argument is neither a declared function nor a builtin.
Returning nil error sets commands exit status to 0. Other exit statuses can be set with NewExitStatus. Any other error will halt an interpreter.
func DefaultExecHandler ¶
func DefaultExecHandler(killTimeout time.Duration) ExecHandlerFunc
DefaultExecHandler returns an ExecHandlerFunc used by default. It finds binaries in PATH and executes them. When context is cancelled, interrupt signal is sent to running processes. KillTimeout is a duration to wait before sending kill signal. A negative value means that a kill signal will be sent immediately. On Windows, the kill signal is always sent immediately, because Go doesn't currently support sending Interrupt on Windows. Runner.New sets killTimeout to 2 seconds by default.
type HandlerContext ¶
type HandlerContext struct { // Env is a read-only version of the interpreter's environment, // including environment variables, global variables, and local function // variables. Env expand.Environ // Dir is the interpreter's current directory. Dir string // Stdin is the interpreter's current standard input reader. Stdin io.Reader // Stdout is the interpreter's current standard output writer. Stdout io.Writer // Stderr is the interpreter's current standard error writer. Stderr io.Writer }
HandlerContext is the data passed to all the handler functions via a context value. It contains some of the current state of the Runner.
func HandlerCtx ¶
func HandlerCtx(ctx context.Context) HandlerContext
HandlerCtx returns HandlerContext value stored in ctx. It panics if ctx has no HandlerContext stored.
type OpenHandlerFunc ¶
type OpenHandlerFunc func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error)
OpenHandlerFunc is a handler which opens files. It is called for all files that are opened directly by the shell, such as in redirects. Files opened by executed programs are not included.
The path parameter may be relative to the current directory, which can be fetched via HandlerCtx.
Use a return error of type *os.PathError to have the error printed to stderr and the exit status set to 1. If the error is of any other type, the interpreter will come to a stop.
func DefaultOpenHandler ¶
func DefaultOpenHandler() OpenHandlerFunc
DefaultOpenHandler returns an OpenHandlerFunc used by default. It uses os.OpenFile to open files.
type Runner ¶
type Runner struct { // Env specifies the initial environment for the interpreter, which must // be non-nil. Env expand.Environ // Dir specifies the working directory of the command, which must be an // absolute path. Dir string // Params are the current shell parameters, e.g. from running a shell // file or calling a function. Accessible via the $@/$* family of vars. Params []string Vars map[string]expand.Variable Funcs map[string]*syntax.Stmt // contains filtered or unexported fields }
A Runner interprets shell programs. It can be reused, but it is not safe for concurrent use. You should typically use New to build a new Runner.
Note that writes to Stdout and Stderr may be concurrent if background commands are used. If you plan on using an io.Writer implementation that isn't safe for concurrent use, consider a workaround like hiding writes behind a mutex.
To create a Runner, use New. Runner's exported fields are meant to be configured via runner options; once a Runner has been created, the fields should be treated as read-only.
func New ¶
func New(opts ...RunnerOption) (*Runner, error)
New creates a new Runner, applying a number of options. If applying any of the options results in an error, it is returned.
Any unset options fall back to their defaults. For example, not supplying the environment falls back to the process's environment, and not supplying the standard output writer means that the output will be discarded.
func (*Runner) Exited ¶
Exited reports whether the last Run call should exit an entire shell. This can be triggered by the "exit" built-in command, for example.
Note that this state is overwritten at every Run call, so it should be checked immediately after each Run call.
func (*Runner) Reset ¶
func (r *Runner) Reset()
Reset returns a runner to its initial state, right before the first call to Run or Reset.
Typically, this function only needs to be called if a runner is reused to run multiple programs non-incrementally. Not calling Reset between each run will mean that the shell state will be kept, including variables, options, and the current directory.
func (*Runner) Run ¶
Run interprets a node, which can be a *File, *Stmt, or Command. If a non-nil error is returned, it will typically contain a command's exit status, which can be retrieved with IsExitStatus.
Run can be called multiple times synchronously to interpret programs incrementally. To reuse a Runner without keeping the internal shell state, call Reset.
Calling Run on an entire *File implies an exit, meaning that an exit trap may run.
func (*Runner) Subshell ¶
Subshell makes a copy of the given Runner, suitable for use concurrently with the original. The copy will have the same environment, including variables and functions, but they can all be modified without affecting the original.
Subshell is not safe to use concurrently with Run. Orchestrating this is left up to the caller; no locking is performed.
To replace e.g. stdin/out/err, do StdIO(r.stdin, r.stdout, r.stderr)(r) on the copy.
type RunnerOption ¶
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).
func Dir ¶
func Dir(path string) RunnerOption
Dir sets the interpreter's working directory. If empty, the process's current directory is used.
func Env ¶
func Env(env expand.Environ) RunnerOption
Env sets the interpreter's environment. If nil, a copy of the current process's environment is used.
func ExecHandler ¶
func ExecHandler(f ExecHandlerFunc) RunnerOption
ExecHandler sets command execution handler. See ExecHandlerFunc for more info.
Example ¶
package main import ( "context" "fmt" "os" "strings" "time" "github.com/MerSna/sh/v4/interp" "github.com/MerSna/sh/v4/syntax" ) func main() { 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 } 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 interp.DefaultExecHandler(2*time.Second)(ctx, args) } runner, _ := interp.New( interp.StdIO(nil, os.Stdout, os.Stdout), interp.ExecHandler(exec), ) runner.Run(context.TODO(), file) }
Output: foo foo!bar!baz missing-program is not installed
func OpenHandler ¶
func OpenHandler(f OpenHandlerFunc) RunnerOption
OpenHandler sets file open handler. See OpenHandlerFunc for more info.
Example ¶
package main import ( "context" "io" "os" "runtime" "strings" "github.com/MerSna/sh/v4/interp" "github.com/MerSna/sh/v4/syntax" ) func main() { src := "echo foo; echo bar >/dev/null" file, _ := syntax.NewParser().Parse(strings.NewReader(src), "") open := func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { if runtime.GOOS == "windows" && path == "/dev/null" { path = "NUL" } return interp.DefaultOpenHandler()(ctx, path, flag, perm) } runner, _ := interp.New( interp.StdIO(nil, os.Stdout, os.Stdout), interp.OpenHandler(open), ) runner.Run(context.TODO(), file) }
Output: foo
func Params ¶
func Params(args ...string) RunnerOption
Params populates the shell options and parameters. For example, Params("-e", "--", "foo") will set the "-e" option and the parameters ["foo"], and Params("+e") will unset the "-e" option and leave the parameters untouched.
This is similar to what the interpreter's "set" builtin does.