diff --git a/cmd/ecli/.build.rc b/cmd/ecli/.build.rc new file mode 100644 index 0000000..21c59b1 --- /dev/null +++ b/cmd/ecli/.build.rc @@ -0,0 +1,14 @@ +# Builder resource file +# Created on gio 21 mag 2026, 15:35:18, CEST + +# Program name +PROGRAM_NAME="ecli" + +# Program version +PROGRAM_VERSION="$( /..." + msgln " ${prog} /..." + msgln " ${prog} --local Build the local exec" + msgln " ${prog} --init Create the resource file in the current directory" + msgln + if [ -r "${RESOURCE_FILE}" ]; then + msgln "Resource file '${RESOURCE_FILE}' content:" + cat >&2 ${RESOURCE_FILE} + else + msgln "Resource file '${RESOURCE_FILE}' not found" + fi +} + +function msgln() { + echo >&2 "${1}" +} + +function exitUsage() { + echo >&2 "${1}" + usage + exit 1 +} + +function exitMsg() { + echo >&2 "${1}" + exit 1 +} + +# CMDLINE: help +if [ "${1}" == "help,," ] || [ "${1,,}" == "--help" ] || [ "${1,,}" == "-h" ]; then + usage + exit +fi + +# CMDLINE: init +if [ "${1,,}" == "--init" ]; then + cat >"${RESOURCE_FILE}" <"${BUILD_REGISTER}" "${PROGRAM_VERSION} ${count}" + echo ${count} +} + +function build() { + local target=${1} ext cmd + IFS=/ read os cpu <<<"${p}" + #msgln "OS=${os}; CPU=${cpu}" + ext="" + if [ "${os}" == 'windows' ]; then + ext=".exe" + fi + cmd="GOOS='${os}' GOARCH='${cpu}' go build -o '${PROGRAM_NAME}_v${PROGRAM_VERSION}_${os}_${cpu}${ext}'" + eval "${cmd}" +} + +function buildLocal() { + local ext cmd + ext="" + if [[ "${OSTYPE}" =~ win.* ]]; then + ext=".exe" + fi + cmd="go build -o '${PROGRAM_NAME}${ext}'" + eval "${cmd}" +} + +function gitTag() { + local gopath gopkg mod + local tag + + if ! tag=$(git tag -l --sort=-version:refname "v[0-9]*.[0-9]*.[0-9]*"|head -1) || [ -z "${tag}" ]; then + gopath=$(go env GOPATH) + gopkg="${gopath}/pkg/mod/git.portale-stac.it/go-pkg" + if cd "${gopkg}" 2>/dev/null; then + mod=$(ls -1v |grep expr@|tail -1) + tag=${mod##*@} + cd - >/dev/null + fi + fi + echo ${tag} +} + +function gitTagDate() { + local tag_name=${1} + local tag_date + + if ! tag_date=$(git show --no-patch --format=%ci "${tag_name}") || [ -z "${tag_date}" ]; then + tag_date="n/a" + fi + echo ${tag_date} +} + +function createVersionSource() { + local tag tag_date + tag=$(gitTag) + if [ -z "${tag}" ]; then + tag="n/a" + else + tag_date=$(gitTagDate "${tag}") + fi + + cat >version.go <&2 "${1}" +} + +function exitMsg() { + echo >&2 "${1}" + exit 1 +} + +function readBuildCount() { + local reg ver count + if [ -r "${BUILD_REGISTER}" ]; then + reg=$(<"${BUILD_REGISTER}") + else + reg="${PROGRAM_VERSION} 0" + fi + read ver count <<<"${reg}" + if [ "${ver}" != "${PROGRAM_VERSION}" ]; then + count=0 + fi + echo ${count} +} + +if [ -r "${GITEA_PASSWORD_FILE}" ]; then + if ! PASSWORD=$(<"${GITEA_PASSWORD_FILE}"); then + exitMsg "Can're password file '${GITEA_PASSWORD_FILE}'" + fi +else + exitMsg "Password file '${GITEA_PASSWORD_FILE}' not found" +fi +if [ -z "${PASSWORD}" ]; then + exitMsg "Empty password. Please, check file '${GITEA_PASSWORD_FILE}'" +fi + +if [ -r "${RESOURCE_FILE}" ]; then + source "${RESOURCE_FILE}" +else + exitMsg "resource file '${RESOURCE_FILE}' not found" +fi + +if [ -r "${BUILD_REGISTER}" ]; then + BUILD_TAG=$(<"${BUILD_REGISTER}") +else + exitMsg "build register file '${BUILD_REGISTER}' not found" +fi +url="${GITEA_HOST}/${GITEA_BASE_PATH}/${GITEA_OWNER}/${GITEA_PKG_TYPE}/${PROGRAM_NAME}/${PROGRAM_VERSION}/files" +#echo "URL: ${url}" +#echo $(curl --user "${GITEA_USER}:${PASSWORD}" -X GET ${url}|jq '.[]."name"') + +declare -a files=( +$(curl --no-progress-meter --user "${GITEA_USER}:${PASSWORD}" -X GET ${url}|jq '.[]."name"') +) + +for name in ${files[@]}; do + filename=${name:1:${#name}-2} + name_terminal=${filename##*_} + filever=${name_terminal%%.*} + + if [ "${BUILD_TAG}" != "${PROGRAM_VERSION} ${filever}" ]; then + msgln "Deleting ${name}" + curl --no-progress-meter --user "${GITEA_USER}:${PASSWORD}" -X DELETE ${GITEA_HOST}/api/packages/${GITEA_OWNER}/${GITEA_PKG_TYPE}/${PROGRAM_NAME}/${PROGRAM_VERSION}/${filename} +# else +# echo "most recent version" + fi +done + +#curl --user "${GITEA_USER}:${PASSWORD}" -X GET https://git.portale-stac.it/api/v1/packages/go-pkg/generic/ecli/1.7.0/files diff --git a/cmd/ecli/commands.go b/cmd/ecli/commands.go new file mode 100644 index 0000000..01478c9 --- /dev/null +++ b/cmd/ecli/commands.go @@ -0,0 +1,236 @@ +// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com). +// All rights reserved. + +// commands.go +package main + +import ( + "fmt" + "io" + "os" + "strings" + + "git.portale-stac.it/go-pkg/expr" + "git.portale-stac.it/go-pkg/expr/kern" + "git.portale-stac.it/go-pkg/utils" +) + +type commandFunction func(opt *Options, ctx kern.ExprContext, args []string) (err error) + +type command struct { + name string + description string + code commandFunction +} + +func (cmd *command) exec(opt *Options, ctx kern.ExprContext, args []string) (err error) { + return cmd.code(opt, ctx, args) +} + +type commandHandler struct { + cmdIndex []string + commands map[string]*command +} + +func NewCommandHandler() *commandHandler { + return &commandHandler{ + cmdIndex: make([]string, 0, 20), + 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.cmdIndex = append(h.cmdIndex, name) + 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 +} + +// ------ +var cmdHandler *commandHandler + +func (h *commandHandler) help() { + fmt.Fprintln(os.Stderr, `--- REPL commands:`) + for _, name := range h.cmdIndex { + cmd := h.commands[name] + 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. + -B, --list-builtins List all builtin module names + -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 cmdExit(opt *Options, ctx kern.ExprContext, args []string) (err error) { + return io.EOF +} + +func cmdHelp(opt *Options, ctx kern.ExprContext, args []string) (err error) { + cmdHandler.help() + return +} + +func cmdMultiLine(opt *Options, ctx kern.ExprContext, args []string) (err error) { + if opt.formOpt&kern.MultiLine == 0 { + opt.formOpt |= kern.MultiLine + } else { + opt.formOpt &= ^kern.MultiLine + } + return +} + +func cmdTty(opt *Options, ctx kern.ExprContext, args []string) (err error) { + if opt.formOpt&kern.TTY == 0 { + opt.formOpt |= kern.TTY + } else { + opt.formOpt &= ^kern.TTY + } + return +} + +func execFile(opt *Options, ctx kern.ExprContext, fileName string) (err error) { + var fh *os.File + if fh, err = os.Open(fileName); err == nil { + goBatch(opt, ctx, fh, false) + fh.Close() + } + return +} + +func cmdSource(opt *Options, ctx kern.ExprContext, args []string) (err error) { + var target string + for _, arg := range args { + if len(arg) == 0 { + continue + } + // TODO migliorare questa parte: eventualmente valutare un'espressione + if target, err = checkStringLiteral(arg); err != nil { + break + } + + if target, err = utils.ExpandPath(target); err != nil { + break + } + + if isPattern(target) { + var fileNames []string + if fileNames, err = matchPathPattern(target); err == nil { + for _, fileName := range fileNames { + if err = execFile(opt, ctx, fileName); err != nil { + break + } + } + } + } else { + err = execFile(opt, ctx, target) + } + + if err != nil { + break + } + } + return +} + +func cmdModules(opt *Options, ctx kern.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(opt *Options, ctx kern.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(opt *Options, ctx kern.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 setupCommands() { + 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) +} diff --git a/cmd/ecli/ecli.adoc b/cmd/ecli/ecli.adoc new file mode 100644 index 0000000..48cd024 --- /dev/null +++ b/cmd/ecli/ecli.adoc @@ -0,0 +1,314 @@ += Ecli +Expression Calculator Interactive Tool +:authors: Celestino Amoroso +:email: celestino.amoroso@gmail.com +:docinfo: shared +:encoding: utf-8 +:toc: right +:toclevels: 4 +:icons: font +:icon-set: fi +:numbered: +:data-uri: +:docinfo1: +:sectlinks: +:sectanchors: +:source-highlighter: rouge +:rouge-style: manni +:stylesdir: /home/share/s3-howto/styles +:stylesheet: adoc-colony.css + +// Workaround to manage double-column in back-tick quotes +:2c: :: +// Workaround to manage double-plus in back-tick quotes +:plusplus: ++ +// Workaround to manage asterisk in back-tick quotes +:star: * + +#Generated by Copilot# + +== Overview + +`ecli` (Expression Calculator Interactive Tool) is an interactive REPL (Read-Eval-Print Loop) application for evaluating expressions using the Expr package. It provides a powerful command-line interface for expression evaluation with support for multiple builtin modules, file operations, and interactive scripting. + +The tool combines the expression evaluation capabilities of the Expr package with an interactive shell environment, making it ideal for: + +- Interactive expression testing and prototyping +- Batch expression evaluation from scripts +- Data processing and transformation +- Mathematical computations with fractions and complex operators + +== Getting Started + +=== Installation + +To build and install `ecli`: + +[source,bash] +---- +cd cmd/ecli +./build.bash +---- + +The compiled binary will be available as `ecli` in the current directory. + +=== Basic Usage + +Start the interactive REPL: + +[source,bash] +---- +./ecli +---- + +You'll see the prompt `>>> ` where you can enter expressions to evaluate. + +=== Command Line Options + +[cols="1,4", options="header"] +|=== +| Option | Description + +| `-e ` | Evaluate an expression directly without entering REPL mode +| `-i` | Force REPL operation after processing all `-e` options +| `-b ` | Import builtin modules (comma-separated list, glob patterns, or 'all') +| `-B, --list-builtins` | List all available builtin module names +| `-m, --modules` | List all builtin modules +| `-p` | Print expressions in prefix form +| `-t` | Print expressions in tree form +| `--noout` | Disable printing of expression results +| `-h, --help` | Show help message +| `-v, --version` | Show program version +|=== + +== Interactive Commands + +Within the REPL, you can use the following commands: + +[cols="2,5", options="header"] +|=== +| Command | Description + +| `help` | Display available commands and command-line options +| `exit` | Exit the REPL +| `multiline` | Toggle multi-line input mode for complex expressions +| `tty` | Toggle TTY mode +| `source ` | Execute expressions from a file +|=== + +== Features + +=== Expression Evaluation + +`ecli` supports the full expression language provided by the Expr package, including: + +- **Arithmetic Operations**: Addition, subtraction, multiplication, division, modulo +- **Bitwise Operations**: AND, OR, XOR, NOT, shift operations +- **Boolean Logic**: AND, OR, NOT operations +- **Relational Operators**: Comparison and equality operators +- **String Operations**: Concatenation and string manipulation +- **Iterators**: Range, list, and custom iterators +- **Functions**: Builtin and user-defined functions +- **Collections**: Lists, dictionaries, and linked lists +- **Fractions**: Support for fractional arithmetic + +=== Builtin Modules + +`ecli` provides access to various builtin modules through the `-b` option: + +[cols="1,4", options="header"] +|=== +| Module | Functionality + +| `base` | Core expression evaluation functions +| `fmt` | String formatting and output functions +| `string` | String manipulation functions +| `math-arith` | Mathematical and arithmetic operations +| `iterator` | Iterator-related functions +| `os-file` | File I/O operations +| `import` | Module import functionality +|=== + +Use `-B` or `--list-builtins` to see all available modules: + +[source,bash] +---- +./ecli --list-builtins +---- + +== Examples + +=== Basic Arithmetic + +[source,bash] +---- +>>> 2 + 3 * 4 +14 +>>> (2 + 3) * 4 +20 +---- + +=== String Operations + +[source,bash] +---- +>>> "hello" + " " + "world" +hello world +---- + +=== Using Iterators + +[source,bash] +---- +>>> [1, 2, 3, 4, 5] | map(. * 2) +[2, 4, 6, 8, 10] +---- + +=== Evaluating from Command Line + +[source,bash] +---- +./ecli -e "2 + 2" +4 +---- + +=== Loading Builtin Modules + +[source,bash] +---- +./ecli -b "math-arith,string" +---- + +=== Loading Expressions from Files + +Inside the REPL: + +[source] +---- +>>> source "expressions.expr" +---- + +Or from command line: + +[source,bash] +---- +./ecli -e '@include "expressions.expr"' +---- + +== Configuration + +=== Resource Files + +`ecli` supports startup resource files: + +- `.ecli.rc` - Main configuration file +- `.ecli.rc.d/` - Directory for modular configuration files + +These files are automatically loaded at startup if they exist in the current directory or home directory. + +== Building from Source + +=== Prerequisites + +- Go 1.18 or later +- Make or bash shell + +=== Build Steps + +[source,bash] +---- +cd cmd/ecli +./build.bash +---- + +=== Build Artifacts + +The build process generates: + +- `ecli` - The main executable +- `version.txt` - Version information +- Platform-specific binaries (e.g., `ecli_v1.17.0_linux_amd64`, `ecli_v1.17.0_darwin_arm64`) + +== Advanced Usage + +=== Multi-line Input + +For complex expressions, toggle multi-line mode: + +[source] +---- +>>> multiline +>>> result = [1, 2, 3, 4, 5] +... | filter(. > 2) +... | map(. * 2) +>>> result +[6, 8, 10] +---- + +=== Script Execution + +Create a file `calculations.expr`: + +[source] +---- +x = 10 +y = 20 +result = x + y * 2 +---- + +Execute it: + +[source,bash] +---- +./ecli -e '@source "calculations.expr"' -e 'result' +---- + +=== Chaining Operations + +[source] +---- +>>> data = [{"name": "alice", "age": 30}, {"name": "bob", "age": 25}] +>>> data | map(.name) +[alice, bob] +---- + +== Troubleshooting + +=== Expression Parsing Errors + +If you encounter parsing errors, check: + +- Bracket matching and quotation marks +- Operator precedence +- Variable and function names + +=== Module Loading Issues + +Verify available modules: + +[source,bash] +---- +./ecli --list-builtins +---- + +=== File Not Found Errors + +Ensure file paths are: + +- Properly quoted in expressions +- Relative to the current working directory or absolute paths +- Readable by the current user + +== Related Documentation + +- link:../../../README.adoc[Expr Package Documentation] +- Expr Expression Language Syntax +- Builtin Modules Reference + +== Version History + +For version information and changes, see the link:version.txt[version file]. + +== License + +Copyright (c) 2024-2026 Celestino Amoroso (celestino.amoroso@gmail.com). All rights reserved. diff --git a/cmd/ecli/go.mod b/cmd/ecli/go.mod new file mode 100644 index 0000000..88ea571 --- /dev/null +++ b/cmd/ecli/go.mod @@ -0,0 +1,15 @@ +module ecli + +go 1.24.0 + +require ( + git.portale-stac.it/go-pkg/expr v0.33.0 + git.portale-stac.it/go-pkg/utils v0.3.0 + github.com/ergochat/readline v0.1.3 +) + +require ( + golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect +) diff --git a/cmd/ecli/go.sum b/cmd/ecli/go.sum new file mode 100644 index 0000000..50bc779 --- /dev/null +++ b/cmd/ecli/go.sum @@ -0,0 +1,36 @@ +git.portale-stac.it/go-pkg/expr v0.1.0 h1:7xGEuUhdh6RRFaRbRnLVqVJBmHJWHfqjDBm2K0fIW2s= +git.portale-stac.it/go-pkg/expr v0.1.0/go.mod h1:kUFEQkUMCJ1IiUKkL0P5/vznaAIzFI26Xf5P0rTXqR0= +git.portale-stac.it/go-pkg/expr v0.2.0 h1:AAaVsV0uaC4EikKU91VuubIpbIN7wuya7t4avyFgg+0= +git.portale-stac.it/go-pkg/expr v0.2.0/go.mod h1:DZqqZ3A9h4qEOs7yMvG4VZq7B/xhFsYqC3IKd3M2VKc= +git.portale-stac.it/go-pkg/expr v0.17.0 h1:4ANGwJfwJO3AmnKka4Cf1AO9/ckGLMj8RIWeoDFKawQ= +git.portale-stac.it/go-pkg/expr v0.17.0/go.mod h1:DZqqZ3A9h4qEOs7yMvG4VZq7B/xhFsYqC3IKd3M2VKc= +git.portale-stac.it/go-pkg/expr v0.32.0 h1:ikXqHjJslIGkD79G1/51xe+c25TFi2CslJ6nu8mOuJY= +git.portale-stac.it/go-pkg/expr v0.32.0/go.mod h1:R2TYIahLtD8YDgNEHtgHCQdoEUZ7yCQsMHyJXhJijmw= +git.portale-stac.it/go-pkg/expr v0.33.0 h1:GJ7PPgA1689GSC/cUWGYm08jn7qMmkp0FMQf/As5sCw= +git.portale-stac.it/go-pkg/expr v0.33.0/go.mod h1:R2TYIahLtD8YDgNEHtgHCQdoEUZ7yCQsMHyJXhJijmw= +git.portale-stac.it/go-pkg/utils v0.2.0 h1:2l4IVUhElzjaIUJlahPG2DZTGb9x7OXuFTO4z1K6LmY= +git.portale-stac.it/go-pkg/utils v0.2.0/go.mod h1:PebQ45Qbe89aMTd3wcbcx1bkpNRW4/frNLnpuyZYovU= +git.portale-stac.it/go-pkg/utils v0.3.0 h1:kCJ3+XcekV7in/SieJjiswdtJKMBS0RTJMlG2fW5mK0= +git.portale-stac.it/go-pkg/utils v0.3.0/go.mod h1:PebQ45Qbe89aMTd3wcbcx1bkpNRW4/frNLnpuyZYovU= +github.com/ergochat/readline v0.1.0 h1:KEIiAnyH9qGZB4K8oq5mgDcExlEKwmZDcyyocgJiABc= +github.com/ergochat/readline v0.1.0/go.mod h1:o3ux9QLHLm77bq7hDB21UTm6HlV2++IPDMfIfKDuOgY= +github.com/ergochat/readline v0.1.1 h1:C8Uuo3ybB23GWOt0uxmHbGzKM9owmtXary6Clrj84s0= +github.com/ergochat/readline v0.1.1/go.mod h1:o3ux9QLHLm77bq7hDB21UTm6HlV2++IPDMfIfKDuOgY= +github.com/ergochat/readline v0.1.3 h1:/DytGTmwdUJcLAe3k3VJgowh5vNnsdifYT6uVaf4pSo= +github.com/ergochat/readline v0.1.3/go.mod h1:o3ux9QLHLm77bq7hDB21UTm6HlV2++IPDMfIfKDuOgY= +golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0= +golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= diff --git a/cmd/ecli/graph.go b/cmd/ecli/graph.go new file mode 100644 index 0000000..4e641b0 --- /dev/null +++ b/cmd/ecli/graph.go @@ -0,0 +1,18 @@ +// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com). +// All rights reserved. + +//go:build graph + +// graph.go +package main + +import ( + "fmt" + + "git.portale-stac.it/go-pkg/expr" +) + +func printGraph() { + r := expr.NewExprReticle(ast) + fmt.Println(r.String()) +} diff --git a/cmd/ecli/main.go b/cmd/ecli/main.go new file mode 100644 index 0000000..7425792 --- /dev/null +++ b/cmd/ecli/main.go @@ -0,0 +1,415 @@ +// 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/expr/kern" + "git.portale-stac.it/go-pkg/expr/scan" + "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(ctx kern.ExprContext, opt *Options) (err error) { + for _, spec := range opt.builtin { + if moduleSpec, ok := spec.(string); ok { + if moduleSpec == "all" { + moduleSpec = "*" + } + _, err = expr.ImportInContextByGlobPattern(ctx, moduleSpec) + } else if moduleSpec, ok := spec.([]string); ok { + notFoundList := make([]string, 0) + for _, name := range moduleSpec { + if !expr.ImportInContext(ctx, 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 kern.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 kern.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 scan.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 kern.ExprContext, r io.Reader, outputEnabled bool) { + 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, outputEnabled) + } + } + sb.Reset() + } +} + +func compute(opt *Options, ctx kern.ExprContext, r io.Reader, outputEnabled bool) { + scanner := scan.NewScanner(r, scan.DefaultTranslations()) + parser := expr.NewParser() + + if ast, err := parser.Parse(scanner); err == nil { + if opt.printPrefix { + fmt.Println(ast) + } + if opt.printTree { + printGraph() + } + + 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.(kern.Formatter); ok { + fmt.Println(f.ToString(opt.formOpt)) + } else if kern.IsInteger(result) { + fmt.Printf(opt.baseVerb, result) + fmt.Println() + } else if kern.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 kern.ExprContext) { + const ( + devParamProp = "prop" + devParamDigits = "digits" + ) + + aboutFunc := func(ctx kern.ExprContext, name string, args map[string]any) (result any, err error) { + result = about() + return + } + + ctrlListFunc := func(ctx kern.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 = kern.ListFromStrings(vars) + return + } + + ctrlFunc := func(ctx kern.ExprContext, name string, args map[string]any) (result any, err error) { + varName, _ := args[devParamProp].(string) + if len(args) == 1 { + result = expr.GlobalCtrlGet(ctx, varName) + } else { + result = expr.GlobalCtrlSet(ctx, varName, args[kern.ParamValue]) + } + return + } + + envSetFunc := func(ctx kern.ExprContext, name string, args map[string]any) (result any, err error) { + var varName, value string + var ok bool + if varName, ok = args[kern.ParamName].(string); !ok { + err = kern.ErrExpectedGot(name, kern.TypeString, args[kern.ParamName]) + return + } + if value, ok = args[kern.ParamValue].(string); !ok { + err = kern.ErrExpectedGot(name, kern.TypeString, args[kern.ParamValue]) + return + } + if err = os.Setenv(varName, value); err == nil { + result = value + } + return + } + + envGetFunc := func(ctx kern.ExprContext, name string, args map[string]any) (result any, err error) { + var varName string + var ok bool + if varName, ok = args[kern.ParamName].(string); !ok { + err = kern.ErrExpectedGot(name, kern.TypeString, args[kern.ParamName]) + return + } + + if result, ok = os.LookupEnv(varName); !ok { + err = fmt.Errorf("environment variable %q does not exist", varName) + } + return + } + + envListFunc := func(ctx kern.ExprContext, name string, args map[string]any) (result any, err error) { + env := os.Environ() + vars := make([]string, 0, len(env)) + for _, e := range env { + name, _, _ := strings.Cut(e, "=") + vars = append(vars, name) + } + result = kern.ListFromStrings(vars) + return + } + + binFunc := func(ctx kern.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[kern.ParamValue].(int64); !ok { + err = kern.ErrExpectedGot(name, kern.TypeInt, args[kern.ParamValue]) + return + } + if digits, ok = args[devParamDigits].(int64); !ok { + err = kern.ErrExpectedGot(name, kern.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< 0 && spec[len(spec)-1] != '\n' { + spec += "\n" + } + 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 "-B", "--list-builtins": + listBuiltins() + os.Exit(0) + 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 +} diff --git a/cmd/ecli/publish.bash b/cmd/ecli/publish.bash new file mode 100755 index 0000000..7949e58 --- /dev/null +++ b/cmd/ecli/publish.bash @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com). +# All rights reserved. + +RESOURCE_FILE=".build.rc" +BUILD_REGISTER=".build_register" +PASSWORD= +GITEA_USER="camoroso" +GITEA_PASSWORD_FILE="${HOME}/.gitea_password" +GITEA_OWNER="go-pkg" +GITEA_HOST="https://git.portale-stac.it" +GITEA_BASE_PATH="api/packages" +GITEA_PKG_TYPE="generic" + +function exitMsg() { + echo >&2 "${1}" + exit 1 +} + +function readBuildCount() { + local reg ver count + if [ -r "${BUILD_REGISTER}" ]; then + reg=$(<"${BUILD_REGISTER}") + else + reg="${PROGRAM_VERSION} 0" + fi + read ver count <<<"${reg}" + if [ "${ver}" != "${PROGRAM_VERSION}" ]; then + count=0 + fi + echo ${count} +} + +if [ -r "${GITEA_PASSWORD_FILE}" ]; then + if ! PASSWORD=$(<"${GITEA_PASSWORD_FILE}"); then + exitMsg "Can're password file '${GITEA_PASSWORD_FILE}'" + fi +else + exitMsg "Password file '${GITEA_PASSWORD_FILE}' not found" +fi +if [ -z "${PASSWORD}" ]; then + exitMsg "Empty password. Please, check file '${GITEA_PASSWORD_FILE}'" +fi + +if ! ./build.bash; then + exitMsg "Build program failed" +fi + +if ! source "${RESOURCE_FILE}"; then + exitMsg "Loading resource file failed" +fi + +if ! exeList=$(echo 2>/dev/null ${PROGRAM_NAME}_v${PROGRAM_VERSION}_*); then + exitMsg "No executable found" +fi + +buildCount=$(readBuildCount) +fileCount=0 +for exe in ${exeList}; do + if [ "${exe/tar.gz/}" != "${exe}" ]; then + continue + fi + ((fileCount++)) + dir="${exe}_${buildCount}" + dist="${dir}.tar.gz" + rm -f "${dist}" + printf "%2d: %-30s --> %s\n" "${fileCount}" "${exe}" "${dist}" + mkdir "${dir}" + cp "${exe}" "${dir}/${PROGRAM_NAME}" + tar czf "${dist}" "${dir}" + rm -fR "${dir}" + + url="${GITEA_HOST}/${GITEA_BASE_PATH}/${GITEA_OWNER}/${GITEA_PKG_TYPE}/${PROGRAM_NAME}/${PROGRAM_VERSION}/${dist}" +# echo "${url}" + curl --user "${USER}:${PASSWORD}" --upload-file "${dist}" "${url}" + + rm -f "${dist}" +done + diff --git a/cmd/ecli/util-string.go b/cmd/ecli/util-string.go new file mode 100644 index 0000000..dc9955a --- /dev/null +++ b/cmd/ecli/util-string.go @@ -0,0 +1,23 @@ +// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com). +// All rights reserved. + +// util-string.go +package main + +import ( + "fmt" +) + +func checkStringLiteral(literal string) (value string, err error) { + length := len(literal) + if length >= 2 { + if (literal[0] == '"' && literal[length-1] == '"') || literal[0] == '\'' && literal[length-1] == '\'' { + value = literal[1 : length-1] + } else { + err = fmt.Errorf("unquoted or partially quoted string literal: `%s`", literal) + } + } else { + err = fmt.Errorf("invalid string literal: `%s`", literal) + } + return +} \ No newline at end of file diff --git a/cmd/ecli/version.txt b/cmd/ecli/version.txt new file mode 100644 index 0000000..092afa1 --- /dev/null +++ b/cmd/ecli/version.txt @@ -0,0 +1 @@ +1.17.0