engine/repl.go
Rama main · réplica de lectura del repositorio original.
package engine
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/chzyer/readline"
"github.com/mitchellh/go-homedir"
"github.com/sirupsen/logrus"
"github.com/xyproto/algernon/lua/codelib"
"github.com/xyproto/algernon/lua/convert"
"github.com/xyproto/algernon/lua/datastruct"
"github.com/xyproto/algernon/lua/jnode"
"github.com/xyproto/algernon/lua/pure"
"github.com/xyproto/algernon/platformdep"
"github.com/xyproto/ask"
lua "github.com/xyproto/gopher-lua"
"github.com/xyproto/vt"
)
const exitMessage = "bye"
// Export Lua functions specific to the REPL
func exportREPLSpecific(L *lua.LState) {
// Attempt to return a more informative text than the memory location.
// Can take several arguments, just like print().
L.SetGlobal("pprint", L.NewFunction(func(L *lua.LState) int {
var buf bytes.Buffer
top := L.GetTop()
for i := 1; i <= top; i++ {
convert.PprintToWriter(&buf, L.Get(i))
if i != top {
buf.WriteString("\t")
}
}
// Output the combined text
fmt.Println(buf.String())
return 0 // number of results
}))
// Get the current directory since this is probably in the REPL
L.SetGlobal("scriptdir", L.NewFunction(func(L *lua.LState) int {
scriptpath, err := os.Getwd()
if err != nil {
logrus.Error(err)
L.Push(lua.LString("."))
return 1 // number of results
}
top := L.GetTop()
if top == 1 {
// Also include a separator and a filename
fn := L.ToString(1)
scriptpath = filepath.Join(scriptpath, fn)
}
// Now have the correct absolute scriptpath
L.Push(lua.LString(scriptpath))
return 1 // number of results
}))
// Given a glob, like "md/*.md", read the files with the scriptdir() as the starting point.
// Then return all the contents of the files as a table.
L.SetGlobal("readglob", L.NewFunction(func(L *lua.LState) int {
var (
pattern = L.ToString(1)
basepath string
err error
)
if L.GetTop() == 2 {
basepath = L.ToString(2)
} else {
basepath, err = os.Getwd()
if err != nil {
logrus.Error(err)
L.Push(lua.LNil)
return 1
}
}
matches, err := filepath.Glob(filepath.Join(basepath, pattern))
if err != nil {
L.Push(lua.LNil)
return 1
}
results := L.NewTable()
for _, match := range matches {
content, err := os.ReadFile(match)
if err != nil {
logrus.Error(err)
L.Push(lua.LNil)
return 1
}
results.Append(lua.LString(content))
}
L.Push(results)
return 1
}))
}
// Syntax highlight the given line
func highlight(line string) string {
unprocessed := line
moduleName := ""
unprocessed, comment := vt.ColorSplit(unprocessed, "//", 0, vt.DarkGray, vt.DarkGray, false)
// Only split on : when present
if strings.Contains(unprocessed, ":") {
moduleName, unprocessed = vt.ColorSplit(unprocessed, ":", vt.LightGreen, vt.Red, 0, false)
}
functionName := ""
if unprocessed != "" {
// Green function names
if strings.Contains(unprocessed, "(") {
fields := strings.SplitN(unprocessed, "(", 2)
functionName = vt.LightGreen.Get(fields[0])
unprocessed = "(" + fields[1]
} else if strings.Contains(unprocessed, "|") {
unprocessed = "<magenta>" + strings.ReplaceAll(unprocessed, "|", "<white>|</white><magenta>") + "</magenta>"
}
}
unprocessed, typed := vt.ColorSplit(unprocessed, "->", 0, vt.LightBlue, vt.Red, false)
unprocessed = strings.ReplaceAll(unprocessed, "string", vt.LightBlue.Get("string"))
unprocessed = strings.ReplaceAll(unprocessed, "number", vt.LightYellow.Get("number"))
unprocessed = strings.ReplaceAll(unprocessed, "function", vt.LightCyan.Get("function"))
return moduleName + functionName + unprocessed + typed + comment
}
// Output syntax highlighted help text, with an additional usage message
func outputHelp(o *vt.TextOutput, helpText string) {
for line := range strings.SplitSeq(helpText, "\n") {
o.Println(highlight(line))
}
o.Println(usageMessage)
}
// Output syntax highlighted help about a specific topic or function
func outputHelpAbout(o *vt.TextOutput, helpText, topic string) {
switch topic {
case "help":
o.Println(vt.DarkGray.Get("Output general help or help about a specific topic."))
return
case "webhelp":
o.Println(vt.DarkGray.Get("Output help about web-related functions."))
return
case "confighelp":
o.Println(vt.DarkGray.Get("Output help about configuration-related functions."))
return
case "quit", "exit", "shutdown", "halt":
o.Println(vt.DarkGray.Get("Quit Algernon."))
return
}
comment := ""
for line := range strings.SplitSeq(helpText, "\n") {
if strings.HasPrefix(line, topic) {
// Output help text, with some surrounding blank lines
o.Println("\n" + highlight(line))
o.Println("\n" + vt.DarkGray.Get(strings.TrimSpace(comment)) + "\n")
return
}
// Gather comments until a non-comment is encountered
if strings.HasPrefix(line, "//") {
comment += strings.TrimSpace(line[2:]) + "\n"
} else {
comment = ""
}
}
o.Println(vt.DarkGray.Get("Found no help for: ") + vt.White.Get(topic))
}
// Take all functions mentioned in the given help text string and add them to the readline completer
func addFunctionsFromHelptextToCompleter(helpText string, completer *readline.PrefixCompleter) {
for line := range strings.SplitSeq(helpText, "\n") {
if !strings.HasPrefix(line, "//") && strings.Contains(line, "(") {
parts := strings.Split(line, "(")
if strings.Contains(line, "()") {
completer.Children = append(completer.Children, &readline.PrefixCompleter{Name: []rune(parts[0] + "()")})
} else {
completer.Children = append(completer.Children, &readline.PrefixCompleter{Name: []rune(parts[0] + "(")})
}
}
}
}
// LoadLuaFunctionsForREPL exports the various Lua functions that might be needed in the REPL
func (ac *Config) LoadLuaFunctionsForREPL(L *lua.LState, o *vt.TextOutput) {
// Server settings functions (available regardless of database backend)
ac.loadServerSettingsFunctions(L, "")
// Server configuration functions (permission-related)
ac.LoadServerConfigFunctions(L, "")
// Other basic system functions, like log()
ac.LoadBasicSystemFunctions(L)
// If there is a database backend
if ac.perm != nil {
// Retrieve the creator struct
creator := ac.perm.UserState().Creator()
// Simpleredis data structures
datastruct.LoadList(L, creator)
datastruct.LoadSet(L, creator)
datastruct.LoadHash(L, creator)
datastruct.LoadKeyValue(L, creator)
// For saving and loading Lua functions
codelib.Load(L, creator)
}
// For handling JSON data
jnode.LoadJSONFunctions(L)
ac.LoadJFile(L, ac.serverDirOrFilename)
jnode.Load(L)
// Extras
pure.Load(L)
// Export pprint and scriptdir
exportREPLSpecific(L)
// Plugin functionality
ac.LoadPluginFunctions(L, o)
// Cache
ac.LoadCacheFunctions(L)
}
// REPL provides a "Read Eval Print" loop for interacting with Lua.
// A variety of functions are exposed to the Lua state.
func (ac *Config) REPL(ready, done chan bool) error {
var (
historyFilename string
err error
)
historydir, err := homedir.Dir()
if err != nil {
logrus.Error("Could not find a user directory to store the REPL history.")
historydir = "."
}
// Retrieve a Lua state
L := ac.luapool.Borrow()
// Don't re-use the Lua state
defer L.Close()
// Colors and input
o := vt.NewTextOutput(platformdep.EnableColors, true)
// Command history file
historyFilename = filepath.Join(historydir, platformdep.HistoryFilename)
// Export a selection of functions to the Lua state
ac.LoadLuaFunctionsForREPL(L, o)
enableOSExit(L)
<-ready // Wait for the server to be ready
// Tell the user that the server is ready
o.Println(vt.LightGreen.Get("Ready"))
// Start the read, eval, print loop
var (
line string
prompt = vt.LightCyan.Get("lua> ")
EOF bool
EOFcount int
)
var initialPrefixCompleters []readline.PrefixCompleterInterface
for _, word := range []string{"bye", "confighelp", "cwd", "dir", "exit", "help", "pwd", "quit", "serverdir", "serverfile", "webhelp", "zalgo"} {
initialPrefixCompleters = append(initialPrefixCompleters, &readline.PrefixCompleter{Name: []rune(word)})
}
prefixCompleter := readline.NewPrefixCompleter(initialPrefixCompleters...)
addFunctionsFromHelptextToCompleter(generalHelpText, prefixCompleter)
l, err := readline.NewEx(&readline.Config{
Prompt: prompt,
HistoryFile: historyFilename,
AutoComplete: prefixCompleter,
InterruptPrompt: "^C",
EOFPrompt: "exit",
HistorySearchFold: true,
})
if err != nil {
logrus.Error("Could not initiate github.com/chzyer/readline: " + err.Error())
}
// To be run at server shutdown
AtShutdown(func() {
// Verbose mode has different log output at shutdown
if !ac.verboseMode {
o.Println(vt.LightBlue.Get(exitMessage))
}
})
for {
// Retrieve user input
EOF = false
if platformdep.Mingw {
// No support for EOF
line = ask.Ask(prompt)
} else {
if line, err = l.Readline(); err != nil {
switch {
case err == io.EOF:
if ac.debugMode {
o.Println(vt.LightMagenta.Get(err.Error()))
}
EOF = true
case err == readline.ErrInterrupt:
logrus.Warn("Interrupted")
done <- true
return nil
default:
logrus.Error("Error reading line(" + err.Error() + ").")
continue
}
}
}
if EOF {
if ac.ctrldTwice {
switch EOFcount {
case 0:
o.Err("Press ctrl-d again to exit.")
EOFcount++
continue
default:
done <- true
return nil
}
} else {
done <- true
return nil
}
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
switch line {
case "help":
outputHelp(o, generalHelpText)
continue
case "webhelp":
outputHelp(o, webHelpText)
continue
case "confighelp":
outputHelp(o, configHelpText)
continue
case "dir":
// Be more helpful than listing the Lua bytecode contents of the dir function. Call "dir()".
line = "dir()"
case "cwd", "pwd":
if cwd, err := os.Getwd(); err != nil {
// Might work if Getwd should fail. Should work on Windows, Linux and macOS
line = "os.getenv'CD' or os.getenv'PWD'"
} else {
fmt.Println(cwd)
continue
}
case "serverfile", "serverdir":
if absdir, err := filepath.Abs(ac.serverDirOrFilename); err != nil {
fmt.Println(ac.serverDirOrFilename)
} else {
fmt.Println(absdir)
}
continue
case "quit", "exit", "shutdown", "halt":
done <- true
return nil
case "zalgo":
// Easter egg
o.ErrExit("Ḫ̷̲̫̰̯̭̀̂̑~ͅĚ̥̖̩̘̱͔͈͈ͬ̚ ̦̦͖̲̀ͦ͂C̜͓̲̹͐̔ͭ̏Oͭ͛͂̋ͭͬͬ͆͏̺͓̰͚͠ͅM̢͉̼̖͍̊̕Ḛ̭̭͗̉̀̆ͬ̐ͪ̒S͉̪͂͌̄")
default:
topic := ""
if len(line) > 5 && (strings.HasPrefix(line, "help(") || strings.HasPrefix(line, "help ")) {
topic = line[5:]
} else if len(line) > 8 && (strings.HasPrefix(line, "webhelp(") || strings.HasPrefix(line, "webhelp ")) {
topic = line[8:]
}
if len(topic) > 0 {
topic = strings.TrimSuffix(topic, ")")
outputHelpAbout(o, generalHelpText+webHelpText+configHelpText, topic)
continue
}
}
// If the line starts with print, don't touch it
if strings.HasPrefix(line, "print(") {
if err = L.DoString(line); err != nil {
// Output the error message
o.Err(err.Error())
}
} else {
// Wrap the line in "pprint"
if err = L.DoString("pprint(" + line + ")"); err != nil {
// If there was a syntax error, try again without pprint
if strings.Contains(err.Error(), "syntax error") {
if err = L.DoString(line); err != nil {
// Output the error message
o.Err(err.Error())
}
// For other kinds of errors, output the error
} else {
// Output the error message
o.Err(err.Error())
}
}
}
}
}