// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com). // All rights reserved. // main.go package main import ( "bufio" "fmt" "io" "os" "strings" "git.portale-stac.it/go-pkg/expr" "git.portale-stac.it/go-pkg/utils" // https://pkg.go.dev/github.com/ergochat/readline#section-readme "github.com/ergochat/readline" ) const ( intro = PROGNAME + ` -- Expressions calculator ` + VERSION + ` Based on the Expr package ` + EXPR_VERSION + ` Type help to get the list of available commands See also https://git.portale-stac.it/go-pkg/expr/src/branch/main/README.adoc ` mainPrompt = ">>> " contPrompt = "... " historyFile = "~/.expr_history" ) // ------ func errOptValueRequired(opt string) error { return fmt.Errorf("option %q requires a value", opt) } func about() string { return PROGNAME + " -- " + VERSION + "; Expr package " + EXPR_VERSION } func importBuiltins(opt *Options) (err error) { for _, spec := range opt.builtin { if moduleSpec, ok := spec.(string); ok { if moduleSpec == "all" { moduleSpec = "*" } _, err = expr.ImportInContextByGlobPattern(moduleSpec) } else if moduleSpec, ok := spec.([]string); ok { notFoundList := make([]string, 0) for _, name := range moduleSpec { if !expr.ImportInContext(name) { notFoundList = append(notFoundList, name) } } if len(notFoundList) > 0 { err = fmt.Errorf("not found modules: %s", strings.Join(notFoundList, ",")) } } } return } func initReadlineConfig(cfg *readline.Config) { if histfile, err := utils.ExpandPath(historyFile); err == nil { cfg.HistoryFile = histfile } cfg.Undo = true cfg.DisableAutoSaveHistory = true } func goInteractiveReadline(opt *Options, ctx expr.ExprContext, r io.Reader) { var sb strings.Builder var cfg readline.Config initReadlineConfig(&cfg) rl, err := readline.NewFromConfig(&cfg) if err != nil { goInteractive(opt, ctx, r) return } defer rl.Close() fmt.Print(intro) rl.SetPrompt(mainPrompt) for line, err := rl.ReadLine(); err == nil; line, err = rl.ReadLine() { if continuation(&sb, line) { rl.SetPrompt(contPrompt) continue } rl.SetPrompt(mainPrompt) sb.WriteString(line) source := strings.TrimSpace(sb.String()) if source != "" && !strings.HasPrefix(source, "//") { if cmd, args := cmdHandler.get(source); cmd != nil { rl.SaveToHistory(source) if err = cmd.exec(opt, ctx, args); err != nil { if err == io.EOF { err = nil break } else { fmt.Fprintln(os.Stderr, "Eval Error:", err) break } } } else { rl.SaveToHistory(source) r := strings.NewReader(source) compute(opt, ctx, r, true) } } sb.Reset() } fmt.Println() } func goInteractive(opt *Options, ctx expr.ExprContext, r io.Reader) { var sb strings.Builder fmt.Print(intro) fmt.Print(mainPrompt) reader := bufio.NewReaderSize(r, 1024) for line, err := reader.ReadString('\n'); err == nil && line != "exit\n"; line, err = reader.ReadString('\n') { if continuation(&sb, line) { continue } sb.WriteString(line) source := strings.TrimSpace(sb.String()) // fmt.Printf("source=%q\n", source) if source != "" && !strings.HasPrefix(source, "//") { if cmd, args := cmdHandler.get(source); cmd != nil { if err = cmd.exec(opt, ctx, args); err != nil { break } } else { r := strings.NewReader(source) compute(opt, ctx, r, true) } } sb.Reset() fmt.Print(mainPrompt) } fmt.Println() } func continuation(sb *strings.Builder, line string) (cont bool) { line = strings.TrimSpace(line) if strings.HasSuffix(line, "\\") { sb.WriteString(line[0 : len(line)-1]) cont = true } else if strings.HasSuffix(line, ";") { sb.WriteString(line) cont = true } else if len(line) > 0 { if expr.StringEndsWithOperator(line) { sb.WriteString(line) cont = true } else { fullInput := sb.String() + line if strings.Count(fullInput, "(") > strings.Count(fullInput, ")") || strings.Count(fullInput, "[") > strings.Count(fullInput, "]") || strings.Count(fullInput, "{") > strings.Count(fullInput, "}") { sb.WriteString(line) cont = true } } } return } func goBatch(opt *Options, ctx expr.ExprContext, r io.Reader) { var sb strings.Builder reader := bufio.NewReaderSize(r, 1024) for line, err := reader.ReadString('\n'); err == nil && line != "exit\n"; line, err = reader.ReadString('\n') { if continuation(&sb, line) { continue } sb.WriteString(line) source := strings.TrimSpace(sb.String()) // fmt.Printf("source=%q\n", source) if source != "" && !strings.HasPrefix(source, "//") { if cmd, args := cmdHandler.get(source); cmd != nil { if err = cmd.exec(opt, ctx, args); err != nil { fmt.Fprintln(os.Stderr, "Eval Error:", err) break } } else { r := strings.NewReader(source) compute(opt, ctx, r, false) } } sb.Reset() } } func compute(opt *Options, ctx expr.ExprContext, r io.Reader, outputEnabled bool) { scanner := expr.NewScanner(r, expr.DefaultTranslations()) parser := expr.NewParser() if ast, err := parser.Parse(scanner); err == nil { if opt.printPrefix { fmt.Println(ast) } if opt.printTree { r := expr.NewExprReticle(ast) fmt.Println(r.String()) } if result, err := ast.Eval(ctx); err == nil { if outputEnabled && opt.output { printResult(opt, result) } } else { fmt.Fprintln(os.Stderr, "Eval Error:", err) } } else { fmt.Fprintln(os.Stderr, "Parse Error:", err) } } func printResult(opt *Options, result any) { if f, ok := result.(expr.Formatter); ok { fmt.Println(f.ToString(opt.formOpt)) } else if expr.IsInteger(result) { fmt.Printf(opt.baseVerb, result) fmt.Println() } else if expr.IsString(result) { fmt.Printf("\"%s\"\n", result) } else { fmt.Println(result) } } func isReaderTerminal(r io.Reader) bool { if fh, ok := r.(*os.File); ok { return utils.StreamIsTerminal(fh) } return false } func registerLocalFunctions(ctx expr.ExprContext) { const ( devParamProp = "prop" ) aboutFunc := func(ctx expr.ExprContext, name string, args map[string]any) (result any, err error) { result = about() return } ctrlListFunc := func(ctx expr.ExprContext, name string, args map[string]any) (result any, err error) { vars := ctx.EnumVars(func(name string) bool { return len(name) > 0 && name[0] == '_' }) result = expr.ListFromStrings(vars) return } ctrlFunc := func(ctx expr.ExprContext, name string, args map[string]any) (result any, err error) { varName, _ := args[devParamProp].(string) if len(args) == 1 { result = expr.GlobalCtrlGet(varName) } else { result = expr.GlobalCtrlSet(varName, args[expr.ParamValue]) } return } envSetFunc := func(ctx expr.ExprContext, name string, args map[string]any) (result any, err error) { var varName, value string var ok bool if varName, ok = args[expr.ParamName].(string); !ok { err = expr.ErrExpectedGot(name, expr.TypeString, args[expr.ParamName]) return } if value, ok = args[expr.ParamValue].(string); !ok { err = expr.ErrExpectedGot(name, expr.TypeString, args[expr.ParamValue]) return } if err = os.Setenv(varName, value); err == nil { result = value } return } envGetFunc := func(ctx expr.ExprContext, name string, args map[string]any) (result any, err error) { var varName string var ok bool if varName, ok = args[expr.ParamName].(string); !ok { err = expr.ErrExpectedGot(name, expr.TypeString, args[expr.ParamName]) return } if result, ok = os.LookupEnv(varName); !ok { err = fmt.Errorf("environment variable %q does not exist", varName) } return } ctx.RegisterFunc("about", expr.NewGolangFunctor(aboutFunc), expr.TypeString, []expr.ExprFuncParam{}) ctx.RegisterFunc("ctrlList", expr.NewGolangFunctor(ctrlListFunc), expr.TypeListOfStrings, []expr.ExprFuncParam{}) ctx.RegisterFunc("ctrl", expr.NewGolangFunctor(ctrlFunc), expr.TypeAny, []expr.ExprFuncParam{ expr.NewFuncParam(devParamProp), expr.NewFuncParamFlag(expr.ParamValue, expr.PfOptional), }) ctx.RegisterFunc("envSet", expr.NewGolangFunctor(envSetFunc), expr.TypeString, []expr.ExprFuncParam{ expr.NewFuncParam(expr.ParamName), expr.NewFuncParam(expr.ParamValue), }) ctx.RegisterFunc("envGet", expr.NewGolangFunctor(envGetFunc), expr.TypeString, []expr.ExprFuncParam{ expr.NewFuncParam(expr.ParamName), }) } func main() { setupCommands() opt := NewOptions() opt.loadRc() if err := opt.parseArgs(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if err := importBuiltins(opt); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } ctx := expr.NewSimpleStore() registerLocalFunctions(ctx) if len(opt.expressions) == opt.rcCount || opt.forceInteractive { opt.expressions = append(opt.expressions, os.Stdin) } for _, input := range opt.expressions { if isReaderTerminal(input) { goInteractiveReadline(opt, ctx, input) } else { goBatch(opt, ctx, input) if f, ok := input.(*os.File); ok { f.Close() } } } // TODO: why did I added these lines? // if opt.output { // printResult(opt, ctx.GetLast()) // } }