Browse Source

feat: CLI support

Matthias Ladkau 3 years ago
parent
commit
7975b738af
8 changed files with 464 additions and 37 deletions
  1. 5 5
      Makefile
  2. 64 5
      cli/ecal.go
  3. 191 0
      cli/interpret.go
  4. 0 27
      eval.go
  5. 38 0
      stdlib/stdlib.go
  6. 8 0
      stdlib/stdlib_test.go
  7. 100 0
      util/logging.go
  8. 58 0
      util/logging_test.go

+ 5 - 5
Makefile

@@ -26,19 +26,19 @@ generate:
 	go generate devt.de/krotik/ecal/stdlib/generate
 
 build: clean mod generate fmt vet
-	go build -ldflags "-s -w" -o $(NAME) cli/ecal.go
+	go build -ldflags "-s -w" -o $(NAME) cli/*.go
 
 build-mac: clean mod generate fmt vet
-	GOOS=darwin GOARCH=amd64 go build -o $(NAME).mac cli/ecal.go
+	GOOS=darwin GOARCH=amd64 go build -o $(NAME).mac cli/*.go
 
 build-win: clean mod generate fmt vet
-	GOOS=windows GOARCH=amd64 go build -o $(NAME).exe cli/ecal.go
+	GOOS=windows GOARCH=amd64 go build -o $(NAME).exe cli/*.go
 
 build-arm7: clean mod generate fmt vet
-	GOOS=linux GOARCH=arm GOARM=7 go build -o $(NAME).arm7 cli/ecal.go
+	GOOS=linux GOARCH=arm GOARM=7 go build -o $(NAME).arm7 cli/*.go
 
 build-arm8: clean mod generate fmt vet
-	GOOS=linux GOARCH=arm64 go build -o $(NAME).arm8 cli/ecal.go
+	GOOS=linux GOARCH=arm64 go build -o $(NAME).arm8 cli/*.go
 
 dist: build build-win build-mac build-arm7 build-arm8
 	rm -fR dist

+ 64 - 5
cli/ecal.go

@@ -10,15 +10,74 @@
 
 package main
 
-import "fmt"
+import (
+	"flag"
+	"fmt"
+	"os"
+
+	"devt.de/krotik/ecal/config"
+)
 
 /*
-Ideas:
-- auto reload code (watch)
-- cron job support (trigger periodic events)
+TODO:
+- CLI interpreter (show base directory when starting)
+-- console can specify a base directory
+-- console can preload code
+
 - create executable binary (pack into single binary)
+
+- debug server support (vscode)
 */
 
 func main() {
-	fmt.Println("ECAL")
+
+	// Initialize the default command line parser
+
+	flag.CommandLine.Init(os.Args[0], flag.ContinueOnError)
+
+	// Define default usage message
+
+	flag.Usage = func() {
+
+		// Print usage for tool selection
+
+		fmt.Println(fmt.Sprintf("Usage of %s <tool>", os.Args[0]))
+		fmt.Println()
+		fmt.Println(fmt.Sprintf("ECAL %v - Event Condition Action Language", config.ProductVersion))
+		fmt.Println()
+		fmt.Println("Available commands:")
+		fmt.Println()
+		fmt.Println("    console   Interactive console (default)")
+		fmt.Println("    run       Execute ECAL code")
+		fmt.Println("    debug     Run a debug server")
+		fmt.Println("    pack      Create a single executable from ECAL code")
+		fmt.Println()
+		fmt.Println(fmt.Sprintf("Use %s <command> -help for more information about a given command.", os.Args[0]))
+		fmt.Println()
+	}
+
+	// Parse the command bit
+
+	err := flag.CommandLine.Parse(os.Args[1:])
+
+	if len(flag.Args()) > 0 {
+
+		arg := flag.Args()[0]
+
+		if arg == "console" {
+			err = interpret(true)
+		} else if arg == "run" {
+			err = interpret(false)
+		} else {
+			flag.Usage()
+		}
+
+	} else if err == nil {
+
+		err = interpret(true)
+	}
+
+	if err != nil {
+		fmt.Println(fmt.Sprintf("Error: %v", err))
+	}
 }

+ 191 - 0
cli/interpret.go

