github.com/neugram/ng@v0.0.0-20180309130942-d472ff93d872/ngcore/ngcore.go (about) 1 // Copyright 2017 The Neugram Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package ngcore presents a Neugram interpreter interface and 6 // the associated machinery that depends on the state of the 7 // interpreter, such as code completion. 8 // 9 // This package is designed for embedding Neugram into a program. 10 package ngcore 11 12 import ( 13 "bufio" 14 "bytes" 15 "context" 16 "errors" 17 "fmt" 18 "io" 19 "os" 20 "os/exec" 21 "path/filepath" 22 "reflect" 23 "strings" 24 "sync" 25 "time" 26 27 "github.com/peterh/liner" 28 "neugram.io/ng/eval" 29 "neugram.io/ng/eval/environ" 30 "neugram.io/ng/eval/shell" 31 "neugram.io/ng/format" 32 "neugram.io/ng/parser" 33 ) 34 35 type Neugram struct { 36 // TODO: Universe *eval.Scope for session initialization 37 38 mu sync.Mutex // guards map, not interior of *Session obj 39 sessions map[string]*Session 40 } 41 42 func New() *Neugram { 43 shell.Init() 44 return &Neugram{ 45 sessions: make(map[string]*Session), 46 } 47 } 48 49 func (ng *Neugram) Close() error { 50 var sessions []*Session 51 ng.mu.Lock() 52 for _, v := range ng.sessions { 53 sessions = append(sessions, v) 54 } 55 ng.mu.Unlock() 56 57 for _, s := range sessions { 58 s.Close() 59 } 60 return nil 61 } 62 63 type Session struct { 64 Parser *parser.Parser 65 Program *eval.Program 66 ShellState *shell.State 67 ParserState parser.ParserState 68 69 // Stdin, Stdout, and Stderr are the stdio to hook up to evaluator. 70 // Nominally Stdout and Stderr are io.Writers. 71 // If these interfaces have the concrete type *os.File the underlying 72 // file descriptor is passed directly to shell jobs. 73 Stdin *os.File 74 Stdout *os.File 75 Stderr *os.File 76 77 ExecCount int // number of statements executed 78 // TODO: record execution statement history here 79 80 Liner *liner.State 81 History struct { 82 Ng History 83 Sh History 84 } 85 name string 86 neugram *Neugram 87 } 88 89 func (n *Neugram) NewSession(ctx context.Context, name string, env []string) (*Session, error) { 90 s := n.newSession(ctx, name, env) 91 92 n.mu.Lock() 93 defer n.mu.Unlock() 94 if n.sessions[name] != nil { 95 return nil, fmt.Errorf("neugram: session %q already exists", name) 96 } 97 n.sessions[name] = s 98 return s, nil 99 } 100 101 func (n *Neugram) GetSession(name string) *Session { 102 n.mu.Lock() 103 defer n.mu.Unlock() 104 return n.sessions[name] 105 } 106 107 func (n *Neugram) newSession(ctx context.Context, name string, env []string) *Session { 108 // TODO: default shell state 109 shellState := &shell.State{ 110 Env: environ.NewFrom(env), 111 Alias: environ.New(), 112 } 113 114 // TODO: wire ctx into *eval.Program for cancellation (replace sigint channel) 115 s := &Session{ 116 Parser: parser.New(name), 117 Program: eval.New("session-"+name, shellState), 118 ShellState: shellState, 119 ParserState: parser.StateUnknown, 120 Liner: liner.NewLiner(), 121 name: name, 122 neugram: n, 123 } 124 return s 125 } 126 127 func (n *Neugram) GetOrNewSession(ctx context.Context, name string, env []string) *Session { 128 n.mu.Lock() 129 defer n.mu.Unlock() 130 if s := n.sessions[name]; s != nil { 131 return s 132 } 133 s := n.newSession(ctx, name, env) 134 n.sessions[name] = s 135 return s 136 } 137 138 // RunScript evaluates a complete Neugram script. 139 func (s *Session) RunScript(r io.Reader) (parser.ParserState, error) { 140 var err error 141 scanner := bufio.NewScanner(r) 142 stdout := s.Stdout 143 if stdout == nil { 144 stdout, err = os.Create(os.DevNull) 145 if err != nil { 146 return s.ParserState, err 147 } 148 } 149 150 for i := 0; scanner.Scan(); i++ { 151 b := scanner.Bytes() 152 if i == 0 && len(b) > 2 && b[0] == '#' && b[1] == '!' { // shebang 153 continue 154 } 155 156 vals, err := s.Exec(b) 157 if err != nil { 158 return s.ParserState, err 159 } 160 s.Display(stdout, vals) 161 } 162 163 switch state := s.ParserState; state { 164 case parser.StateStmtPartial, parser.StateCmdPartial: 165 name := "<input>" 166 type namer interface { 167 Name() string 168 } 169 if f, ok := r.(namer); ok { 170 name = f.Name() 171 } 172 return state, fmt.Errorf("%s: ends in a partial statement", name) 173 default: 174 return state, nil 175 } 176 } 177 178 // Exec returns the evaluation of the content of src and an error, if any. 179 // If src contains multiple statements, Exec returns the value of the last one. 180 func (s *Session) Exec(src []byte) ([]reflect.Value, error) { 181 var err error 182 stdout := s.Stdout 183 if stdout == nil { 184 stdout, err = os.Create(os.DevNull) 185 if err != nil { 186 return nil, err 187 } 188 } 189 stderr := s.Stderr 190 if stderr == nil { 191 stdout, err = os.Create(os.DevNull) 192 if err != nil { 193 return nil, err 194 } 195 } 196 197 s.ExecCount++ 198 199 res := s.Parser.ParseLine(src) 200 s.ParserState = res.State 201 202 if len(res.Errs) > 0 { 203 errs := make([]error, len(res.Errs)) 204 for i, err := range res.Errs { 205 errs[i] = err 206 } 207 return nil, Error{Phase: "parser", List: errs} 208 } 209 var out []reflect.Value 210 for _, stmt := range res.Stmts { 211 v, err := s.Program.Eval(stmt, nil) 212 if err != nil { 213 str := err.Error() 214 if strings.HasPrefix(str, "typecheck: ") { // TODO: gross 215 return nil, Error{ 216 Phase: "typecheck", 217 List: []error{ 218 errors.New(strings.TrimPrefix(str, "typecheck: ")), 219 }, 220 } 221 } 222 return nil, Error{Phase: "eval", List: []error{err}} 223 } 224 out = v 225 } 226 for _, cmd := range res.Cmds { 227 j := &shell.Job{ 228 State: s.ShellState, 229 Cmd: cmd, 230 Params: s.Program, 231 Stdin: s.Stdin, 232 Stdout: stdout, 233 Stderr: stderr, 234 } 235 if err := j.Start(); err != nil { 236 fmt.Fprintln(stdout, err) 237 continue 238 } 239 done, err := j.Wait() 240 if err != nil { 241 return nil, Error{Phase: "shell", List: []error{err}} 242 } 243 if !done { 244 break // TODO not right, instead we should just have one cmd, not Cmds here. 245 } 246 } 247 return out, nil 248 } 249 250 // Display displays the results of an execution to w. 251 func (s *Session) Display(w io.Writer, vals []reflect.Value) { 252 if len(vals) > 1 { 253 fmt.Fprint(w, "(") 254 } 255 for i, val := range vals { 256 if i > 0 { 257 fmt.Fprint(w, ", ") 258 } 259 if val == (reflect.Value{}) { 260 fmt.Fprint(w, "<nil>") 261 continue 262 } 263 switch v := val.Interface().(type) { 264 case eval.UntypedInt: 265 fmt.Fprint(w, v.String()) 266 case eval.UntypedFloat: 267 fmt.Fprint(w, v.String()) 268 case eval.UntypedComplex: 269 fmt.Fprint(w, v.String()) 270 case eval.UntypedString: 271 fmt.Fprint(w, v.String) 272 case eval.UntypedRune: 273 fmt.Fprintf(w, "%v", v.Rune) 274 case eval.UntypedBool: 275 fmt.Fprint(w, v.Bool) 276 default: 277 fmt.Fprint(w, format.Debug(v)) 278 } 279 } 280 if len(vals) > 1 { 281 fmt.Fprintln(w, ")") 282 } else if len(vals) == 1 { 283 fmt.Fprintln(w, "") 284 } 285 } 286 287 func (s *Session) Run(ctx context.Context, startInShell bool, sigint chan os.Signal) error { 288 state := parser.StateStmt 289 if startInShell { 290 initFile := filepath.Join(os.Getenv("HOME"), ".ngshinit") 291 if f, err := os.Open(initFile); err == nil { 292 var err error 293 state, err = s.RunScript(f) 294 f.Close() 295 return err 296 } 297 if state == parser.StateStmt { 298 res, err := s.Exec([]byte("$$")) 299 if err != nil { 300 return err 301 } 302 s.Display(s.Stdout, res) 303 state = s.ParserState 304 } 305 } 306 307 s.Liner.SetTabCompletionStyle(liner.TabPrints) 308 s.Liner.SetWordCompleter(s.Completer) 309 s.Liner.SetCtrlCAborts(true) 310 311 if home := os.Getenv("HOME"); home != "" { 312 if s.History.Ng.Name == "" { 313 s.History.Ng.Name = filepath.Join(home, ".ng_history") 314 } 315 if s.History.Sh.Name == "" { 316 s.History.Sh.Name = filepath.Join(home, ".ngsh_history") 317 } 318 } 319 320 s.History.Sh.init("sh", s.Liner) 321 s.History.Ng.init("ng", s.Liner) 322 323 go s.History.Sh.Run(ctx) 324 go s.History.Ng.Run(ctx) 325 326 for { 327 var ( 328 mode string 329 prompt string 330 history chan string 331 ) 332 switch state { 333 case parser.StateUnknown: 334 mode, prompt, history = "ng", "??> ", s.History.Ng.src 335 case parser.StateStmt: 336 mode, prompt, history = "ng", "ng> ", s.History.Ng.src 337 case parser.StateStmtPartial: 338 mode, prompt, history = "ng", "..> ", s.History.Ng.src 339 case parser.StateCmd: 340 mode, prompt, history = "sh", ps1(s.Program.Environ()), s.History.Sh.src 341 case parser.StateCmdPartial: 342 mode, prompt, history = "sh", "..$ ", s.History.Sh.src 343 default: 344 return fmt.Errorf("unkown parser state: %v", state) 345 } 346 s.Liner.SetMode(mode) 347 data, err := s.Liner.Prompt(prompt) 348 if err == liner.ErrPromptAborted { 349 switch state { 350 case parser.StateStmtPartial: 351 fmt.Printf("TODO interrupt partial statement\n") 352 case parser.StateCmdPartial: 353 fmt.Printf("TODO interrupt partial command\n") 354 } 355 } else if err != nil { 356 if err == io.EOF { 357 return nil 358 } 359 return fmt.Errorf("error reading input: %v", err) 360 } 361 if data == "" { 362 continue 363 } 364 s.Liner.AppendHistory(mode, data) 365 history <- data 366 select { // drain sigint 367 case <-sigint: 368 default: 369 } 370 res, err := s.Exec([]byte(data)) 371 if err != nil { 372 fmt.Fprintf(s.Stderr, "%v\n", err) 373 } 374 s.Display(s.Stdout, res) 375 state = s.ParserState 376 } 377 return nil 378 } 379 380 func (s *Session) Close() { 381 s.neugram.mu.Lock() 382 delete(s.neugram.sessions, s.name) 383 s.neugram.mu.Unlock() 384 385 err := s.Liner.Close() 386 if err != nil { 387 panic(err) 388 } 389 s.Parser.Close() 390 // TODO: clean up Program 391 } 392 393 type Error struct { 394 Phase string 395 List []error 396 } 397 398 func (e Error) Error() string { 399 listStr := "" 400 switch len(e.List) { 401 case 0: 402 listStr = "empty error list" 403 case 1: 404 listStr = e.List[0].Error() 405 default: 406 listStr = fmt.Sprintf("%v (and %d more)", e.List[0].Error(), len(e.List)-1) 407 } 408 return fmt.Sprintf("ng: %s: %v", e.Phase, listStr) 409 } 410 411 // History represents a shell (POSIX, Neugram) history. 412 type History struct { 413 Name string // path to the shell's history file 414 src chan string // receives entries to be added to the history file 415 } 416 417 func (h *History) init(mode string, liner *liner.State) { 418 h.src = make(chan string, 1) 419 f, err := os.Open(h.Name) 420 if err != nil { 421 return 422 } 423 defer f.Close() 424 liner.SetMode(mode) 425 liner.ReadHistory(f) 426 f.Close() 427 } 428 429 func (h *History) Run(ctx context.Context) { 430 var batch []string 431 ticker := time.Tick(250 * time.Millisecond) 432 for { 433 select { 434 case line := <-h.src: 435 batch = append(batch, line) 436 case <-ticker: 437 h.append(h.Name, batch) 438 batch = nil 439 case <-ctx.Done(): 440 h.append(h.Name, batch) 441 batch = nil 442 return 443 } 444 } 445 } 446 447 func (h *History) append(dst string, batch []string) { 448 if len(batch) == 0 || dst == "" { 449 return 450 } 451 // TODO: FcntlFlock 452 f, err := os.OpenFile(dst, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0664) 453 if err != nil { 454 return 455 } 456 for _, line := range batch { 457 fmt.Fprintf(f, "%s\n", line) 458 } 459 f.Close() 460 } 461 462 func ps1(env *environ.Environ) string { 463 v := env.Get("PS1") 464 if v == "" { 465 return "ng$ " 466 } 467 if strings.IndexByte(v, '\\') == -1 { 468 return v 469 } 470 var buf []byte 471 for { 472 i := strings.IndexByte(v, '\\') 473 if i == -1 || i == len(v)-1 { 474 break 475 } 476 buf = append(buf, v[:i]...) 477 b := v[i+1] 478 v = v[i+2:] 479 switch b { 480 case 'h', 'H': 481 out, err := exec.Command("hostname").CombinedOutput() 482 if err != nil { 483 fmt.Fprintf(os.Stderr, "ng: %v\n", err) 484 continue 485 } 486 if b == 'h' { 487 if i := bytes.IndexByte(out, '.'); i >= 0 { 488 out = out[:i] 489 } 490 } 491 if len(out) > 0 && out[len(out)-1] == '\n' { 492 out = out[:len(out)-1] 493 } 494 buf = append(buf, out...) 495 case 'n': 496 buf = append(buf, '\n') 497 case 'w', 'W': 498 cwd := env.Get("PWD") 499 if home := env.Get("HOME"); home != "" { 500 cwd = strings.Replace(cwd, home, "~", 1) 501 } 502 if b == 'W' { 503 cwd = filepath.Base(cwd) 504 } 505 buf = append(buf, cwd...) 506 } 507 // TODO: '!', '#', '$', 'nnn', 's', 'j', and more. 508 } 509 buf = append(buf, v...) 510 return string(buf) 511 }