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  }