@@ -0,0 +1,191 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"os"
+
+	"devt.de/krotik/common/fileutil"
+	"devt.de/krotik/common/termutil"
+	"devt.de/krotik/ecal/config"
+	"devt.de/krotik/ecal/interpreter"
+	"devt.de/krotik/ecal/parser"
+	"devt.de/krotik/ecal/scope"
+	"devt.de/krotik/ecal/util"
+)
+
+/*
+interpret starts the ECAL code interpreter.
+*/
+func interpret(interactive bool) error {
+	var err error
+
+	wd, _ := os.Getwd()
+
+	idir := flag.String("dir", wd, "Root directory for ECAL interpreter")
+	ilogFile := flag.String("logfile", "", "Log to a file")
+	ilogLevel := flag.String("loglevel", "Info", "Logging level (Debug, Info, Error)")
+	showHelp := flag.Bool("help", false, "Show this help message")
+
+	flag.Usage = func() {
+		fmt.Println()
+		if !interactive {
+			fmt.Println(fmt.Sprintf("Usage of %s run [options] <file>", os.Args[0]))
+		} else {
+			fmt.Println(fmt.Sprintf("Usage of %s [options]", os.Args[0]))
+		}
+		fmt.Println()
+		flag.PrintDefaults()
+		fmt.Println()
+	}
+
+	if len(os.Args) > 2 {
+		flag.CommandLine.Parse(os.Args[2:])
+
+		if *showHelp {
+			flag.Usage()
+			return nil
+		}
+	}
+
+	var clt termutil.ConsoleLineTerminal
+	var logger util.Logger
+
+	clt, err = termutil.NewConsoleLineTerminal(os.Stdout)
+
+	// Create the logger
+
+	if err == nil {
+
+		// Check if we should log to a file
+
+		if ilogFile != nil && *ilogFile != "" {
+			var logWriter io.Writer
+			logFileRollover := fileutil.SizeBasedRolloverCondition(1000000) // Each file can be up to a megabyte
+			logWriter, err = fileutil.NewMultiFileBuffer(*ilogFile, fileutil.ConsecutiveNumberIterator(10), logFileRollover)
+			logger = util.NewBufferLogger(logWriter)
+
+		} else {
+
+			// Log to the console by default
+
+			logger = util.NewStdOutLogger()
+		}
+
+		// Set the log level
+
+		if err == nil {
+			if ilogLevel != nil && *ilogLevel != "" {
+				logger, err = util.NewLogLevelLogger(logger, *ilogLevel)
+			}
+
+		}
+	}
+
+	// Get the import locator
+
+	importLocator := &util.FileImportLocator{Root: *idir}
+
+	if err == nil {
+
+		name := "ECAL console"
+
+		// Create interpreter
+
+		erp := interpreter.NewECALRuntimeProvider(name, importLocator, logger)
+
+		// Create global variable scope
+
+		vs := scope.NewScope(scope.GlobalScope)
+
+		// TODO Execute file
+
+		if interactive {
+
+			// Drop into interactive shell
+
+			if err == nil {
+				isExitLine := func(s string) bool {
+					return s == "exit" || s == "q" || s == "quit" || s == "bye" || s == "\x04"
+				}
+
+				// Add history functionality without file persistence
+
+				clt, err = termutil.AddHistoryMixin(clt, "",
+					func(s string) bool {
+						return isExitLine(s)
+					})
+
+				if err == nil {
+
+					if err = clt.StartTerm(); err == nil {
+						var line string
+
+						defer clt.StopTerm()
+
+						fmt.Println(fmt.Sprintf("ECAL %v", config.ProductVersion))
+						fmt.Println("Type 'q' or 'quit' to exit the shell and '?' to get help")
+
+						line, err = clt.NextLine()
+						for err == nil && !isExitLine(line) {
+
+							// Process the entered line
+
+							if line == "?" {
+
+								// Show help
+
+								clt.WriteString(fmt.Sprintf("ECAL %v\n", config.ProductVersion))
+								clt.WriteString(fmt.Sprintf("\n"))
+								clt.WriteString(fmt.Sprintf("Console supports all normal ECAL statements and the following special commands:\n"))
+								clt.WriteString(fmt.Sprintf("\n"))
+								clt.WriteString(fmt.Sprintf("    !syms - List all available inbuild functions and available stdlib packages of ECAL.\n"))
+								clt.WriteString(fmt.Sprintf("    !stdl - List all available constants and functions of a stdlib package.\n"))
+								clt.WriteString(fmt.Sprintf("    !lk   - Do a full text search through all docstrings.\n"))
+								clt.WriteString(fmt.Sprintf("\n"))
+
+							} else if line == "!funcs" {
+
+							} else if line == "!reset" {
+
+							} else {
+								var ierr error
+								var ast *parser.ASTNode
+								var res interface{}
+
+								if ast, ierr = parser.ParseWithRuntime("console input", line, erp); ierr == nil {
+
+									if ierr = ast.Runtime.Validate(); ierr == nil {
+
+										if res, ierr = ast.Runtime.Eval(vs, make(map[string]interface{})); ierr == nil && res != nil {
+											clt.WriteString(fmt.Sprintln(res))
+										}
+									}
+								}
+
+								if ierr != nil {
+									clt.WriteString(fmt.Sprintln(ierr.Error()))
+								}
+							}
+
+							line, err = clt.NextLine()
+						}
+					}
+				}
+			}
+		}
+	}
+
+	return err
+}

