repl.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. package engine
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "os"
  7. "path/filepath"
  8. "runtime"
  9. "strings"
  10. "github.com/chzyer/readline"
  11. "github.com/mitchellh/go-homedir"
  12. log "github.com/sirupsen/logrus"
  13. "github.com/xyproto/algernon/lua/codelib"
  14. "github.com/xyproto/algernon/lua/convert"
  15. "github.com/xyproto/algernon/lua/datastruct"
  16. "github.com/xyproto/algernon/lua/jnode"
  17. "github.com/xyproto/algernon/lua/pure"
  18. "github.com/xyproto/ask"
  19. "github.com/xyproto/env/v2"
  20. lua "github.com/xyproto/gopher-lua"
  21. "github.com/xyproto/textoutput"
  22. )
  23. const exitMessage = "bye"
  24. // Export Lua functions specific to the REPL
  25. func exportREPLSpecific(L *lua.LState) {
  26. // Attempt to return a more informative text than the memory location.
  27. // Can take several arguments, just like print().
  28. L.SetGlobal("pprint", L.NewFunction(func(L *lua.LState) int {
  29. var buf bytes.Buffer
  30. top := L.GetTop()
  31. for i := 1; i <= top; i++ {
  32. convert.PprintToWriter(&buf, L.Get(i))
  33. if i != top {
  34. buf.WriteString("\t")
  35. }
  36. }
  37. // Output the combined text
  38. fmt.Println(buf.String())
  39. return 0 // number of results
  40. }))
  41. // Get the current directory since this is probably in the REPL
  42. L.SetGlobal("scriptdir", L.NewFunction(func(L *lua.LState) int {
  43. scriptpath, err := os.Getwd()
  44. if err != nil {
  45. log.Error(err)
  46. L.Push(lua.LString("."))
  47. return 1 // number of results
  48. }
  49. top := L.GetTop()
  50. if top == 1 {
  51. // Also include a separator and a filename
  52. fn := L.ToString(1)
  53. scriptpath = filepath.Join(scriptpath, fn)
  54. }
  55. // Now have the correct absolute scriptpath
  56. L.Push(lua.LString(scriptpath))
  57. return 1 // number of results
  58. }))
  59. }
  60. // Split the given line in three parts, and color the parts
  61. func colorSplit(line, sep string, colorFunc1, colorFuncSep, colorFunc2 func(string) string, reverse bool) (string, string) {
  62. if strings.Contains(line, sep) {
  63. fields := strings.SplitN(line, sep, 2)
  64. s1 := ""
  65. if colorFunc1 != nil {
  66. s1 += colorFunc1(fields[0])
  67. } else {
  68. s1 += fields[0]
  69. }
  70. s2 := ""
  71. if colorFunc2 != nil {
  72. s2 += colorFuncSep(sep) + colorFunc2(fields[1])
  73. } else {
  74. s2 += sep + fields[1]
  75. }
  76. return s1, s2
  77. }
  78. if reverse {
  79. return "", line
  80. }
  81. return line, ""
  82. }
  83. // Syntax highlight the given line
  84. func highlight(o *textoutput.TextOutput, line string) string {
  85. unprocessed := line
  86. unprocessed, comment := colorSplit(unprocessed, "//", nil, o.DarkGray, o.DarkGray, false)
  87. module, unprocessed := colorSplit(unprocessed, ":", o.LightGreen, o.DarkRed, nil, true)
  88. function := ""
  89. if unprocessed != "" {
  90. // Green function names
  91. if strings.Contains(unprocessed, "(") {
  92. fields := strings.SplitN(unprocessed, "(", 2)
  93. function = o.LightGreen(fields[0])
  94. unprocessed = "(" + fields[1]
  95. } else if strings.Contains(unprocessed, "|") {
  96. unprocessed = "<magenta>" + strings.ReplaceAll(unprocessed, "|", "<white>|</white><magenta>") + "</magenta>"
  97. }
  98. }
  99. unprocessed, typed := colorSplit(unprocessed, "->", nil, o.LightBlue, o.DarkRed, false)
  100. unprocessed = strings.ReplaceAll(unprocessed, "string", o.LightBlue("string"))
  101. unprocessed = strings.ReplaceAll(unprocessed, "number", o.LightYellow("number"))
  102. unprocessed = strings.ReplaceAll(unprocessed, "function", o.LightCyan("function"))
  103. return module + function + unprocessed + typed + comment
  104. }
  105. // Output syntax highlighted help text, with an additional usage message
  106. func outputHelp(o *textoutput.TextOutput, helpText string) {
  107. for _, line := range strings.Split(helpText, "\n") {
  108. o.Println(highlight(o, line))
  109. }
  110. o.Println(usageMessage)
  111. }
  112. // Output syntax highlighted help about a specific topic or function
  113. func outputHelpAbout(o *textoutput.TextOutput, helpText, topic string) {
  114. switch topic {
  115. case "help":
  116. o.Println(o.DarkGray("Output general help or help about a specific topic."))
  117. return
  118. case "webhelp":
  119. o.Println(o.DarkGray("Output help about web-related functions."))
  120. return
  121. case "confighelp":
  122. o.Println(o.DarkGray("Output help about configuration-related functions."))
  123. return
  124. case "quit", "exit", "shutdown", "halt":
  125. o.Println(o.DarkGray("Quit Algernon."))
  126. return
  127. }
  128. comment := ""
  129. for _, line := range strings.Split(helpText, "\n") {
  130. if strings.HasPrefix(line, topic) {
  131. // Output help text, with some surrounding blank lines
  132. o.Println("\n" + highlight(o, line))
  133. o.Println("\n" + o.DarkGray(strings.TrimSpace(comment)) + "\n")
  134. return
  135. }
  136. // Gather comments until a non-comment is encountered
  137. if strings.HasPrefix(line, "//") {
  138. comment += strings.TrimSpace(line[2:]) + "\n"
  139. } else {
  140. comment = ""
  141. }
  142. }
  143. o.Println(o.DarkGray("Found no help for: ") + o.White(topic))
  144. }
  145. // Take all functions mentioned in the given help text string and add them to the readline completer
  146. func addFunctionsFromHelptextToCompleter(helpText string, completer *readline.PrefixCompleter) {
  147. for _, line := range strings.Split(helpText, "\n") {
  148. if !strings.HasPrefix(line, "//") && strings.Contains(line, "(") {
  149. parts := strings.Split(line, "(")
  150. if strings.Contains(line, "()") {
  151. completer.Children = append(completer.Children, &readline.PrefixCompleter{Name: []rune(parts[0] + "()")})
  152. } else {
  153. completer.Children = append(completer.Children, &readline.PrefixCompleter{Name: []rune(parts[0] + "(")})
  154. }
  155. }
  156. }
  157. }
  158. // LoadLuaFunctionsForREPL exports the various Lua functions that might be needed in the REPL
  159. func (ac *Config) LoadLuaFunctionsForREPL(L *lua.LState, o *textoutput.TextOutput) {
  160. // Server configuration functions
  161. ac.LoadServerConfigFunctions(L, "")
  162. // Other basic system functions, like log()
  163. ac.LoadBasicSystemFunctions(L)
  164. // If there is a database backend
  165. if ac.perm != nil {
  166. // Retrieve the creator struct
  167. creator := ac.perm.UserState().Creator()
  168. // Simpleredis data structures
  169. datastruct.LoadList(L, creator)
  170. datastruct.LoadSet(L, creator)
  171. datastruct.LoadHash(L, creator)
  172. datastruct.LoadKeyValue(L, creator)
  173. // For saving and loading Lua functions
  174. codelib.Load(L, creator)
  175. }
  176. // For handling JSON data
  177. jnode.LoadJSONFunctions(L)
  178. ac.LoadJFile(L, ac.serverDirOrFilename)
  179. jnode.Load(L)
  180. // Extras
  181. pure.Load(L)
  182. // Export pprint and scriptdir
  183. exportREPLSpecific(L)
  184. // Plugin functionality
  185. ac.LoadPluginFunctions(L, o)
  186. // Cache
  187. ac.LoadCacheFunctions(L)
  188. }
  189. // REPL provides a "Read Eval Print" loop for interacting with Lua.
  190. // A variety of functions are exposed to the Lua state.
  191. func (ac *Config) REPL(ready, done chan bool) error {
  192. var (
  193. historyFilename string
  194. err error
  195. )
  196. historydir, err := homedir.Dir()
  197. if err != nil {
  198. log.Error("Could not find a user directory to store the REPL history.")
  199. historydir = "."
  200. }
  201. // Retrieve a Lua state
  202. L := ac.luapool.Get()
  203. // Don't re-use the Lua state
  204. defer L.Close()
  205. // Colors and input
  206. windows := (runtime.GOOS == "windows")
  207. mingw := windows && strings.HasPrefix(env.Str("TERM"), "xterm")
  208. enableColors := !windows || mingw
  209. o := textoutput.NewTextOutput(enableColors, true)
  210. // Command history file
  211. if windows {
  212. historyFilename = filepath.Join(historydir, "algernon_history.txt")
  213. } else {
  214. historyFilename = filepath.Join(historydir, ".algernon_history")
  215. }
  216. // Export a selection of functions to the Lua state
  217. ac.LoadLuaFunctionsForREPL(L, o)
  218. <-ready // Wait for the server to be ready
  219. // Tell the user that the server is ready
  220. o.Println(o.LightGreen("Ready"))
  221. // Start the read, eval, print loop
  222. var (
  223. line string
  224. prompt = o.LightCyan("lua> ")
  225. EOF bool
  226. EOFcount int
  227. )
  228. var initialPrefixCompleters []readline.PrefixCompleterInterface
  229. for _, word := range []string{"bye", "confighelp", "cwd", "dir", "exit", "help", "pwd", "quit", "serverdir", "serverfile", "webhelp", "zalgo"} {
  230. initialPrefixCompleters = append(initialPrefixCompleters, &readline.PrefixCompleter{Name: []rune(word)})
  231. }
  232. prefixCompleter := readline.NewPrefixCompleter(initialPrefixCompleters...)
  233. addFunctionsFromHelptextToCompleter(generalHelpText, prefixCompleter)
  234. l, err := readline.NewEx(&readline.Config{
  235. Prompt: prompt,
  236. HistoryFile: historyFilename,
  237. AutoComplete: prefixCompleter,
  238. InterruptPrompt: "^C",
  239. EOFPrompt: "exit",
  240. HistorySearchFold: true,
  241. })
  242. if err != nil {
  243. log.Error("Could not initiate github.com/chzyer/readline: " + err.Error())
  244. }
  245. // To be run at server shutdown
  246. AtShutdown(func() {
  247. // Verbose mode has different log output at shutdown
  248. if !ac.verboseMode {
  249. o.Println(o.LightBlue(exitMessage))
  250. }
  251. })
  252. for {
  253. // Retrieve user input
  254. EOF = false
  255. if mingw {
  256. // No support for EOF
  257. line = ask.Ask(prompt)
  258. } else {
  259. if line, err = l.Readline(); err != nil {
  260. switch {
  261. case err == io.EOF:
  262. if ac.debugMode {
  263. o.Println(o.LightPurple(err.Error()))
  264. }
  265. EOF = true
  266. case err == readline.ErrInterrupt:
  267. log.Warn("Interrupted")
  268. done <- true
  269. return nil
  270. default:
  271. log.Error("Error reading line(" + err.Error() + ").")
  272. continue
  273. }
  274. }
  275. }
  276. if EOF {
  277. if ac.ctrldTwice {
  278. switch EOFcount {
  279. case 0:
  280. o.Err("Press ctrl-d again to exit.")
  281. EOFcount++
  282. continue
  283. default:
  284. done <- true
  285. return nil
  286. }
  287. } else {
  288. done <- true
  289. return nil
  290. }
  291. }
  292. line = strings.TrimSpace(line)
  293. if line == "" {
  294. continue
  295. }
  296. switch line {
  297. case "help":
  298. outputHelp(o, generalHelpText)
  299. continue
  300. case "webhelp":
  301. outputHelp(o, webHelpText)
  302. continue
  303. case "confighelp":
  304. outputHelp(o, configHelpText)
  305. continue
  306. case "dir":
  307. // Be more helpful than listing the Lua bytecode contents of the dir function. Call "dir()".
  308. line = "dir()"
  309. case "cwd", "pwd":
  310. if cwd, err := os.Getwd(); err != nil {
  311. // Might work if Getwd should fail. Should work on Windows, Linux and macOS
  312. line = "os.getenv'CD' or os.getenv'PWD'"
  313. } else {
  314. fmt.Println(cwd)
  315. continue
  316. }
  317. case "serverfile", "serverdir":
  318. if absdir, err := filepath.Abs(ac.serverDirOrFilename); err != nil {
  319. fmt.Println(ac.serverDirOrFilename)
  320. } else {
  321. fmt.Println(absdir)
  322. }
  323. continue
  324. case "quit", "exit", "shutdown", "halt":
  325. done <- true
  326. return nil
  327. case "zalgo":
  328. // Easter egg
  329. o.ErrExit("Ḫ̷̲̫̰̯̭̀̂̑~ͅĚ̥̖̩̘̱͔͈͈ͬ̚ ̦̦͖̲̀ͦ͂C̜͓̲̹͐̔ͭ̏Oͭ͛͂̋ͭͬͬ͆͏̺͓̰͚͠ͅM̢͉̼̖͍̊̕Ḛ̭̭͗̉̀̆ͬ̐ͪ̒S͉̪͂͌̄")
  330. default:
  331. topic := ""
  332. if len(line) > 5 && (strings.HasPrefix(line, "help(") || strings.HasPrefix(line, "help ")) {
  333. topic = line[5:]
  334. } else if len(line) > 8 && (strings.HasPrefix(line, "webhelp(") || strings.HasPrefix(line, "webhelp ")) {
  335. topic = line[8:]
  336. }
  337. if len(topic) > 0 {
  338. topic = strings.TrimSuffix(topic, ")")
  339. outputHelpAbout(o, generalHelpText+webHelpText+configHelpText, topic)
  340. continue
  341. }
  342. }
  343. // If the line starts with print, don't touch it
  344. if strings.HasPrefix(line, "print(") {
  345. if err = L.DoString(line); err != nil {
  346. // Output the error message
  347. o.Err(err.Error())
  348. }
  349. } else {
  350. // Wrap the line in "pprint"
  351. if err = L.DoString("pprint(" + line + ")"); err != nil {
  352. // If there was a syntax error, try again without pprint
  353. if strings.Contains(err.Error(), "syntax error") {
  354. if err = L.DoString(line); err != nil {
  355. // Output the error message
  356. o.Err(err.Error())
  357. }
  358. // For other kinds of errors, output the error
  359. } else {
  360. // Output the error message
  361. o.Err(err.Error())
  362. }
  363. }
  364. }
  365. }
  366. }