// Package eval handles evaluation of parsed Elvish code and provides runtime // facilities.
package eval import ( ) var logger = logutil.GetLogger("[eval] ") const ( // FnSuffix is the suffix for the variable names of functions. Defining a // function "foo" is equivalent to setting a variable named "foo~", and vice // versa. FnSuffix = "~" // NsSuffix is the suffix for the variable names of namespaces. Defining a // namespace foo is equivalent to setting a variable named "foo:", and vice // versa. NsSuffix = ":" ) const ( defaultValuePrefix = "▶ " defaultNotifyBgJobSuccess = true initIndent = vals.NoPretty ) // Evaler provides methods for evaluating code, and maintains state that is // persisted between evaluation of different pieces of code. An Evaler is safe // to use concurrently. type Evaler struct { // All mutations to Evaler should be guarded by this mutex. // // Note that this is *not* a GIL; most state mutations when executing Elvish // code is localized and do not need to hold this mutex. // // TODO: Actually guard all mutations by this mutex. mu sync.RWMutex global, builtin *Ns deprecations deprecationRegistry // State of the module system. // // Library directory. libDir string // Internal modules are indexed by use specs. External modules are indexed by // absolute paths. modules map[string]*Ns // Various states and configs exposed to Elvish code. // // The prefix to prepend to value outputs when writing them to terminal, // exposed as $value-out-prefix. valuePrefix string // Whether to notify the success of background jobs, exposed as // $notify-bg-job-sucess. notifyBgJobSuccess bool // The current number of background jobs, exposed as $num-bg-jobs. numBgJobs int // Command-line arguments, exposed as $args. args vals.List // Chdir hooks, exposed indirectly as $before-chdir and $after-chdir. beforeChdir, afterChdir []func(string) // Dependencies. // // TODO: Remove these dependency by providing more general extension points. daemonClient daemon.Client } // Editor is the interface that the line editor has to satisfy. It is needed so // that this package does not depend on the edit package. type Editor interface { RunAfterCommandHooks(src parse.Source, duration float64, err error) } //elvdoc:var after-chdir // // A list of functions to run after changing directory. These functions are always // called with directory to change it, which might be a relative path. The // following example also shows `$before-chdir`: // // ```elvish-transcript // ~> before-chdir = [[dir]{ echo "Going to change to "$dir", pwd is "$pwd }] // ~> after-chdir = [[dir]{ echo "Changed to "$dir", pwd is "$pwd }] // ~> cd /usr // Going to change to /usr, pwd is /Users/xiaq // Changed to /usr, pwd is /usr // /usr> cd local // Going to change to local, pwd is /usr // Changed to local, pwd is /usr/local // /usr/local> // ``` // // @cf before-chdir //elvdoc:var before-chdir // // A list of functions to run before changing directory. These functions are always // called with the new working directory. // // @cf after-chdir //elvdoc:var num-bg-jobs // // Number of background jobs. //elvdoc:var notify-bg-job-success // // Whether to notify success of background jobs, defaulting to `$true`. // // Failures of background jobs are always notified. //elvdoc:var value-out-indicator // // A string put before value outputs (such as those of of `put`). Defaults to // `'▶ '`. Example: // // ```elvish-transcript // ~> put lorem ipsum // ▶ lorem // ▶ ipsum // ~> value-out-indicator = 'val> ' // ~> put lorem ipsum // val> lorem // val> ipsum // ``` // // Note that you almost always want some trailing whitespace for readability. // NewEvaler creates a new Evaler. func () *Evaler { := builtinNs.Ns() , := vector.Empty, vector.Empty := &Evaler{ global: new(Ns), builtin: , deprecations: newDeprecationRegistry(), modules: map[string]*Ns{"builtin": }, valuePrefix: defaultValuePrefix, notifyBgJobSuccess: defaultNotifyBgJobSuccess, numBgJobs: 0, args: vals.EmptyList, } .beforeChdir = []func(string){ adaptChdirHook("before-chdir", , &)} .afterChdir = []func(string){ adaptChdirHook("after-chdir", , &)} := NsBuilder{}. Add("pwd", NewPwdVar()). Add("before-chdir", vars.FromPtr(&)). Add("after-chdir", vars.FromPtr(&)). Add("value-out-indicator", vars.FromPtrWithMutex( &.valuePrefix, &.mu)). Add("notify-bg-job-success", vars.FromPtrWithMutex( &.notifyBgJobSuccess, &.mu)). Add("num-bg-jobs", vars.FromGet(func() interface{} { return strconv.Itoa(.getNumBgJobs()) })). Add("args", vars.FromGet(func() interface{} { return .getArgs() })). Ns() .slots = append(.slots, .slots...) .names = append(.names, .names...) .deleted = append(.deleted, make([]bool, len(.names))...) return } func ( string, *Evaler, *vector.Vector) func(string) { return func( string) { , := PortsFromStdFiles(.ValuePrefix()) defer () := CallCfg{Args: []interface{}{}, From: "[hook " + + "]"} := EvalCfg{Ports: [:]} for := (*).Iterator(); .HasElem(); .Next() { , := .Elem().(Callable) if ! { fmt.Fprintln(os.Stderr, , "hook must be callable") continue } := .Call(, , ) if != nil { // TODO: Stack trace fmt.Fprintln(os.Stderr, ) } } } } // Access methods. // Global returns the global Ns. func ( *Evaler) () *Ns { .mu.RLock() defer .mu.RUnlock() return .global } // AddGlobal merges the given *Ns into the global namespace. func ( *Evaler) ( *Ns) { .mu.Lock() defer .mu.Unlock() .global = CombineNs(.global, ) } // Builtin returns the builtin Ns. func ( *Evaler) () *Ns { .mu.RLock() defer .mu.RUnlock() return .builtin } // AddBuiltin merges the given *Ns into the builtin namespace. func ( *Evaler) ( *Ns) { .mu.Lock() defer .mu.Unlock() .builtin = CombineNs(.builtin, ) } func ( *Evaler) ( deprecation) bool { .mu.Lock() defer .mu.Unlock() return .deprecations.register() } // Returns libdir. func ( *Evaler) () string { .mu.RLock() defer .mu.RUnlock() return .libDir } // SetLibDir sets the library directory for finding external modules. func ( *Evaler) ( string) { .mu.Lock() defer .mu.Unlock() .libDir = } // AddModule add an internal module so that it can be used with "use $name" from // script. func ( *Evaler) ( string, *Ns) { .mu.Lock() defer .mu.Unlock() .modules[] = } // ValuePrefix returns the prefix to prepend to value outputs when writing them // to terminal. func ( *Evaler) () string { .mu.RLock() defer .mu.RUnlock() return .valuePrefix } func ( *Evaler) () bool { .mu.RLock() defer .mu.RUnlock() return .notifyBgJobSuccess } func ( *Evaler) () int { .mu.RLock() defer .mu.RUnlock() return .numBgJobs } func ( *Evaler) ( int) { .mu.Lock() defer .mu.Unlock() .numBgJobs += } func ( *Evaler) () vals.List { .mu.RLock() defer .mu.RUnlock() return .args } // SetArgs sets the value of the $args variable to a list of strings, built from // the given slice. func ( *Evaler) ( []string) { := listOfStrings() .mu.Lock() defer .mu.Unlock() .args = } // Returns copies of beforeChdir and afterChdir. func ( *Evaler) () ([]func(string), []func(string)) { .mu.RLock() defer .mu.RUnlock() return append(([]func(string))(nil), .beforeChdir...), append(([]func(string))(nil), .afterChdir...) } // AddBeforeChdir adds a function to run before changing directory. func ( *Evaler) ( func(string)) { .mu.Lock() defer .mu.Unlock() .beforeChdir = append(.beforeChdir, ) } // AddAfterChdir adds a function to run after changing directory. func ( *Evaler) ( func(string)) { .mu.Lock() defer .mu.Unlock() .afterChdir = append(.afterChdir, ) } // SetDaemonClient sets the daemon client associated with the Evaler. func ( *Evaler) ( daemon.Client) { .mu.Lock() defer .mu.Unlock() .daemonClient = } // DaemonClient returns the daemon client associated with the Evaler. func ( *Evaler) () daemon.Client { .mu.RLock() defer .mu.RUnlock() return .daemonClient } // Chdir changes the current directory. On success it also updates the PWD // environment variable and records the new directory in the directory history. // It runs the functions in beforeChdir immediately before changing the // directory, and the functions in afterChdir immediately after (if chdir was // successful). It returns nil as long as the directory changing part succeeds. func ( *Evaler) ( string) error { , := .chdirHooks() for , := range { () } := os.Chdir() if != nil { return } for , := range { () } , := os.Getwd() if != nil { logger.Println("getwd after cd:", ) return nil } os.Setenv(env.PWD, ) return nil } // EvalCfg keeps configuration for the (*Evaler).Eval method. type EvalCfg struct { // Ports to use in evaluation. The first 3 elements, if not specified // (either being nil or Ports containing fewer than 3 elements), // will be filled with DummyInputPort, DummyOutputPort and // DummyOutputPort respectively. Ports []*Port // Callback to get a channel of interrupt signals and a function to call // when the channel is no longer needed. Interrupt func() (<-chan struct{}, func()) // Whether the Eval method should try to put the Elvish in the foreground // after the code is executed. PutInFg bool // If not nil, used the given global namespace, instead of Evaler's own. Global *Ns } func ( *EvalCfg) () { if len(.Ports) < 3 { .Ports = append(.Ports, make([]*Port, 3-len(.Ports))...) } if .Ports[0] == nil { .Ports[0] = DummyInputPort } if .Ports[1] == nil { .Ports[1] = DummyOutputPort } if .Ports[2] == nil { .Ports[2] = DummyOutputPort } } // Eval evaluates a piece of source code with the given configuration. The // returned error may be a parse error, compilation error or exception. func ( *Evaler) ( parse.Source, EvalCfg) error { .fillDefaults() := .Ports[2].File , := parse.Parse(, parse.Config{WarningWriter: }) if != nil { return } .mu.Lock() := .builtin := .Global == nil if { // If cfg.Global is nil, use the Evaler's default global, and also // mutate the default global. .Global = .global // Continue to hold the mutex; it will be released when ev.global gets // mutated. } else { .mu.Unlock() } , := compile(.static(), .Global.static(), , ) if != nil { if { .mu.Unlock() } return } , := .prepareFrame(, ) defer () , := .prepare() if { .global = .mu.Unlock() } return () } // CallCfg keeps configuration for the (*Evaler).Call method. type CallCfg struct { // Arguments to pass to the the function. Args []interface{} // Options to pass to the function. Opts map[string]interface{} // The name of the internal source that is calling the function. From string } func ( *CallCfg) () { if .Opts == nil { .Opts = NoOpts } if .From == "" { .From = "[internal]" } } // Call calls a given function. func ( *Evaler) ( Callable, CallCfg, EvalCfg) error { .fillDefaults() .fillDefaults() if .Global == nil { .Global = .Global() } , := .prepareFrame(parse.Source{Name: .From}, ) defer () return .Call(, .Args, .Opts) } func ( *Evaler) ( parse.Source, EvalCfg) (*Frame, func()) { var <-chan struct{} var func() if .Interrupt != nil { , = .Interrupt() } := &Frame{, , .Global, new(Ns), , .Ports, nil, false} return , func() { if != nil { () } if .PutInFg { := putSelfInFg() if != nil { fmt.Fprintln(.Ports[2].File, "failed to put myself in foreground:", ) } } } } // Check checks the given source code for any parse error and compilation error. // It always tries to compile the code even if there is a parse error; both // return values may be non-nil. If w is not nil, deprecation messages are // written to it. func ( *Evaler) ( parse.Source, io.Writer) (*parse.Error, *diag.Error) { , := parse.Parse(, parse.Config{WarningWriter: }) return parse.GetError(), .CheckTree(, ) } // CheckTree checks the given parsed source tree for compilation errors. If w is // not nil, deprecation messages are written to it. func ( *Evaler) ( parse.Tree, io.Writer) *diag.Error { , := .compile(, .Global(), ) return GetCompilationError() } // Compiles a parsed tree. func ( *Evaler) ( parse.Tree, *Ns, io.Writer) (nsOp, error) { return compile(.Builtin().static(), .static(), , ) }