+ 0 - 27
eval.go

@@ -1,27 +0,0 @@
-/*
- * ECAL
- *
- * Copyright 2020 Matthias Ladkau. All rights reserved.
- *
- * This Source Code Form is subject to the terms of the MIT
- * License, If a copy of the MIT License was not distributed with this
- * file, You can obtain one at https://opensource.org/licenses/MIT.
- */
-
-/*
-Package eval contains the main API for the event condition language ECAL.
-*/
-package eval
-
-import "devt.de/krotik/ecal/util"
-
-// TODO: Maybe API documentation - access comments during runtime
-
-/*
-processor is the main implementation for the Processor interface.
-*/
-type processor struct {
-	// TODO: GM GraphManager is part of initial values published in the global scope
-
-	util.Logger
-}

+ 38 - 0
stdlib/stdlib.go

@@ -17,6 +17,44 @@ import (
 	"devt.de/krotik/ecal/util"
 )
 
+/*
+GetStdlibSymbols returns all available packages of stdlib and their constant
+and function symbols.
+*/
+func GetStdlibSymbols() ([]string, []string, []string) {
+	var constSymbols, funcSymbols []string
+	var packageNames []string
+
+	packageSet := make(map[string]bool)
+
+	addSym := func(sym string, suffix string, symMap map[interface{}]interface{},
+		ret []string) []string {
+
+		if strings.HasSuffix(sym, suffix) {
+			trimSym := strings.TrimSuffix(sym, suffix)
+			packageSet[trimSym] = true
+			for k := range symMap {
+				ret = append(ret, fmt.Sprintf("%v.%v", trimSym, k))
+			}
+		}
+
+		return ret
+	}
+
+	for k, v := range genStdlib {
+		sym := fmt.Sprint(k)
+		symMap := v.(map[interface{}]interface{})
+
+		constSymbols = addSym(sym, "-const", symMap, constSymbols)
+		funcSymbols = addSym(sym, "-func", symMap, funcSymbols)
+	}
+	for k := range packageSet {
+		packageNames = append(packageNames, k)
+	}
+
+	return packageNames, constSymbols, funcSymbols
+}
+
 /*
 GetStdlibConst looks up a constant from stdlib.
 */

+ 8 - 0
stdlib/stdlib_test.go

@@ -15,6 +15,14 @@ import (
 	"testing"
 )
 
