github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/gnovm/cmd/gno/repl.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "context" 6 "errors" 7 "flag" 8 "fmt" 9 "go/scanner" 10 "os" 11 "strings" 12 13 "github.com/gnolang/gno/gnovm/pkg/gnoenv" 14 "github.com/gnolang/gno/gnovm/pkg/repl" 15 "github.com/gnolang/gno/tm2/pkg/commands" 16 ) 17 18 type replCfg struct { 19 rootDir string 20 initialCommand string 21 skipUsage bool 22 } 23 24 func newReplCmd() *commands.Command { 25 cfg := &replCfg{} 26 27 return commands.NewCommand( 28 commands.Metadata{ 29 Name: "repl", 30 ShortUsage: "repl [flags]", 31 ShortHelp: "starts a GnoVM REPL", 32 }, 33 cfg, 34 func(_ context.Context, args []string) error { 35 return execRepl(cfg, args) 36 }, 37 ) 38 } 39 40 func (c *replCfg) RegisterFlags(fs *flag.FlagSet) { 41 fs.StringVar( 42 &c.rootDir, 43 "root-dir", 44 "", 45 "clone location of github.com/gnolang/gno (gno tries to guess it)", 46 ) 47 48 fs.StringVar( 49 &c.initialCommand, 50 "command", 51 "", 52 "initial command to run", 53 ) 54 55 fs.BoolVar( 56 &c.skipUsage, 57 "skip-usage", 58 false, 59 "do not print usage", 60 ) 61 } 62 63 func execRepl(cfg *replCfg, args []string) error { 64 if len(args) > 0 { 65 return flag.ErrHelp 66 } 67 68 if cfg.rootDir == "" { 69 cfg.rootDir = gnoenv.RootDir() 70 } 71 72 if !cfg.skipUsage { 73 fmt.Fprint(os.Stderr, `// Usage: 74 // gno> import "gno.land/p/demo/avl" // import the p/demo/avl package 75 // gno> func a() string { return "a" } // declare a new function named a 76 // gno> /src // print current generated source 77 // gno> /editor // enter in multi-line mode, end with ';' 78 // gno> /reset // remove all previously inserted code 79 // gno> println(a()) // print the result of calling a() 80 // gno> /exit // alternative to <Ctrl-D> 81 `) 82 } 83 84 return runRepl(cfg) 85 } 86 87 func runRepl(cfg *replCfg) error { 88 r := repl.NewRepl() 89 90 if cfg.initialCommand != "" { 91 handleInput(r, cfg.initialCommand) 92 } 93 94 fmt.Fprint(os.Stdout, "gno> ") 95 96 inEdit := false 97 prev := "" 98 liner := bufio.NewScanner(os.Stdin) 99 100 for liner.Scan() { 101 line := liner.Text() 102 103 if l := strings.TrimSpace(line); l == ";" { 104 line, inEdit = "", false 105 } else if l == "/editor" { 106 line, inEdit = "", true 107 fmt.Fprintln(os.Stdout, "// enter a single ';' to quit and commit") 108 } 109 if prev != "" { 110 line = prev + "\n" + line 111 prev = "" 112 } 113 if inEdit { 114 fmt.Fprint(os.Stdout, "... ") 115 prev = line 116 continue 117 } 118 119 if err := handleInput(r, line); err != nil { 120 var goScanError scanner.ErrorList 121 if errors.As(err, &goScanError) { 122 // We assune that a Go scanner error indicates an incomplete Go statement. 123 // Append next line and retry. 124 prev = line 125 } else { 126 fmt.Fprintln(os.Stderr, err) 127 } 128 } 129 130 if prev == "" { 131 fmt.Fprint(os.Stdout, "gno> ") 132 } else { 133 fmt.Fprint(os.Stdout, "... ") 134 } 135 } 136 return nil 137 } 138 139 // handleInput executes specific "/" commands, or evaluates input as Gno source code. 140 func handleInput(r *repl.Repl, input string) error { 141 switch strings.TrimSpace(input) { 142 case "/reset": 143 r.Reset() 144 case "/src": 145 fmt.Fprintln(os.Stdout, r.Src()) 146 case "/exit": 147 os.Exit(0) 148 case "": 149 // Avoid to increase the repl execution counter if no input. 150 default: 151 out, err := r.Process(input) 152 if err != nil { 153 return err 154 } 155 fmt.Fprintln(os.Stdout, out) 156 } 157 return nil 158 }