1/* 2 * gomacro - A Go interpreter with Lisp-like macros 3 * 4 * Copyright (C) 2018-2019 Massimiliano Ghilardi 5 * 6 * This Source Code Form is subject to the terms of the Mozilla Public 7 * License, v. 2.0. If a copy of the MPL was not distributed with this 8 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * 11 * cmd.go 12 * 13 * Created on: Apr 20, 2018 14 * Author: Massimiliano Ghilardi 15 */ 16 17package fast 18 19import ( 20 "errors" 21 "io" 22 "sort" 23 "strings" 24 25 "github.com/cosmos72/gomacro/base/paths" 26 27 "github.com/cosmos72/gomacro/base" 28 bstrings "github.com/cosmos72/gomacro/base/strings" 29) 30 31// ====================== Cmd ============================== 32 33// Cmd is an interpreter special command. 34// 35// The following Interp methods look for special commands and execute them: 36// Cmd, EvalFile, EvalReader, ParseEvalPrint, ReadParseEvalPrint, Repl, ReplStdin 37// note that Interp.Eval() does **not** look for special commands! 38// 39// Cmd.Name is the command name **without** the initial ':' 40// it must be a valid Go identifier and must not be empty. 41// Using a reserved Go keyword (const, for, func, if, package, return, switch, type, var...) 42// or predefined identifier (bool, int, rune, true, false, nil...) 43// is a bad idea because it interferes with gomacro preprocessor mode. 44// Current limitation: Cmd.Name[0] must be ASCII. 45// 46// Cmd.Help is the help string that will be displayed by :help 47// please look at current :help output and use the same layout if possible. 48// 49// Cmd.Func is the command implementation. it receives as arguments: 50// - the current Interp object, 51// - the (possibly multi-line) argument string typed by the user 52// note: it will always have balanced amounts of {} [] () '' "" and `` 53// - the current command options 54// 55// Cmd.Func can perform any action desired by the implementor, 56// including calls to Interp methods, and it must return: 57// - a string to be subsequently evaluated by the interpreter. 58// return the empty string if the command does not need any subsequent evaluation, 59// or if it performed the evaluation by itself. 60// - the updated command options. 61// return the received 'opt' argument unless you need to update it. 62// 63// If Cmd.Func needs to print something, it's recommended to use 64// g := &interp.Comp.Globals 65// g.Fprintf(g.Stdout, FORMAT, ARGS...) 66// instead of the various fmt.*Print* functions, in order to 67// pretty-print interpreter-generated objects (g.Fprintf) 68// and to honour configured redirections (g.Stdout) 69// 70// To register a new special command, use Commands.Add() 71// To unregister an existing special command, use Commands.Del() 72// To list existing special commands, use Commands.List() 73type Cmd struct { 74 Name string 75 Func func(interp *Interp, arg string, opt base.CmdOpt) (string, base.CmdOpt) 76 Help string 77} 78 79// if cmd.Name starts with prefix return 0; 80// else if cmd.Name < prefix return -1; 81// else return 1 82func (cmd *Cmd) Match(prefix string) int { 83 name := cmd.Name 84 if strings.HasPrefix(name, prefix) { 85 return 0 86 } else if name < prefix { 87 return -1 88 } else { 89 return 1 90 } 91} 92 93func (cmd *Cmd) ShowHelp(g *base.Globals) { 94 c := string(g.ReplCmdChar) 95 96 help := strings.Replace(cmd.Help, "%c", c, -1) 97 g.Fprintf(g.Stdout, "%s%s\n", c, help) 98} 99 100// ===================== Cmds ============================== 101 102type Cmds struct { 103 m map[byte][]Cmd 104} 105 106// search for a Cmd whose name starts with prefix. 107// return (zero value, io.EOF) if no match. 108// return (cmd, nil) if exactly one match. 109// return (zero value, list of match names) if more than one match 110func (cmds Cmds) Lookup(prefix string) (Cmd, error) { 111 if len(prefix) != 0 { 112 if vec, ok := cmds.m[prefix[0]]; ok { 113 i, err := prefixSearch(vec, prefix) 114 if err != nil { 115 return Cmd{}, err 116 } 117 return vec[i], nil 118 } 119 } 120 return Cmd{}, io.EOF 121} 122 123// prefix search: find all the Cmds whose name start with prefix. 124// if there are none, return 0 and io.EOF 125// if there is exactly one, return its index and nil. 126// if there is more than one, return 0 and an error listing the matching ones 127func prefixSearch(vec []Cmd, prefix string) (int, error) { 128 lo, _ := binarySearch(vec, prefix) 129 n := len(vec) 130 for ; lo < n; lo++ { 131 cmp := vec[lo].Match(prefix) 132 if cmp < 0 { 133 continue 134 } else if cmp == 0 { 135 break 136 } else { 137 return 0, io.EOF 138 } 139 } 140 if lo == n { 141 return 0, io.EOF 142 } 143 hi := lo + 1 144 for ; hi < n; hi++ { 145 if vec[hi].Match(prefix) > 0 { 146 break 147 } 148 } 149 if lo+1 == hi { 150 return lo, nil 151 } 152 names := make([]string, hi-lo) 153 for i := lo; i < hi; i++ { 154 names[i-lo] = vec[i].Name 155 } 156 return 0, errors.New(strings.Join(names, " ")) 157} 158 159// plain binary search for exact Cmd name 160func binarySearch(vec []Cmd, exact string) (int, bool) { 161 lo, hi := 0, len(vec)-1 162 for lo <= hi { 163 mid := (lo + hi) / 2 164 name := vec[mid].Name 165 if name < exact { 166 lo = mid + 1 167 } else if name > exact { 168 hi = mid - 1 169 } else { 170 return mid, true 171 } 172 } 173 return lo, false 174} 175 176// return the list of currently registered special commands 177func (cmds Cmds) List() []Cmd { 178 var list []Cmd 179 for _, vec := range cmds.m { 180 for _, cmd := range vec { 181 list = append(list, cmd) 182 } 183 } 184 sortCmdList(list) 185 return list 186} 187 188// order Cmd list by name 189func sortCmdList(vec []Cmd) { 190 sort.Slice(vec, func(i, j int) bool { 191 return vec[i].Name < vec[j].Name 192 }) 193} 194 195// register a new Cmd. 196// if cmd.Name is the empty string, do nothing and return false. 197// overwrites any existing Cmd with the same name 198func (cmds Cmds) Add(cmd Cmd) bool { 199 name := cmd.Name 200 if len(name) == 0 { 201 return false 202 } 203 c := name[0] 204 vec, _ := cmds.m[c] 205 if pos, ok := binarySearch(vec, name); ok { 206 vec[pos] = cmd 207 } else { 208 vec = append(vec, cmd) 209 sortCmdList(vec) 210 cmds.m[c] = vec 211 } 212 return true 213} 214 215// unregister an existing Cmd by name. return true if existed. 216// Use with care! 217func (cmds Cmds) Del(name string) bool { 218 if len(name) != 0 { 219 c := name[0] 220 if vec, ok := cmds.m[c]; ok { 221 if pos, ok := binarySearch(vec, name); ok { 222 vec = removeCmd(vec, pos) 223 if len(vec) == 0 { 224 delete(cmds.m, c) 225 } else { 226 cmds.m[c] = vec 227 } 228 return true 229 } 230 } 231 } 232 return false 233} 234 235// remove Cmd at index 'pos' from slice. 236// return updated slice. 237func removeCmd(vec []Cmd, pos int) []Cmd { 238 head := vec[:pos] 239 n := len(vec) 240 if pos == n-1 { 241 return head 242 } 243 tail := vec[pos+1:] 244 if pos == 0 { 245 return tail 246 } 247 headn, tailn := pos, len(tail) 248 if headn >= tailn { 249 copy(vec[headn:], tail) 250 vec = vec[:n-1] 251 } else { 252 copy(vec[1:], head) 253 vec = vec[1:] 254 } 255 return vec 256} 257 258func (cmds Cmds) ShowHelp(g *base.Globals) { 259 out := g.Stdout 260 g.Fprintf(out, "%s", 261 "// type Go code to execute it. example: func add(x, y int) int { return x + y }\n\n// interpreter commands:\n") 262 263 for _, cmd := range cmds.List() { 264 cmd.ShowHelp(g) 265 } 266 g.Fprintf(out, "%s", "// abbreviations are allowed if unambiguous.\n") 267} 268 269var Commands Cmds 270 271func init() { 272 Commands.m = map[byte][]Cmd{ 273 'd': []Cmd{{"debug", (*Interp).cmdDebug, `debug EXPR debug expression or statement interactively`}}, 274 'e': []Cmd{{"env", (*Interp).cmdEnv, `env [NAME] show available functions, variables and constants 275 in current package, or from imported package NAME`}}, 276 'h': []Cmd{{"help", (*Interp).cmdHelp, `help show this help`}}, 277 'i': []Cmd{{"inspect", (*Interp).cmdInspect, `inspect EXPR inspect expression interactively`}}, 278 'o': []Cmd{{"options", (*Interp).cmdOptions, `options [OPTS] show or toggle interpreter options`}}, 279 'p': []Cmd{{"package", (*Interp).cmdPackage, `package "PKGPATH" switch to package PKGPATH, importing it if possible`}}, 280 'q': []Cmd{{"quit", (*Interp).cmdQuit, `quit quit the interpreter`}}, 281 'u': []Cmd{{"unload", (*Interp).cmdUnload, `unload "PKGPATH" remove package PKGPATH from the list of known packages. 282 later attempts to import it will trigger a recompile`}}, 283 'w': []Cmd{{"write", (*Interp).cmdWrite, `write [FILE] write collected declarations and/or statements to standard output or to FILE 284 use %copt Declarations and/or %copt Statements to start collecting them`}}, 285 } 286} 287 288// ==================== Interp ============================= 289 290// execute one of the REPL commands starting with ':' 291// return any remainder string to be evaluated, and the options to evaluate it 292func (ir *Interp) Cmd(src string) (string, base.CmdOpt) { 293 g := &ir.Comp.Globals 294 var opt base.CmdOpt 295 296 trim := strings.TrimSpace(src) 297 n := len(trim) 298 if n > 0 && trim[0] == g.ReplCmdChar { 299 prefix, arg := bstrings.Split2(trim[1:], ' ') // skip g.ReplCmdChar 300 cmd, err := Commands.Lookup(prefix) 301 if err == nil { 302 src, opt = cmd.Func(ir, arg, opt) 303 } else if err == io.EOF { 304 // ":<something>" 305 // temporarily disable collection of declarations and statements, 306 // and temporarily disable macroexpandonly (i.e. re-enable eval) 307 opt |= base.CmdOptForceEval 308 src = " " + src[1:] // slower than src = src[1:], but gives accurate column positions in error messages 309 } else { 310 g.Warnf("ambiguous command %q matches: %s", prefix, err) 311 return "", opt 312 } 313 } else if g.Options&base.OptMacroExpandOnly == 0 && (trim == "package" || strings.HasPrefix(trim, "package ")) { 314 _, arg := bstrings.Split2(trim, ' ') 315 src, opt = ir.cmdPackage(arg, opt) 316 } 317 return src, opt 318} 319 320func (ir *Interp) cmdDebug(arg string, opt base.CmdOpt) (string, base.CmdOpt) { 321 g := &ir.Comp.Globals 322 if len(arg) == 0 { 323 g.Fprintf(g.Stdout, "// debug: missing argument\n") 324 } else { 325 g.Print(ir.Debug(arg)) 326 } 327 return "", opt 328} 329 330func (ir *Interp) cmdEnv(arg string, opt base.CmdOpt) (string, base.CmdOpt) { 331 ir.ShowPackage(arg) 332 return "", opt 333} 334 335func (ir *Interp) cmdHelp(arg string, opt base.CmdOpt) (string, base.CmdOpt) { 336 Commands.ShowHelp(&ir.Comp.Globals) 337 return "", opt 338} 339 340func (ir *Interp) cmdInspect(arg string, opt base.CmdOpt) (string, base.CmdOpt) { 341 g := &ir.Comp.Globals 342 if len(arg) == 0 { 343 g.Fprintf(g.Stdout, "// inspect: missing argument\n") 344 } else { 345 ir.Inspect(arg) 346 } 347 return "", opt 348} 349 350func (ir *Interp) cmdOptions(arg string, opt base.CmdOpt) (string, base.CmdOpt) { 351 c := ir.Comp 352 g := &c.Globals 353 354 if len(arg) != 0 { 355 g.Options ^= base.ParseOptions(arg) 356 357 debugdepth := 0 358 if g.Options&base.OptDebugFromReflect != 0 { 359 debugdepth = 1 360 } 361 c.CompGlobals.Universe.DebugDepth = debugdepth 362 363 } else { 364 g.Fprintf(g.Stdout, "// current options: %v\n", g.Options) 365 g.Fprintf(g.Stdout, "// unset options: %v\n", ^g.Options) 366 } 367 return "", opt 368} 369 370// change package. pkgpath can be empty or a package path WITH quotes 371// 'package NAME' where NAME is without quotes has no effect. 372func (ir *Interp) cmdPackage(path string, cmdopt base.CmdOpt) (string, base.CmdOpt) { 373 c := ir.Comp 374 g := &c.Globals 375 path = strings.TrimSpace(path) 376 n := len(path) 377 if len(path) == 0 { 378 g.Fprintf(g.Stdout, "// current package: %s %q\n", c.Name, c.Path) 379 } else if n > 2 && path[0] == '"' && path[n-1] == '"' { 380 path = path[1 : n-1] 381 ir.ChangePackage(paths.FileName(path), path) 382 } else if g.Options&base.OptShowPrompt != 0 { 383 g.Debugf(`package %s has no effect. To switch to a different package, use package "PACKAGE/FULL/PATH" - note the quotes`, path) 384 } 385 return "", cmdopt 386} 387 388func (ir *Interp) cmdQuit(_ string, opt base.CmdOpt) (string, base.CmdOpt) { 389 return "", opt | base.CmdOptQuit 390} 391 392// remove package 'path' from the list of known packages 393func (ir *Interp) cmdUnload(path string, opt base.CmdOpt) (string, base.CmdOpt) { 394 if len(path) != 0 { 395 ir.Comp.UnloadPackage(path) 396 } 397 return "", opt 398} 399 400func (ir *Interp) cmdWrite(filepath string, opt base.CmdOpt) (string, base.CmdOpt) { 401 g := &ir.Comp.Globals 402 if len(filepath) == 0 { 403 g.WriteDeclsToStream(g.Stdout) 404 } else { 405 g.WriteDeclsToFile(filepath) 406 } 407 return "", opt 408} 409