interpret.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  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. CLIInterpreter is a commandline interpreter for ECAL.
  30. */
  31. type CLIInterpreter struct {
  32. GlobalVS parser.Scope // Global variable scope
  33. RuntimeProvider *interpreter.ECALRuntimeProvider // Runtime provider of the interpreter
  34. // Customizations of output and input handling
  35. CustomHandler CLIInputHandler
  36. CustomWelcomeMessage string
  37. CustomHelpString string
  38. // Parameter these can either be set programmatically or via CLI args
  39. Dir *string // Root dir for interpreter
  40. LogFile *string // Logfile (blank for stdout)
  41. LogLevel *string // Log level string (Debug, Info, Error)
  42. }
  43. /*
  44. NewCLIInterpreter creates a new commandline interpreter for ECAL.
  45. */
  46. func NewCLIInterpreter() *CLIInterpreter {
  47. return &CLIInterpreter{scope.NewScope(scope.GlobalScope), nil, nil, "", "", nil, nil, nil}
  48. }
  49. /*
  50. ParseArgs parses the command line arguments. Call this after adding custon flags.
  51. Returns true if the program should exit.
  52. */
  53. func (i *CLIInterpreter) ParseArgs() bool {
  54. if i.Dir != nil && i.LogFile != nil && i.LogLevel != nil {
  55. return false
  56. }
  57. wd, _ := os.Getwd()
  58. i.Dir = flag.String("dir", wd, "Root directory for ECAL interpreter")
  59. i.LogFile = flag.String("logfile", "", "Log to a file")
  60. i.LogLevel = flag.String("loglevel", "Info", "Logging level (Debug, Info, Error)")
  61. showHelp := flag.Bool("help", false, "Show this help message")
  62. flag.Usage = func() {
  63. fmt.Println()
  64. fmt.Println(fmt.Sprintf("Usage of %s run [options] [file]", os.Args[0]))
  65. fmt.Println()
  66. flag.PrintDefaults()
  67. fmt.Println()
  68. }
  69. if len(os.Args) >= 2 {
  70. flag.CommandLine.Parse(os.Args[2:])
  71. if *showHelp {
  72. flag.Usage()
  73. }
  74. }
  75. return *showHelp
  76. }
  77. /*
  78. Create the runtime provider of this interpreter. This function expects Dir,
  79. LogFile and LogLevel to be set.
  80. */
  81. func (i *CLIInterpreter) CreateRuntimeProvider(name string) error {
  82. var logger util.Logger
  83. var err error
  84. if i.RuntimeProvider != nil {
  85. return nil
  86. }
  87. // Check if we should log to a file
  88. if i.LogFile != nil && *i.LogFile != "" {
  89. var logWriter io.Writer
  90. logFileRollover := fileutil.SizeBasedRolloverCondition(1000000) // Each file can be up to a megabyte
  91. logWriter, err = fileutil.NewMultiFileBuffer(*i.LogFile, fileutil.ConsecutiveNumberIterator(10), logFileRollover)
  92. logger = util.NewBufferLogger(logWriter)
  93. } else {
  94. // Log to the console by default
  95. logger = util.NewStdOutLogger()
  96. }
  97. // Set the log level
  98. if err == nil {
  99. if i.LogLevel != nil && *i.LogLevel != "" {
  100. logger, err = util.NewLogLevelLogger(logger, *i.LogLevel)
  101. }
  102. if err == nil {
  103. // Get the import locator
  104. importLocator := &util.FileImportLocator{Root: *i.Dir}
  105. // Create interpreter
  106. i.RuntimeProvider = interpreter.NewECALRuntimeProvider(name, importLocator, logger)
  107. }
  108. }
  109. return err
  110. }
  111. /*
  112. Interpret starts the ECAL code interpreter. Starts an interactive console in
  113. the current tty if the interactive flag is set.
  114. */
  115. func (i *CLIInterpreter) Interpret(interactive bool) error {
  116. if i.ParseArgs() {
  117. return nil
  118. }
  119. clt, err := termutil.NewConsoleLineTerminal(os.Stdout)
  120. if interactive {
  121. fmt.Println(fmt.Sprintf("ECAL %v", config.ProductVersion))
  122. }
  123. // Create Runtime Provider
  124. if err == nil {
  125. if err = i.CreateRuntimeProvider("console"); err == nil {
  126. if interactive {
  127. if lll, ok := i.RuntimeProvider.Logger.(*util.LogLevelLogger); ok {
  128. fmt.Print(fmt.Sprintf("Log level: %v - ", lll.Level()))
  129. }
  130. fmt.Println(fmt.Sprintf("Root directory: %v", *i.Dir))
  131. if i.CustomWelcomeMessage != "" {
  132. fmt.Println(fmt.Sprintf(i.CustomWelcomeMessage))
  133. }
  134. }
  135. // Execute file if given
  136. if cargs := flag.Args(); len(cargs) > 0 {
  137. var ast *parser.ASTNode
  138. var initFile []byte
  139. initFileName := flag.Arg(0)
  140. initFile, err = ioutil.ReadFile(initFileName)
  141. if ast, err = parser.ParseWithRuntime(initFileName, string(initFile), i.RuntimeProvider); err == nil {
  142. if err = ast.Runtime.Validate(); err == nil {
  143. _, err = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{}))
  144. }
  145. }
  146. }
  147. if err == nil {
  148. if interactive {
  149. // Drop into interactive shell
  150. if err == nil {
  151. isExitLine := func(s string) bool {
  152. return s == "exit" || s == "q" || s == "quit" || s == "bye" || s == "\x04"
  153. }
  154. // Add history functionality without file persistence
  155. clt, err = termutil.AddHistoryMixin(clt, "",
  156. func(s string) bool {
  157. return isExitLine(s)
  158. })
  159. if err == nil {
  160. if err = clt.StartTerm(); err == nil {
  161. var line string
  162. defer clt.StopTerm()
  163. fmt.Println("Type 'q' or 'quit' to exit the shell and '?' to get help")
  164. line, err = clt.NextLine()
  165. for err == nil && !isExitLine(line) {
  166. trimmedLine := strings.TrimSpace(line)
  167. i.HandleInput(clt, trimmedLine)
  168. line, err = clt.NextLine()
  169. }
  170. }
  171. }
  172. }
  173. }
  174. }
  175. }
  176. }
  177. return err
  178. }
  179. /*
  180. HandleInput handles input to this interpreter. It parses a given input line
  181. and outputs on the given output terminal.
  182. */
  183. func (i *CLIInterpreter) HandleInput(ot interpreter.OutputTerminal, line string) {
  184. // Process the entered line
  185. if line == "?" {
  186. // Show help
  187. ot.WriteString(fmt.Sprintf("ECAL %v\n", config.ProductVersion))
  188. ot.WriteString(fmt.Sprint("\n"))
  189. ot.WriteString(fmt.Sprint("Console supports all normal ECAL statements and the following special commands:\n"))
  190. ot.WriteString(fmt.Sprint("\n"))
  191. ot.WriteString(fmt.Sprint(" @sym [glob] - List all available inbuild functions and available stdlib packages of ECAL.\n"))
  192. ot.WriteString(fmt.Sprint(" @std <package> [glob] - List all available constants and functions of a stdlib package.\n"))
  193. if i.CustomHelpString != "" {
  194. ot.WriteString(i.CustomHelpString)
  195. }
  196. ot.WriteString(fmt.Sprint("\n"))
  197. 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"))
  198. } else if strings.HasPrefix(line, "@sym") {
  199. i.displaySymbols(ot, strings.Split(line, " ")[1:])
  200. } else if strings.HasPrefix(line, "@std") {
  201. i.displayPackage(ot, strings.Split(line, " ")[1:])
  202. } else if i.CustomHandler != nil && i.CustomHandler.CanHandle(line) {
  203. i.CustomHandler.Handle(ot, strings.Split(line, " ")[1:])
  204. } else {
  205. var ierr error
  206. var ast *parser.ASTNode
  207. var res interface{}
  208. if ast, ierr = parser.ParseWithRuntime("console input", line, i.RuntimeProvider); ierr == nil {
  209. if ierr = ast.Runtime.Validate(); ierr == nil {
  210. if res, ierr = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{})); ierr == nil && res != nil {
  211. ot.WriteString(fmt.Sprintln(res))
  212. }
  213. }
  214. }
  215. if ierr != nil {
  216. ot.WriteString(fmt.Sprintln(ierr.Error()))
  217. }
  218. }
  219. }
  220. /*
  221. displaySymbols lists all available inbuild functions and available stdlib packages of ECAL.
  222. */
  223. func (i *CLIInterpreter) displaySymbols(ot interpreter.OutputTerminal, args []string) {
  224. tabData := []string{"Inbuild function", "Description"}
  225. for name, f := range interpreter.InbuildFuncMap {
  226. ds, _ := f.DocString()
  227. if len(args) > 0 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", name, ds), args[0]) {
  228. continue
  229. }
  230. tabData = fillTableRow(tabData, name, ds)
  231. }
  232. if len(tabData) > 2 {
  233. ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
  234. stringutil.SingleDoubleLineTable))
  235. }
  236. packageNames, _, _ := stdlib.GetStdlibSymbols()
  237. tabData = []string{"Package name", "Description"}
  238. for _, p := range packageNames {
  239. ps, _ := stdlib.GetPkgDocString(p)
  240. if len(args) > 0 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", p, ps), args[0]) {
  241. continue
  242. }
  243. tabData = fillTableRow(tabData, p, ps)
  244. }
  245. if len(tabData) > 2 {
  246. ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
  247. stringutil.SingleDoubleLineTable))
  248. }
  249. }
  250. /*
  251. displayPackage list all available constants and functions of a stdlib package.
  252. */
  253. func (i *CLIInterpreter) displayPackage(ot interpreter.OutputTerminal, args []string) {
  254. _, constSymbols, funcSymbols := stdlib.GetStdlibSymbols()
  255. tabData := []string{"Constant", "Value"}
  256. for _, s := range constSymbols {
  257. if len(args) > 0 && !strings.HasPrefix(s, args[0]) {
  258. continue
  259. }
  260. val, _ := stdlib.GetStdlibConst(s)
  261. tabData = fillTableRow(tabData, s, fmt.Sprint(val))
  262. }
  263. if len(tabData) > 2 {
  264. ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
  265. stringutil.SingleDoubleLineTable))
  266. }
  267. tabData = []string{"Function", "Description"}
  268. for _, f := range funcSymbols {
  269. if len(args) > 0 && !strings.HasPrefix(f, args[0]) {
  270. continue
  271. }
  272. fObj, _ := stdlib.GetStdlibFunc(f)
  273. fDoc, _ := fObj.DocString()
  274. fDoc = strings.Replace(fDoc, "\n", " ", -1)
  275. fDoc = strings.Replace(fDoc, "\t", " ", -1)
  276. if len(args) > 1 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", f, fDoc), args[1]) {
  277. continue
  278. }
  279. tabData = fillTableRow(tabData, f, fDoc)
  280. }
  281. if len(tabData) > 2 {
  282. ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
  283. stringutil.SingleDoubleLineTable))
  284. }
  285. }