github.com/elves/Elvish@v0.12.0/eval/eval.go (about)

     1  // Package eval handles evaluation of parsed Elvish code and provides runtime
     2  // facilities.
     3  package eval
     4  
     5  import (
     6  	"fmt"
     7  	"os"
     8  	"os/signal"
     9  	"strings"
    10  	"syscall"
    11  
    12  	"github.com/elves/elvish/daemon"
    13  	"github.com/elves/elvish/eval/bundled"
    14  	"github.com/elves/elvish/eval/vals"
    15  	"github.com/elves/elvish/eval/vars"
    16  	"github.com/elves/elvish/parse"
    17  	"github.com/elves/elvish/sys"
    18  	"github.com/elves/elvish/util"
    19  	"github.com/xiaq/persistent/hashmap"
    20  	"github.com/xiaq/persistent/vector"
    21  )
    22  
    23  var logger = util.GetLogger("[eval] ")
    24  
    25  const (
    26  	// FnSuffix is the suffix for the variable names of functions. Defining a
    27  	// function "foo" is equivalent to setting a variable named "foo~", and vice
    28  	// versa.
    29  	FnSuffix = "~"
    30  	// NsSuffix is the suffix for the variable names of namespaces. Defining a
    31  	// namespace foo is equivalent to setting a variable named "foo:", and vice
    32  	// versa.
    33  	NsSuffix = ":"
    34  )
    35  
    36  const (
    37  	defaultValuePrefix        = "▶ "
    38  	defaultNotifyBgJobSuccess = true
    39  	defaultNumBgJobs          = 0
    40  	initIndent                = vals.NoPretty
    41  )
    42  
    43  // Evaler is used to evaluate elvish sources. It maintains runtime context
    44  // shared among all evalCtx instances.
    45  type Evaler struct {
    46  	evalerScopes
    47  	valuePrefix        string
    48  	notifyBgJobSuccess bool
    49  	numBgJobs          int
    50  	beforeChdir        []func(string)
    51  	afterChdir         []func(string)
    52  	DaemonClient       *daemon.Client
    53  	modules            map[string]Ns
    54  	// bundled modules
    55  	bundled map[string]string
    56  	Editor  Editor
    57  	libDir  string
    58  	intCh   chan struct{}
    59  }
    60  
    61  type evalerScopes struct {
    62  	Global  Ns
    63  	Builtin Ns
    64  }
    65  
    66  // NewEvaler creates a new Evaler.
    67  func NewEvaler() *Evaler {
    68  	builtin := builtinNs.Clone()
    69  
    70  	ev := &Evaler{
    71  		valuePrefix:        defaultValuePrefix,
    72  		notifyBgJobSuccess: defaultNotifyBgJobSuccess,
    73  		numBgJobs:          defaultNumBgJobs,
    74  		evalerScopes: evalerScopes{
    75  			Global:  make(Ns),
    76  			Builtin: builtin,
    77  		},
    78  		modules: map[string]Ns{
    79  			"builtin": builtin,
    80  		},
    81  		bundled: bundled.Get(),
    82  		Editor:  nil,
    83  		intCh:   nil,
    84  	}
    85  
    86  	beforeChdirElvish, afterChdirElvish := vector.Empty, vector.Empty
    87  	ev.beforeChdir = append(ev.beforeChdir,
    88  		adaptChdirHook("before-chdir", ev, &beforeChdirElvish))
    89  	ev.afterChdir = append(ev.afterChdir,
    90  		adaptChdirHook("after-chdir", ev, &afterChdirElvish))
    91  	builtin["before-chdir"] = vars.FromPtr(&beforeChdirElvish)
    92  	builtin["after-chdir"] = vars.FromPtr(&afterChdirElvish)
    93  
    94  	builtin["value-out-indicator"] = vars.FromPtr(&ev.valuePrefix)
    95  	builtin["notify-bg-job-success"] = vars.FromPtr(&ev.notifyBgJobSuccess)
    96  	builtin["num-bg-jobs"] = vars.FromPtr(&ev.numBgJobs)
    97  	builtin["pwd"] = PwdVariable{ev}
    98  
    99  	return ev
   100  }
   101  
   102  func adaptChdirHook(name string, ev *Evaler, pfns *vector.Vector) func(string) {
   103  	return func(path string) {
   104  		stdPorts := newStdPorts(os.Stdin, os.Stdout, os.Stderr, ev.valuePrefix)
   105  		defer stdPorts.close()
   106  		for it := (*pfns).Iterator(); it.HasElem(); it.Next() {
   107  			fn, ok := it.Elem().(Callable)
   108  			if !ok {
   109  				fmt.Fprintln(os.Stderr, name, "hook must be callable")
   110  				continue
   111  			}
   112  			fm := NewTopFrame(ev,
   113  				NewInternalSource("["+name+" hook]"), stdPorts.ports[:])
   114  			err := fm.Call(fn, []interface{}{path}, NoOpts)
   115  			if err != nil {
   116  				// TODO: Stack trace
   117  				fmt.Fprintln(os.Stderr, err)
   118  			}
   119  		}
   120  	}
   121  }
   122  
   123  // Close releases resources allocated when creating this Evaler. Currently this
   124  // does nothing and always returns a nil error.
   125  func (ev *Evaler) Close() error {
   126  	return nil
   127  }
   128  
   129  // AddBeforeChdir adds a function to run before changing directory.
   130  func (ev *Evaler) AddBeforeChdir(f func(string)) {
   131  	ev.beforeChdir = append(ev.beforeChdir, f)
   132  }
   133  
   134  // AddAfterChdir adds a function to run after changing directory.
   135  func (ev *Evaler) AddAfterChdir(f func(string)) {
   136  	ev.afterChdir = append(ev.afterChdir, f)
   137  }
   138  
   139  // InstallDaemonClient installs a daemon client to the Evaler.
   140  func (ev *Evaler) InstallDaemonClient(client *daemon.Client) {
   141  	ev.DaemonClient = client
   142  }
   143  
   144  // InstallModule installs a module to the Evaler so that it can be used with
   145  // "use $name" from script.
   146  func (ev *Evaler) InstallModule(name string, mod Ns) {
   147  	ev.modules[name] = mod
   148  }
   149  
   150  // InstallBundled installs a bundled module to the Evaler.
   151  func (ev *Evaler) InstallBundled(name, src string) {
   152  	ev.bundled[name] = src
   153  }
   154  
   155  // SetArgs sets the $args builtin variable.
   156  func (ev *Evaler) SetArgs(args []string) {
   157  	v := vector.Empty
   158  	for _, arg := range args {
   159  		v = v.Cons(arg)
   160  	}
   161  	ev.Builtin["args"] = vars.NewRo(v)
   162  }
   163  
   164  // SetLibDir sets the library directory, in which external modules are to be
   165  // found.
   166  func (ev *Evaler) SetLibDir(libDir string) {
   167  	ev.libDir = libDir
   168  }
   169  
   170  func searchPaths() []string {
   171  	return strings.Split(os.Getenv("PATH"), ":")
   172  }
   173  
   174  // growPorts makes the size of ec.ports at least n, adding nil's if necessary.
   175  func (fm *Frame) growPorts(n int) {
   176  	if len(fm.ports) >= n {
   177  		return
   178  	}
   179  	ports := fm.ports
   180  	fm.ports = make([]*Port, n)
   181  	copy(fm.ports, ports)
   182  }
   183  
   184  // EvalWithStdPorts sets up the Evaler with standard ports and evaluates an Op.
   185  // The supplied name and text are used in diagnostic messages.
   186  func (ev *Evaler) EvalWithStdPorts(op Op, src *Source) error {
   187  	stdPorts := newStdPorts(os.Stdin, os.Stdout, os.Stderr, ev.valuePrefix)
   188  	defer stdPorts.close()
   189  	return ev.Eval(op, stdPorts.ports[:], src)
   190  }
   191  
   192  // Eval sets up the Evaler with the given ports and evaluates an Op.
   193  // The supplied name and text are used in diagnostic messages.
   194  func (ev *Evaler) Eval(op Op, ports []*Port, src *Source) error {
   195  	// Ignore TTOU.
   196  	//
   197  	// When a subprocess in its own process group puts itself in the foreground,
   198  	// Elvish will be put in the background. When the code finishes execution,
   199  	// Elvish will attempt to move itself back to the foreground by calling
   200  	// tcsetpgrp. However, whenever a background process calls tcsetpgrp (or
   201  	// otherwise attempts to modify the terminal configuration), TTOU will be
   202  	// sent, whose default handler is to stop the process. Or, if the process
   203  	// lives in an orphaned process group (which is often the case for Elvish),
   204  	// the call will outright fail. Therefore, for Elvish to be able to move
   205  	// itself back to the foreground later, we need to ignore TTOU now.
   206  	ignoreTTOU()
   207  	defer unignoreTTOU()
   208  
   209  	stopSigGoroutine := make(chan struct{})
   210  	sigGoRoutineDone := make(chan struct{})
   211  	// Set up intCh.
   212  	ev.intCh = make(chan struct{})
   213  	sigCh := make(chan os.Signal)
   214  	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGQUIT)
   215  	go func() {
   216  		closedIntCh := false
   217  	loop:
   218  		for {
   219  			select {
   220  			case <-sigCh:
   221  				if !closedIntCh {
   222  					close(ev.intCh)
   223  					closedIntCh = true
   224  				}
   225  			case <-stopSigGoroutine:
   226  				break loop
   227  			}
   228  		}
   229  		ev.intCh = nil
   230  		signal.Stop(sigCh)
   231  		close(sigGoRoutineDone)
   232  	}()
   233  
   234  	err := ev.eval(op, ports, src)
   235  
   236  	close(stopSigGoroutine)
   237  	<-sigGoRoutineDone
   238  
   239  	// Put myself in foreground, in case some command has put me in background.
   240  	// XXX Should probably use fd of /dev/tty instead of 0.
   241  	if sys.IsATTY(os.Stdin) {
   242  		err := putSelfInFg()
   243  		if err != nil {
   244  			fmt.Println("failed to put myself in foreground:", err)
   245  		}
   246  	}
   247  
   248  	return err
   249  }
   250  
   251  // eval evaluates a chunk node n. The supplied name and text are used in
   252  // diagnostic messages.
   253  func (ev *Evaler) eval(op Op, ports []*Port, src *Source) error {
   254  	ec := NewTopFrame(ev, src, ports)
   255  	return ec.Eval(op)
   256  }
   257  
   258  // Compile compiles Elvish code in the global scope. If the error is not nil, it
   259  // always has type CompilationError.
   260  func (ev *Evaler) Compile(n *parse.Chunk, src *Source) (Op, error) {
   261  	return compile(ev.Builtin.static(), ev.Global.static(), n, src)
   262  }
   263  
   264  // EvalSource evaluates a chunk of Elvish source.
   265  func (ev *Evaler) EvalSource(src *Source) error {
   266  	n, err := parse.Parse(src.name, src.code)
   267  	if err != nil {
   268  		return err
   269  	}
   270  	op, err := ev.Compile(n, src)
   271  	if err != nil {
   272  		return err
   273  	}
   274  	return ev.EvalWithStdPorts(op, src)
   275  }
   276  
   277  // SourceRC evaluates a rc.elv file. It evaluates the file in the global
   278  // namespace. If the file defines a $-exports- map variable, the variable is
   279  // removed and its content is poured into the global namespace, not overriding
   280  // existing variables.
   281  func (ev *Evaler) SourceRC(src *Source) error {
   282  	n, err := parse.Parse(src.name, src.code)
   283  	if err != nil {
   284  		return err
   285  	}
   286  	op, err := ev.Compile(n, src)
   287  	if err != nil {
   288  		return err
   289  	}
   290  	errEval := ev.EvalWithStdPorts(op, src)
   291  	var errExports error
   292  	if ev.Global.HasName("-exports-") {
   293  		exports := ev.Global.PopName("-exports-").Get()
   294  		switch exports := exports.(type) {
   295  		case hashmap.Map:
   296  			for it := exports.Iterator(); it.HasElem(); it.Next() {
   297  				k, v := it.Elem()
   298  				if name, ok := k.(string); ok {
   299  					if !ev.Global.HasName(name) {
   300  						ev.Global.Add(name, vars.NewAnyWithInit(v))
   301  					}
   302  				} else {
   303  					errKey := fmt.Errorf("non-string key in $-exports-: %s",
   304  						vals.Repr(k, vals.NoPretty))
   305  					errExports = util.Errors(errExports, errKey)
   306  				}
   307  			}
   308  		default:
   309  			errExports = fmt.Errorf(
   310  				"$-exports- should be a map, got %s", vals.Kind(exports))
   311  		}
   312  	}
   313  	return util.Errors(errEval, errExports)
   314  }