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 }