// 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" ) type commandFunction func(ctx expr.ExprContext, args []string) (err error) type command struct { name string description string code commandFunction } func (cmd *command) exec(ctx expr.ExprContext, args []string) (err error) { return cmd.code(ctx, args) } type commandHandler struct { commands map[string]*command // ctx expr.ExprContext } func NewCommandHandler() *commandHandler { return &commandHandler{ commands: make(map[string]*command), } } // func (h *commandHandler) setContext(ctx expr.ExprContext) { // h.ctx = ctx // } func (h *commandHandler) add(name, description string, f commandFunction) { h.commands[name] = &command{name: name, description: description, code: f} } func (h *commandHandler) get(cmdLine string) (cmd *command, args []string) { if len(cmdLine) > 0 { tokens := strings.Split(cmdLine, " ") name := tokens[0] args = make([]string, 0, len(tokens)-1) if cmd = h.commands[name]; cmd != nil && len(tokens) > 1 { for _, tk := range tokens[1:] { if tk != "" { args = append(args, tk) } } } } return } // ------ type Options struct { printTree bool printPrefix bool forceInteractive bool builtin []any expressions []io.Reader formOpt expr.FmtOpt baseVerb string base int output bool rcCount int } func NewOptions() *Options { return &Options{ expressions: make([]io.Reader, 0), builtin: make([]any, 0), formOpt: expr.Base10, baseVerb: "%d", base: 10, output: true, rcCount: 0, } } 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 (opt *Options) loadRc() { var rcPath string var fh *os.File var err error if rcPath, err = utils.ExpandPath("~/.dev-expr.rc"); err != nil { return } if fh, err = os.Open(rcPath); err == nil { opt.expressions = append(opt.expressions, fh) opt.rcCount++ } } func (opt *Options) parseArgs() (err error) { for i := 1; i < len(os.Args) && err == nil; i++ { arg := os.Args[i] switch arg { case "-i": opt.forceInteractive = true case "-t": opt.printTree = true case "-p": opt.printPrefix = true case "-e": if i+1 < len(os.Args) { i++ spec := os.Args[i] if strings.HasPrefix(spec, "@") { var f *os.File if f, err = os.Open(spec[1:]); err == nil { opt.expressions = append(opt.expressions, f) } else { return } } else { opt.expressions = append(opt.expressions, strings.NewReader(spec)) } } else { err = errOptValueRequired(arg) } case "-b": if i+1 < len(os.Args) { i++ specs := strings.Split(os.Args[i], ",") if len(specs) == 1 { opt.builtin = append(opt.builtin, specs[0]) } else { opt.builtin = append(opt.builtin, specs) } } else { err = errOptValueRequired(arg) } case "-m", "--modules": expr.IterateBuiltinModules(func(name, description string, _ bool) bool { fmt.Printf("%20q: %s\n", name, description) return true }) os.Exit(0) case "--noout": opt.output = false case "-h", "--help", "help": cmdHandler.help() os.Exit(0) case "-v", "--version", "version", "about": fmt.Println(about()) os.Exit(0) default: err = fmt.Errorf("invalid option nr %d %q", i+1, arg) } } return } // ------ var cmdHandler *commandHandler func (h *commandHandler) help() { fmt.Fprintln(os.Stderr, `--- REPL commands:`) for _, cmd := range h.commands { fmt.Fprintf(os.Stderr, "%12s -- %s\n", cmd.name, cmd.description) } fmt.Fprint(os.Stderr, ` --- Command line options: -b Import builtin modules. can be a list of module names or a glob-pattern. Use the special value 'all' or the pattern '*' to import all modules. -e Evaluate instead of standard-input -i Force REPL operation when all -e occurences have been processed -h, --help, help Show this help menu -m, --modules List all builtin modules --noout Disable printing of expression results -p Print prefix form -t Print tree form -v, --version Show program 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 len(line) > 0 && line[len(line)-1] == '\\' { sb.WriteString(line[0 : len(line)-1]) 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(ctx, args); err != nil { 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 strings.HasSuffix(line, "\\\n") { sb.WriteString(line[0 : len(line)-2]) fmt.Print(contPrompt) 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(ctx, args); err != nil { break } } else { r := strings.NewReader(source) compute(opt, ctx, r, true) } } sb.Reset() fmt.Print(mainPrompt) } fmt.Println() } 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 strings.HasSuffix(line, "\\\n") { sb.WriteString(line[0 : len(line)-2]) 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(ctx, args); err != nil { 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()) } outputEnabled = outputEnabled && opt.output if result, err := ast.Eval(ctx); err == nil { if outputEnabled { 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) { aboutFunc := func(ctx expr.ExprContext, name string, args []any) (result any, err error) { result = about() return } ctrlListFunc := func(ctx expr.ExprContext, name string, args []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 []any) (result any, err error) { varName, _ := args[0].(string) if len(args) == 1 { result = expr.GlobalCtrlGet(varName) } else { result = expr.GlobalCtrlSet(varName, args[1]) } return } envSetFunc := func(ctx expr.ExprContext, name string, args []any) (result any, err error) { var varName, value string var ok bool if varName, ok = args[0].(string); !ok { err = expr.ErrExpectedGot(name, expr.TypeString, args[0]) return } if value, ok = args[1].(string); !ok { err = expr.ErrExpectedGot(name, expr.TypeString, args[1]) return } if err = os.Setenv(varName, value); err == nil { result = value } return } envGetFunc := func(ctx expr.ExprContext, name string, args []any) (result any, err error) { var varName string var ok bool if varName, ok = args[0].(string); !ok { err = expr.ErrExpectedGot(name, expr.TypeString, args[0]) 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("prop"), 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), }) } var opt *Options func main() { 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() } } } if opt.output { printResult(opt, ctx.GetLast()) } } // -------- func cmdExit(ctx expr.ExprContext, args []string) (err error) { return io.EOF } func cmdHelp(ctx expr.ExprContext, args []string) (err error) { cmdHandler.help() return } func cmdMultiLine(ctx expr.ExprContext, args []string) (err error) { if opt.formOpt&expr.MultiLine == 0 { opt.formOpt |= expr.MultiLine } else { opt.formOpt &= ^expr.MultiLine } return } func cmdTty(ctx expr.ExprContext, args []string) (err error) { if opt.formOpt&expr.TTY == 0 { opt.formOpt |= expr.TTY } else { opt.formOpt &= ^expr.TTY } return } func cmdSource(ctx expr.ExprContext, args []string) (err error) { var fh *os.File for _, arg := range args { length := len(arg) if length == 0 { continue } if length >= 2 { if (arg[0] == '"' && arg[length-1] == '"') || arg[0] == '\'' && arg[length-1] == '\'' { arg = arg[1 : length-1] } } if fh, err = os.Open(arg); err == nil { goBatch(opt, ctx, fh) fh.Close() } else { break } } return } func cmdModules(ctx expr.ExprContext, args []string) (err error) { expr.IterateBuiltinModules(func(name, description string, imported bool) bool { var check rune = ' ' if imported { check = '*' } fmt.Printf("%c %20q: %s\n", check, name, description) return true }) return } func cmdBase(ctx expr.ExprContext, args []string) (err error) { if len(args) == 0 { fmt.Println(opt.base) } else if args[0] == "2" { opt.baseVerb = "0b%b" opt.base = 2 } else if args[0] == "8" { opt.baseVerb = "0o%o" opt.base = 8 } else if args[0] == "10" { opt.baseVerb = "%d" opt.base = 10 } else if args[0] == "16" { opt.baseVerb = "0x%x" opt.base = 16 } else { err = fmt.Errorf("invalid number base %s", args[0]) } return } func cmdOutput(ctx expr.ExprContext, args []string) (err error) { var outputArg string if len(args) == 0 { outputArg = "status" } else { outputArg = strings.ToLower(args[0]) } switch outputArg { case "on": opt.output = true case "off": opt.output = false case "status": if opt.output { fmt.Println("on") } else { fmt.Println("off") } default: err = fmt.Errorf("output: unknown option %q", outputArg) } return } //------------------ func init() { cmdHandler = NewCommandHandler() cmdHandler.add("base", "Set the integer output base: 2, 8, 10, or 16", cmdBase) cmdHandler.add("exit", "Exit the program", cmdExit) cmdHandler.add("help", "Show command list", cmdHelp) cmdHandler.add("ml", "Enable/Disable multi-line output", cmdMultiLine) cmdHandler.add("mods", "List builtin modules", cmdModules) cmdHandler.add("output", "Enable/Disable printing expression results. Options 'on', 'off', 'status'", cmdOutput) cmdHandler.add("source", "Load a file as input", cmdSource) cmdHandler.add("tty", "Enable/Disable ansi output", cmdTty) }