401 lines
10 KiB
Go
401 lines
10 KiB
Go
// 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 + ` (` + EXPR_DATE + `)
|
|
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"
|
|
devParamDigits = "digits"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
binFunc := func(ctx expr.ExprContext, name string, args map[string]any) (result any, err error) {
|
|
var value, digits int64
|
|
var ok bool
|
|
var sb strings.Builder
|
|
|
|
if value, ok = args[expr.ParamValue].(int64); !ok {
|
|
err = expr.ErrExpectedGot(name, expr.TypeInt, args[expr.ParamValue])
|
|
return
|
|
}
|
|
if digits, ok = args[devParamDigits].(int64); !ok {
|
|
err = expr.ErrExpectedGot(name, expr.TypeInt, args[devParamDigits])
|
|
return
|
|
}
|
|
if digits != 64 && digits != 32 && digits != 16 && digits != 8 {
|
|
err = fmt.Errorf("%s param allows 8, 16, 32, or 64 values only", devParamDigits)
|
|
return
|
|
}
|
|
|
|
mask := uint64(0)
|
|
for i := 0; i < int(digits); i++ {
|
|
mask |= (1 << i)
|
|
}
|
|
maskedValue := uint64(value) & mask
|
|
// if maskedValue != uint64(value) {
|
|
// err = fmt.Errorf("%s param (%d) is not compatible with the value (%d) of %s param", expr.ParamValue, value, digits, devParamDigits)
|
|
// return
|
|
// }
|
|
|
|
for i := int(digits) - 1; i >= 0; i-- {
|
|
if maskedValue&(1<<i) == 0 {
|
|
sb.WriteByte('0')
|
|
} else {
|
|
sb.WriteByte('1')
|
|
}
|
|
}
|
|
|
|
result = sb.String()
|
|
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),
|
|
})
|
|
|
|
ctx.RegisterFunc("bin", expr.NewGolangFunctor(binFunc), expr.TypeInt, []expr.ExprFuncParam{
|
|
expr.NewFuncParam(expr.ParamValue),
|
|
expr.NewFuncParamFlagDef(devParamDigits, expr.PfOptional|expr.PfDefault, int64(8)),
|
|
})
|
|
}
|
|
|
|
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())
|
|
// }
|
|
}
|