github.com/cayleygraph/cayley@v0.7.7/internal/repl/repl.go (about)

     1  // Copyright 2014 The Cayley Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // +build !appengine
    16  
    17  package repl
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"os/signal"
    25  	"strconv"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/peterh/liner"
    30  
    31  	"github.com/cayleygraph/cayley/clog"
    32  	"github.com/cayleygraph/cayley/graph"
    33  	"github.com/cayleygraph/cayley/query"
    34  	"github.com/cayleygraph/quad/nquads"
    35  )
    36  
    37  func trace(s string) (string, time.Time) {
    38  	return s, time.Now()
    39  }
    40  
    41  func un(s string, startTime time.Time) {
    42  	endTime := time.Now()
    43  
    44  	fmt.Printf(s, float64(endTime.UnixNano()-startTime.UnixNano())/float64(1e6))
    45  }
    46  
    47  func Run(ctx context.Context, qu string, ses query.REPLSession) error {
    48  	nResults := 0
    49  	startTrace, startTime := trace("Elapsed time: %g ms\n\n")
    50  	defer func() {
    51  		if nResults > 0 {
    52  			un(startTrace, startTime)
    53  		}
    54  	}()
    55  	fmt.Printf("\n")
    56  	it, err := ses.Execute(ctx, qu, query.Options{
    57  		Collation: query.REPL,
    58  		Limit:     100,
    59  	})
    60  	if err != nil {
    61  		return err
    62  	}
    63  	defer it.Close()
    64  	for it.Next(ctx) {
    65  		fmt.Print(it.Result())
    66  		nResults++
    67  	}
    68  	if err := it.Err(); err != nil {
    69  		return err
    70  	}
    71  	if nResults > 0 {
    72  		results := "Result"
    73  		if nResults > 1 {
    74  			results += "s"
    75  		}
    76  		fmt.Printf("-----------\n%d %s\n", nResults, results)
    77  	}
    78  	return nil
    79  }
    80  
    81  const (
    82  	defaultLanguage = "gizmo"
    83  
    84  	ps1 = "cayley> "
    85  	ps2 = "...     "
    86  
    87  	history = ".cayley_history"
    88  )
    89  
    90  func Repl(ctx context.Context, h *graph.Handle, queryLanguage string, timeout time.Duration) error {
    91  	if queryLanguage == "" {
    92  		queryLanguage = defaultLanguage
    93  	}
    94  	l := query.GetLanguage(queryLanguage)
    95  	if l == nil || l.REPL == nil {
    96  		return fmt.Errorf("unsupported query language: %q", queryLanguage)
    97  	}
    98  	ses := l.REPL(h.QuadStore)
    99  
   100  	term, err := terminal(history)
   101  	if os.IsNotExist(err) {
   102  		fmt.Printf("creating new history file: %q\n", history)
   103  	}
   104  	defer persist(term, history)
   105  
   106  	var (
   107  		prompt = ps1
   108  
   109  		code string
   110  	)
   111  
   112  	newCtx := func() (context.Context, func()) { return ctx, func() {} }
   113  	if timeout > 0 {
   114  		newCtx = func() (context.Context, func()) { return context.WithTimeout(ctx, timeout) }
   115  	}
   116  
   117  	for {
   118  		select {
   119  		case <-ctx.Done():
   120  			return ctx.Err()
   121  		default:
   122  		}
   123  		if len(code) == 0 {
   124  			prompt = ps1
   125  		} else {
   126  			prompt = ps2
   127  		}
   128  		line, err := term.Prompt(prompt)
   129  		if err != nil {
   130  			if err == io.EOF {
   131  				fmt.Println()
   132  				return nil
   133  			}
   134  			return err
   135  		}
   136  
   137  		term.AppendHistory(line)
   138  
   139  		line = strings.TrimSpace(line)
   140  		if len(line) == 0 || line[0] == '#' {
   141  			continue
   142  		}
   143  
   144  		if code == "" {
   145  			cmd, args := splitLine(line)
   146  
   147  			switch cmd {
   148  			case ":debug":
   149  				args = strings.TrimSpace(args)
   150  				var debug bool
   151  				switch args {
   152  				case "t":
   153  					debug = true
   154  				case "f":
   155  					// Do nothing.
   156  				default:
   157  					debug, err = strconv.ParseBool(args)
   158  					if err != nil {
   159  						fmt.Printf("Error: cannot parse %q as a valid boolean - acceptable values: 't'|'true' or 'f'|'false'\n", args)
   160  						continue
   161  					}
   162  				}
   163  				if debug {
   164  					clog.SetV(2)
   165  				} else {
   166  					clog.SetV(0)
   167  				}
   168  				fmt.Printf("Debug set to %t\n", debug)
   169  				continue
   170  
   171  			case ":a":
   172  				quad, err := nquads.Parse(args)
   173  				if err == nil {
   174  					err = h.QuadWriter.AddQuad(quad)
   175  				}
   176  				if err != nil {
   177  					fmt.Printf("Error: not a valid quad: %v\n", err)
   178  					continue
   179  				}
   180  				continue
   181  
   182  			case ":d":
   183  				quad, err := nquads.Parse(args)
   184  				if err != nil {
   185  					fmt.Printf("Error: not a valid quad: %v\n", err)
   186  					continue
   187  				}
   188  				err = h.QuadWriter.RemoveQuad(quad)
   189  				if err != nil {
   190  					fmt.Printf("error deleting: %v\n", err)
   191  				}
   192  				continue
   193  
   194  			case "help":
   195  				fmt.Printf("Help\n\texit // Exit\n\thelp // this help\n\td: <quad> // delete quad\n\ta: <quad> // add quad\n\t:debug [t|f]\n")
   196  				continue
   197  
   198  			case "exit":
   199  				term.Close()
   200  				os.Exit(0)
   201  
   202  			default:
   203  				if cmd[0] == ':' {
   204  					fmt.Printf("Unknown command: %q\n", cmd)
   205  					continue
   206  				}
   207  			}
   208  		}
   209  
   210  		code += line
   211  
   212  		nctx, cancel := newCtx()
   213  		err = Run(nctx, code, ses)
   214  		cancel()
   215  		if err == query.ErrParseMore {
   216  			// collect more input
   217  		} else if err != nil {
   218  			fmt.Println("Error: ", err)
   219  			code = ""
   220  		} else {
   221  			code = ""
   222  		}
   223  	}
   224  }
   225  
   226  // Splits a line into a command and its arguments
   227  // e.g. ":a b c d ." will be split into ":a" and " b c d ."
   228  func splitLine(line string) (string, string) {
   229  	var command, arguments string
   230  
   231  	line = strings.TrimSpace(line)
   232  
   233  	// An empty line/a line consisting of whitespace contains neither command nor arguments
   234  	if len(line) > 0 {
   235  		command = strings.Fields(line)[0]
   236  
   237  		// A line containing only a command has no arguments
   238  		if len(line) > len(command) {
   239  			arguments = line[len(command):]
   240  		}
   241  	}
   242  
   243  	return command, arguments
   244  }
   245  
   246  func terminal(path string) (*liner.State, error) {
   247  	term := liner.NewLiner()
   248  
   249  	go func() {
   250  		c := make(chan os.Signal, 1)
   251  		signal.Notify(c, os.Interrupt, os.Kill)
   252  		<-c
   253  
   254  		err := persist(term, history)
   255  		if err != nil {
   256  			fmt.Fprintf(os.Stderr, "failed to properly clean up terminal: %v\n", err)
   257  			os.Exit(1)
   258  		}
   259  
   260  		os.Exit(0)
   261  	}()
   262  
   263  	f, err := os.Open(path)
   264  	if err != nil {
   265  		return term, err
   266  	}
   267  	defer f.Close()
   268  	_, err = term.ReadHistory(f)
   269  	return term, err
   270  }
   271  
   272  func persist(term *liner.State, path string) error {
   273  	f, err := os.OpenFile(path, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
   274  	if err != nil {
   275  		return fmt.Errorf("could not open %q to append history: %v", path, err)
   276  	}
   277  	defer f.Close()
   278  	_, err = term.WriteHistory(f)
   279  	if err != nil {
   280  		return fmt.Errorf("could not write history to %q: %v", path, err)
   281  	}
   282  	return term.Close()
   283  }