Ctrl K
ComunidadGuías…

engine/repl.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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())
				}
			}
		}
	}
}