interp

package
v3.0.0-alpha3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 23, 2019 License: BSD-3-Clause Imports: 21 Imported by: 82

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"

	"mvdan.cc/sh/v3/expand"
	"mvdan.cc/sh/v3/interp"
	"mvdan.cc/sh/v3/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

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultExec

func DefaultExec(ctx context.Context, args []string) error

func DefaultOpen

func DefaultOpen(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error)

func LookPath

func LookPath(env expand.Environ, file string) (string, error)

LookPath 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.

Types

type ExecModule

type ExecModule func(ctx context.Context, args []string) error

ExecModule is the module responsible for executing a simple command. It is executed for all CallExpr nodes where the first argument is neither a declared function nor a builtin.

Use a return error of type ExitStatus to set the exit status. A nil error has the same effect as ExitStatus(0). If the error is of any other type, the interpreter will come to a stop.

Example
package main

import (
	"context"
	"fmt"
	"os"
	"strings"

	"mvdan.cc/sh/v3/interp"
	"mvdan.cc/sh/v3/syntax"
)

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

	join := func(next interp.ExecModule) interp.ExecModule {
		return func(ctx context.Context, args []string) error {
			if args[0] == "join" {
				mc, _ := interp.FromModuleContext(ctx)
				fmt.Fprintln(mc.Stdout, strings.Join(args[2:], args[1]))
				return nil
			}
			return next(ctx, args)
		}
	}
	notInstalled := func(next interp.ExecModule) interp.ExecModule {
		return func(ctx context.Context, args []string) error {
			mc, _ := interp.FromModuleContext(ctx)
			if _, err := interp.LookPath(mc.Env, args[0]); err != nil {
				fmt.Printf("%s is not installed\n", args[0])
				return interp.ExitStatus(1)
			}
			return next(ctx, args)
		}
	}
	runner, _ := interp.New(
		interp.StdIO(nil, os.Stdout, os.Stdout),
		interp.WithExecModules(join, notInstalled),
	)
	runner.Run(context.TODO(), file)
}
Output:

foo
foo!bar!baz
missing-program is not installed

type ExitStatus

type ExitStatus uint8

ExitStatus is a non-zero status code resulting from running a shell node.

func (ExitStatus) Error

func (s ExitStatus) Error() string

type ModuleCtx

type ModuleCtx 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

	// KillTimeout is the duration configured by Runner.KillTimeout; refer
	// to its docs for its purpose. It is needed to implement DefaultExec.
	KillTimeout time.Duration
}

ModuleCtx is the data passed to all the module functions via a context value. It contains some of the current state of the Runner, as well as some fields necessary to implement some of the modules.

func FromModuleContext

func FromModuleContext(ctx context.Context) (ModuleCtx, bool)

FromModuleContext returns the ModuleCtx value stored in ctx, if any.

type OpenModule

type OpenModule func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error)

OpenModule is the module responsible for opening a file. It is executed 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 FromModuleContext.

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 OpenDevImpls

func OpenDevImpls(next OpenModule) OpenModule

type Runner

type Runner struct {
	// Env specifies the environment of 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

	// Exec is the module responsible for executing programs. It must be
	// non-nil.
	Exec ExecModule
	// Open is the module responsible for opening files. It must be non-nil.
	Open OpenModule

	Stdin  io.Reader
	Stdout io.Writer
	Stderr io.Writer

	Vars  map[string]expand.Variable
	Funcs map[string]*syntax.Stmt

	// KillTimeout holds how much time the interpreter will wait for a
	// program to stop after being sent an interrupt signal, after
	// which a kill signal will be sent. This process will happen when the
	// interpreter's context is cancelled.
	//
	// The zero value will default to 2 seconds.
	//
	// 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.
	KillTimeout time.Duration
	// 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

func (r *Runner) Exited() bool

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

func (r *Runner) Run(ctx context.Context, node syntax.Node) error

Run interprets a node, which can be a *File, *Stmt, or Command. If a non-nil error is returned, it will typically be of type ExitStatus.

Run can be called multiple times synchronously to interpret programs incrementally. To reuse a Runner without keeping the internal shell state, call Reset.

type RunnerOption

type RunnerOption func(*Runner) error

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 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.

func StdIO

func StdIO(in io.Reader, out, err io.Writer) RunnerOption

StdIO configures an interpreter's standard input, standard output, and standard error. If out or err are nil, they default to a writer that discards the output.

func WithExecModules

func WithExecModules(mods ...func(next ExecModule) ExecModule) RunnerOption

WithExecModule sets up a runner with a chain of ExecModule middlewares. The chain is set up starting at the end, so that the first middleware in the list will be the first one to execute as part of the interpreter.

The last or innermost module is always DefaultExec. You can make it unreachable by adding a middleware that never calls its next module.

func WithOpenModules

func WithOpenModules(mods ...func(next OpenModule) OpenModule) RunnerOption

WithOpenModule sets up a runner with a chain of OpenModule middlewares. The chain is set up starting at the end, so that the first middleware in the list will be the first one to execute as part of the interpreter.

The last or innermost module is always DefaultOpen. You can make it unreachable by adding a middleware that never calls its next module.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL