interpret.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. /*
  2. * ECAL
  3. *
  4. * Copyright 2020 Matthias Ladkau. All rights reserved.
  5. *
  6. * This Source Code Form is subject to the terms of the MIT
  7. * License, If a copy of the MIT License was not distributed with this
  8. * file, You can obtain one at https://opensource.org/licenses/MIT.
  9. */
  10. package tool
  11. import (
  12. "flag"
  13. "fmt"
  14. "io"
  15. "io/ioutil"
  16. "os"
  17. "strings"
  18. "devt.de/krotik/common/fileutil"
  19. "devt.de/krotik/common/stringutil"
  20. "devt.de/krotik/common/termutil"
  21. "devt.de/krotik/ecal/config"
  22. "devt.de/krotik/ecal/interpreter"
  23. "devt.de/krotik/ecal/parser"
  24. "devt.de/krotik/ecal/scope"
  25. "devt.de/krotik/ecal/stdlib"
  26. "devt.de/krotik/ecal/util"
  27. )
  28. /*
  29. CLICustomHandler is a handler for custom operations.
  30. */
  31. type CLICustomHandler interface {
  32. CLIInputHandler
  33. /*
  34. LoadInitialFile clears the global scope and reloads the initial file.
  35. */
  36. LoadInitialFile(tid uint64) error
  37. }
  38. /*
  39. CLIInterpreter is a commandline interpreter for ECAL.
  40. */
  41. type CLIInterpreter struct {
  42. GlobalVS parser.Scope // Global variable scope
  43. RuntimeProvider *interpreter.ECALRuntimeProvider // Runtime provider of the interpreter
  44. // Customizations of output and input handling
  45. CustomHandler CLICustomHandler
  46. CustomWelcomeMessage string
  47. CustomHelpString string
  48. EntryFile string // Entry file for the program
  49. // Parameter these can either be set programmatically or via CLI args
  50. Dir *string // Root dir for interpreter
  51. LogFile *string // Logfile (blank for stdout)
  52. LogLevel *string // Log level string (Debug, Info, Error)
  53. // Log output
  54. LogOut io.Writer
  55. }
  56. /*
  57. NewCLIInterpreter creates a new commandline interpreter for ECAL.
  58. */
  59. func NewCLIInterpreter() *CLIInterpreter {
  60. return &CLIInterpreter{scope.NewScope(scope.GlobalScope), nil, nil, "", "", "", nil, nil, nil, os.Stdout}
  61. }
  62. /*
  63. ParseArgs parses the command line arguments. Call this after adding custon flags.
  64. Returns true if the program should exit.
  65. */
  66. func (i *CLIInterpreter) ParseArgs() bool {
  67. if i.Dir != nil && i.LogFile != nil && i.LogLevel != nil {
  68. return false
  69. }
  70. wd, _ := os.Getwd()
  71. i.Dir = flag.String("dir", wd, "Root directory for ECAL interpreter")
  72. i.LogFile = flag.String("logfile", "", "Log to a file")
  73. i.LogLevel = flag.String("loglevel", "Info", "Logging level (Debug, Info, Error)")
  74. showHelp := flag.Bool("help", false, "Show this help message")
  75. flag.Usage = func() {
  76. fmt.Println()
  77. fmt.Println(fmt.Sprintf("Usage of %s run [options] [file]", os.Args[0]))
  78. fmt.Println()
  79. flag.PrintDefaults()
  80. fmt.Println()
  81. }
  82. if len(os.Args) >= 2 {
  83. flag.CommandLine.Parse(os.Args[2:])
  84. if cargs := flag.Args(); len(cargs) > 0 {
  85. i.EntryFile = flag.Arg(0)
  86. }
  87. if *showHelp {
  88. flag.Usage()
  89. }
  90. }
  91. return *showHelp
  92. }
  93. /*
  94. Create the runtime provider of this interpreter. This function expects Dir,
  95. LogFile and LogLevel to be set.
  96. */
  97. func (i *CLIInterpreter) CreateRuntimeProvider(name string) error {
  98. var logger util.Logger
  99. var err error
  100. if i.RuntimeProvider != nil {
  101. return nil
  102. }
  103. // Check if we should log to a file
  104. if i.LogFile != nil && *i.LogFile != "" {
  105. var logWriter io.Writer
  106. logFileRollover := fileutil.SizeBasedRolloverCondition(1000000) // Each file can be up to a megabyte
  107. logWriter, err = fileutil.NewMultiFileBuffer(*i.LogFile, fileutil.ConsecutiveNumberIterator(10), logFileRollover)
  108. logger = util.NewBufferLogger(logWriter)
  109. } else {
  110. // Log to the console by default
  111. logger = util.NewStdOutLogger()
  112. }
  113. // Set the log level
  114. if err == nil {
  115. if i.LogLevel != nil && *i.LogLevel != "" {
  116. logger, err = util.NewLogLevelLogger(logger, *i.LogLevel)
  117. }
  118. if err == nil {
  119. // Get the import locator
  120. importLocator := &util.FileImportLocator{Root: *i.Dir}
  121. // Create interpreter
  122. i.RuntimeProvider = interpreter.NewECALRuntimeProvider(name, importLocator, logger)
  123. }
  124. }
  125. return err
  126. }
  127. /*
  128. LoadInitialFile clears the global scope and reloads the initial file.
  129. */
  130. func (i *CLIInterpreter) LoadInitialFile(tid uint64) error {
  131. var err error
  132. if i.CustomHandler != nil {
  133. i.CustomHandler.LoadInitialFile(tid)
  134. }
  135. i.GlobalVS.Clear()
  136. if i.EntryFile != "" {
  137. var ast *parser.ASTNode
  138. var initFile []byte
  139. initFile, err = ioutil.ReadFile(i.EntryFile)
  140. if ast, err = parser.ParseWithRuntime(i.EntryFile, string(initFile), i.RuntimeProvider); err == nil {
  141. if err = ast.Runtime.Validate(); err == nil {
  142. _, err = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{}), tid)
  143. }
  144. defer func() {
  145. if i.RuntimeProvider.Debugger != nil {
  146. i.RuntimeProvider.Debugger.RecordThreadFinished(tid)
  147. }
  148. }()
  149. }
  150. }
  151. return err
  152. }
  153. /*
  154. Interpret starts the ECAL code interpreter. Starts an interactive console in
  155. the current tty if the interactive flag is set.
  156. */
  157. func (i *CLIInterpreter) Interpret(interactive bool) error {
  158. if i.ParseArgs() {
  159. return nil
  160. }
  161. clt, err := termutil.NewConsoleLineTerminal(os.Stdout)
  162. if interactive {
  163. fmt.Fprintln(i.LogOut, fmt.Sprintf("ECAL %v", config.ProductVersion))
  164. }
  165. // Create Runtime Provider
  166. if err == nil {
  167. if err = i.CreateRuntimeProvider("console"); err == nil {
  168. tid := i.RuntimeProvider.NewThreadID()
  169. if interactive {
  170. if lll, ok := i.RuntimeProvider.Logger.(*util.LogLevelLogger); ok {
  171. fmt.Fprint(i.LogOut, fmt.Sprintf("Log level: %v - ", lll.Level()))
  172. }
  173. fmt.Fprintln(i.LogOut, fmt.Sprintf("Root directory: %v", *i.Dir))
  174. if i.CustomWelcomeMessage != "" {
  175. fmt.Fprintln(i.LogOut, fmt.Sprintf(i.CustomWelcomeMessage))
  176. }
  177. }
  178. // Execute file if given
  179. if err = i.LoadInitialFile(tid); err == nil {
  180. if interactive {
  181. // Drop into interactive shell
  182. if err == nil {
  183. isExitLine := func(s string) bool {
  184. return s == "exit" || s == "q" || s == "quit" || s == "bye" || s == "\x04"
  185. }
  186. // Add history functionality without file persistence
  187. clt, err = termutil.AddHistoryMixin(clt, "",
  188. func(s string) bool {
  189. return isExitLine(s)
  190. })
  191. if err == nil {
  192. if err = clt.StartTerm(); err == nil {
  193. var line string
  194. defer clt.StopTerm()
  195. fmt.Fprintln(i.LogOut, "Type 'q' or 'quit' to exit the shell and '?' to get help")
  196. line, err = clt.NextLine()
  197. for err == nil && !isExitLine(line) {
  198. trimmedLine := strings.TrimSpace(line)
  199. i.HandleInput(clt, trimmedLine, tid)
  200. line, err = clt.NextLine()
  201. }
  202. }
  203. }
  204. }
  205. }
  206. }
  207. }
  208. }
  209. return err
  210. }
  211. /*
  212. HandleInput handles input to this interpreter. It parses a given input line
  213. and outputs on the given output terminal. Requires a thread ID of the executing
  214. thread - use the RuntimeProvider to generate a unique one.
  215. */
  216. func (i *CLIInterpreter) HandleInput(ot OutputTerminal, line string, tid uint64) {
  217. // Process the entered line
  218. if line == "?" {
  219. // Show help
  220. ot.WriteString(fmt.Sprintf("ECAL %v\n", config.ProductVersion))
  221. ot.WriteString(fmt.Sprint("\n"))
  222. ot.WriteString(fmt.Sprint("Console supports all normal ECAL statements and the following special commands:\n"))
  223. ot.WriteString(fmt.Sprint("\n"))
  224. ot.WriteString(fmt.Sprint(" @reload - Clear the interpreter and reload the initial file if it was given.\n"))
  225. ot.WriteString(fmt.Sprint(" @sym [glob] - List all available inbuild functions and available stdlib packages of ECAL.\n"))
  226. ot.WriteString(fmt.Sprint(" @std <package> [glob] - List all available constants and functions of a stdlib package.\n"))
  227. if i.CustomHelpString != "" {
  228. ot.WriteString(i.CustomHelpString)
  229. }
  230. ot.WriteString(fmt.Sprint("\n"))
  231. ot.WriteString(fmt.Sprint("Add an argument after a list command to do a full text search. The search string should be in glob format.\n"))
  232. } else if strings.HasPrefix(line, "@reload") {
  233. // Reload happens in a separate thread as it may be suspended on start
  234. go i.LoadInitialFile(i.RuntimeProvider.NewThreadID())
  235. ot.WriteString(fmt.Sprintln(fmt.Sprintln("Reloading interpreter state")))
  236. } else if strings.HasPrefix(line, "@sym") {
  237. i.displaySymbols(ot, strings.Split(line, " ")[1:])
  238. } else if strings.HasPrefix(line, "@std") {
  239. i.displayPackage(ot, strings.Split(line, " ")[1:])
  240. } else if i.CustomHandler != nil && i.CustomHandler.CanHandle(line) {
  241. i.CustomHandler.Handle(ot, line)
  242. } else {
  243. var ierr error
  244. var ast *parser.ASTNode
  245. var res interface{}
  246. if line != "" {
  247. if ast, ierr = parser.ParseWithRuntime("console input", line, i.RuntimeProvider); ierr == nil {
  248. if ierr = ast.Runtime.Validate(); ierr == nil {
  249. if res, ierr = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{}), tid); ierr == nil && res != nil {
  250. ot.WriteString(fmt.Sprintln(stringutil.ConvertToString(res)))
  251. }
  252. defer func() {
  253. if i.RuntimeProvider.Debugger != nil {
  254. i.RuntimeProvider.Debugger.RecordThreadFinished(tid)
  255. }
  256. }()
  257. }
  258. }
  259. if ierr != nil {
  260. ot.WriteString(fmt.Sprintln(ierr.Error()))
  261. }
  262. }
  263. }
  264. }
  265. /*
  266. displaySymbols lists all available inbuild functions and available stdlib packages of ECAL.
  267. */
  268. func (i *CLIInterpreter) displaySymbols(ot OutputTerminal, args []string) {
  269. tabData := []string{"Inbuild function", "Description"}
  270. for name, f := range interpreter.InbuildFuncMap {
  271. ds, _ := f.DocString()
  272. if len(args) > 0 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", name, ds), args[0]) {
  273. continue
  274. }
  275. tabData = fillTableRow(tabData, name, ds)
  276. }
  277. if len(tabData) > 2 {
  278. ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
  279. stringutil.SingleDoubleLineTable))
  280. }
  281. packageNames, _, _ := stdlib.GetStdlibSymbols()
  282. tabData = []string{"Package name", "Description"}
  283. for _, p := range packageNames {
  284. ps, _ := stdlib.GetPkgDocString(p)
  285. if len(args) > 0 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", p, ps), args[0]) {
  286. continue
  287. }
  288. tabData = fillTableRow(tabData, p, ps)
  289. }
  290. if len(tabData) > 2 {
  291. ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
  292. stringutil.SingleDoubleLineTable))
  293. }
  294. }
  295. /*
  296. displayPackage list all available constants and functions of a stdlib package.
  297. */
  298. func (i *CLIInterpreter) displayPackage(ot OutputTerminal, args []string) {
  299. _, constSymbols, funcSymbols := stdlib.GetStdlibSymbols()
  300. tabData := []string{"Constant", "Value"}
  301. for _, s := range constSymbols {
  302. if len(args) > 0 && !strings.HasPrefix(s, args[0]) {
  303. continue
  304. }
  305. val, _ := stdlib.GetStdlibConst(s)
  306. tabData = fillTableRow(tabData, s, fmt.Sprint(val))
  307. }
  308. if len(tabData) > 2 {
  309. ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
  310. stringutil.SingleDoubleLineTable))
  311. }
  312. tabData = []string{"Function", "Description"}
  313. for _, f := range funcSymbols {
  314. if len(args) > 0 && !strings.HasPrefix(f, args[0]) {
  315. continue
  316. }
  317. fObj, _ := stdlib.GetStdlibFunc(f)
  318. fDoc, _ := fObj.DocString()
  319. fDoc = strings.Replace(fDoc, "\n", " ", -1)
  320. fDoc = strings.Replace(fDoc, "\t", " ", -1)
  321. if len(args) > 1 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", f, fDoc), args[1]) {
  322. continue
  323. }
  324. tabData = fillTableRow(tabData, f, fDoc)
  325. }
  326. if len(tabData) > 2 {
  327. ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
  328. stringutil.SingleDoubleLineTable))
  329. }
  330. }