github.com/NeowayLabs/nash@v0.2.2-0.20200127205349-a227041ffd50/cmd/nash/cli.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 "strings" 9 10 "github.com/madlambda/nash" 11 "github.com/madlambda/nash/ast" 12 "github.com/madlambda/nash/parser" 13 "github.com/madlambda/nash/sh" 14 "github.com/chzyer/readline" 15 ) 16 17 type ( 18 Interrupted interface { 19 Interrupted() bool 20 } 21 22 Ignored interface { 23 Ignore() bool 24 } 25 26 BlockNotFinished interface { 27 Unfinished() bool 28 } 29 ) 30 31 var completers = []readline.PrefixCompleterInterface{} 32 33 func execFn(shell *nash.Shell, fnDef sh.FnDef, args []sh.Obj) { 34 fn := fnDef.Build() 35 err := fn.SetArgs(args) 36 if err != nil { 37 fmt.Fprintf(os.Stderr, "%s failed: %s\n", fnDef.Name(), err.Error()) 38 } 39 fn.SetStdin(shell.Stdin()) 40 fn.SetStdout(shell.Stdout()) 41 fn.SetStderr(shell.Stderr()) 42 43 if err := fn.Start(); err != nil { 44 fmt.Fprintf(os.Stderr, "%s failed: %s\n", fnDef.Name(), err.Error()) 45 return 46 } 47 48 if err := fn.Wait(); err != nil { 49 fmt.Fprintf(os.Stderr, "%s failed: %s\n", fnDef.Name(), err.Error()) 50 return 51 } 52 } 53 54 func importInitFile(shell *nash.Shell, initFile string) (bool, error) { 55 if d, err := os.Stat(initFile); err == nil { 56 if m := d.Mode(); !m.IsDir() { 57 err := shell.ExecuteString("init", 58 fmt.Sprintf("import %q", initFile)) 59 if err != nil { 60 return false, fmt.Errorf("Failed to evaluate '%s': %s", initFile, err.Error()) 61 } 62 return true, nil 63 } 64 } 65 return false, nil 66 } 67 68 func loadInit(shell *nash.Shell) error { 69 70 if noInit { 71 return nil 72 } 73 74 initFiles := []string{ 75 shell.NashPath() + "/init", 76 shell.NashPath() + "/init.sh", 77 } 78 79 for _, init := range initFiles { 80 imported, err := importInitFile(shell, init) 81 if err != nil { 82 return err 83 } 84 if imported { 85 break 86 } 87 } 88 89 return nil 90 } 91 92 func cli(shell *nash.Shell) error { 93 94 shell.SetInteractive(true) 95 96 if err := loadInit(shell); err != nil { 97 fmt.Fprintf(os.Stderr, "error loading init file:\n%s\n", err) 98 } 99 100 historyFile := shell.NashPath() + "/history" 101 cfg := readline.Config{ 102 Prompt: shell.Prompt(), 103 HistoryFile: historyFile, 104 InterruptPrompt: "^C", 105 EOFPrompt: "exit", 106 } 107 108 term, err := readline.NewTerminal(&cfg) 109 if err != nil { 110 return err 111 } 112 113 op := term.Readline() 114 rline := &readline.Instance{ 115 Config: &cfg, 116 Terminal: term, 117 Operation: op, 118 } 119 120 defer rline.Close() 121 122 completer := NewCompleter(op, term, shell) 123 cfg.AutoComplete = completer 124 125 if lineMode, ok := shell.Getvar("LINEMODE"); ok { 126 if lineStr, ok := lineMode.(*sh.StrObj); ok && lineStr.Str() == "vim" { 127 rline.SetVimMode(true) 128 } else { 129 rline.SetVimMode(false) 130 } 131 } 132 133 return docli(shell, rline) 134 } 135 136 func docli(shell *nash.Shell, rline *readline.Instance) error { 137 var ( 138 content bytes.Buffer 139 lineidx int 140 line string 141 parse *parser.Parser 142 tr *ast.Tree 143 err error 144 unfinished bool 145 prompt string 146 ) 147 148 for { 149 if fnDef, err := shell.GetFn("nash_repl_before"); err == nil && !unfinished { 150 execFn(shell, fnDef, nil) 151 } 152 153 if !unfinished { 154 prompt = shell.Prompt() 155 } 156 157 rline.SetPrompt(prompt) 158 line, err = rline.Readline() 159 160 if err == readline.ErrInterrupt { 161 goto cont 162 } else if err == io.EOF { 163 err = nil 164 break 165 } 166 167 lineidx++ 168 line = strings.TrimSpace(line) 169 170 // handle special cli commands 171 switch { 172 case strings.HasPrefix(line, "set mode "): 173 switch line[9:] { 174 case "vi": 175 rline.SetVimMode(true) 176 case "emacs": 177 rline.SetVimMode(false) 178 default: 179 fmt.Printf("invalid mode: %s\n", line[9:]) 180 } 181 182 goto cont 183 case line == "mode": 184 if rline.IsVimMode() { 185 fmt.Printf("Current mode: vim\n") 186 } else { 187 fmt.Printf("Current mode: emacs\n") 188 } 189 190 goto cont 191 case line == "exit": 192 break 193 } 194 195 content.Write([]byte(line + "\n")) 196 parse = parser.NewParser(fmt.Sprintf("<stdin line %d>", lineidx), string(content.Bytes())) 197 line = string(content.Bytes()) 198 199 tr, err = parse.Parse() 200 if err != nil { 201 if interrupted, ok := err.(Interrupted); ok && interrupted.Interrupted() { 202 content.Reset() 203 goto cont 204 } else if errBlock, ok := err.(BlockNotFinished); ok && errBlock.Unfinished() { 205 prompt = ">>> " 206 unfinished = true 207 goto cont 208 } 209 210 fmt.Printf("ERROR: %s\n", err.Error()) 211 content.Reset() 212 goto cont 213 } 214 215 unfinished = false 216 content.Reset() 217 218 _, err = shell.ExecuteTree(tr) 219 if err != nil { 220 fmt.Printf("ERROR: %s\n", err.Error()) 221 } 222 223 cont: 224 if fnDef, err := shell.GetFn("nash_repl_after"); err == nil && !unfinished { 225 var status sh.Obj 226 var ok bool 227 228 if status, ok = shell.Getvar("status"); !ok { 229 status = sh.NewStrObj("") 230 } 231 232 execFn(shell, fnDef, []sh.Obj{sh.NewStrObj(line), status}) 233 } 234 235 rline.SetPrompt(prompt) 236 } 237 238 return nil 239 }