+func TestSymbols(t *testing.T) {
+	p, c, f := GetStdlibSymbols()
+	if len(p) == 0 || len(c) == 0 || len(f) == 0 {
+		t.Error("Should have some entries in symbol lists:", p, c, f)
+		return
+	}
+}
+
 func TestSplitModuleAndName(t *testing.T) {
 
 	if m, n := splitModuleAndName("fmt.Println"); m != "fmt" || n != "Println" {

+ 100 - 0
util/logging.go

@@ -12,11 +12,76 @@ package util
 
 import (
 	"fmt"
+	"io"
 	"log"
+	"strings"
 
 	"devt.de/krotik/common/datautil"
 )
 
+// Loger with loglevel support
+// ===========================
+
+/*
+LogLevel represents a logging level
+*/
+type LogLevel string
+
+/*
+Log levels
+*/
+const (
+	Debug LogLevel = "debug"
+	Info           = "info"
+	Error          = "error"
+)
+
+/*
+LogLevelLogger is a wrapper around loggers to add log level functionality.
+*/
+type LogLevelLogger struct {
+	logger Logger
+	level  LogLevel
+}
+
+func NewLogLevelLogger(logger Logger, level string) (*LogLevelLogger, error) {
+	llevel := LogLevel(strings.ToLower(level))
+
+	if llevel != Debug && llevel != Info && llevel != Error {
+		return nil, fmt.Errorf("Invalid log level: %v", llevel)
+	}
+
+	return &LogLevelLogger{
+		logger,
+		llevel,
+	}, nil
+}
+
+/*
+LogError adds a new error log message.
+*/
+func (ll *LogLevelLogger) LogError(m ...interface{}) {
+	ll.logger.LogError(m...)
+}
+
+/*
+LogInfo adds a new info log message.
+*/
+func (ll *LogLevelLogger) LogInfo(m ...interface{}) {
+	if ll.level == Info || ll.level == Debug {
+		ll.logger.LogInfo(m...)
+	}
+}
+
+/*
+LogDebug adds a new debug log message.
+*/
+func (ll *LogLevelLogger) LogDebug(m ...interface{}) {
+	if ll.level == Debug {
+		ll.logger.LogDebug(m...)
+	}
+}
+
 // Logging implementations
 // =======================
 
@@ -153,3 +218,38 @@ LogDebug adds a new debug log message.
 */
 func (nl *NullLogger) LogDebug(m ...interface{}) {
 }
+
+/*
+BufferLogger logs into a buffer.
+*/
+type BufferLogger struct {
+	buf io.Writer
+}
+
+/*
+NewNullLogger returns a buffer logger instance.
+*/
+func NewBufferLogger(buf io.Writer) *BufferLogger {
+	return &BufferLogger{buf}
+}
+
+/*
+LogError adds a new error log message.
+*/
+func (bl *BufferLogger) LogError(m ...interface{}) {
+	fmt.Fprintln(bl.buf, fmt.Sprintf("error: %v", fmt.Sprint(m...)))
+}
+
+/*
+LogInfo adds a new info log message.
+*/
+func (bl *BufferLogger) LogInfo(m ...interface{}) {
+	fmt.Fprintln(bl.buf, fmt.Sprintf("%v", fmt.Sprint(m...)))
+}
+
+/*
+LogDebug adds a new debug log message.
+*/
+func (bl *BufferLogger) LogDebug(m ...interface{}) {
+	fmt.Fprintln(bl.buf, fmt.Sprintf("debug: %v", fmt.Sprint(m...)))
+}

+ 58 - 0
util/logging_test.go

@@ -11,6 +11,7 @@
 package util
 
 import (
+	"bytes"
 	"fmt"
 	"testing"
 )
@@ -59,4 +60,61 @@ test` {
 	sol.LogDebug(nil, "test")
 	sol.LogInfo(nil, "test")
 	sol.LogError(nil, "test")
+
+	ml.Reset()
+
+	if _, err := NewLogLevelLogger(ml, "test"); err == nil || err.Error() != "Invalid log level: test" {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	ml.Reset()
+	ll, _ := NewLogLevelLogger(ml, "debug")
+	ll.LogDebug("l", "test1")
+	ll.LogInfo(nil, "test2")
+	ll.LogError("l", "test3")
+
+	if ml.String() != `debug: ltest1
+<nil>test2
+error: ltest3` {
+		t.Error("Unexpected result:", ml.String())
+		return
+	}
+
+	ml.Reset()
+	ll, _ = NewLogLevelLogger(ml, "info")
+	ll.LogDebug("l", "test1")
+	ll.LogInfo(nil, "test2")
+	ll.LogError("l", "test3")
+
+	if ml.String() != `<nil>test2
+error: ltest3` {
+		t.Error("Unexpected result:", ml.String())
+		return
+	}
+
+	ml.Reset()
+	ll, _ = NewLogLevelLogger(ml, "error")
+	ll.LogDebug("l", "test1")
+	ll.LogInfo(nil, "test2")
+	ll.LogError("l", "test3")
+
+	if ml.String() != `error: ltest3` {
+		t.Error("Unexpected result:", ml.String())
+		return
+	}
+
+	buf := bytes.NewBuffer(nil)
+	bl := NewBufferLogger(buf)
+	bl.LogDebug("l", "test1")
+	bl.LogInfo(nil, "test2")
+	bl.LogError("l", "test3")
+
+	if buf.String() != `debug: ltest1
+<nil>test2
+error: ltest3
+` {
+		t.Error("Unexpected result:", buf.String())
+		return
+	}
 }