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 }