interpret.go 13 KB

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