Compare commits

...

15 Commits

Author SHA1 Message Date
f100adead3 increased test coverage and splitted some utility functions by OS 2026-04-26 06:16:43 +02:00
7d2cf1e687 new operator 'join' 2026-04-25 07:00:30 +02:00
49728307f3 t_operator_test.go: added a test for the digest operator 2026-04-25 06:33:35 +02:00
20dc502438 new operator 'digest' 2026-04-25 06:25:39 +02:00
ce7bfc5e3f operator-map/filter.go: added deletion of temporary variables '_index' and '_count' 2026-04-25 06:24:28 +02:00
6e98bdd16b new operator 'filter' 2026-04-24 22:42:46 +02:00
6ee365bacc new operator 'map' ans general variable access by ${} 2026-04-24 20:11:25 +02:00
20d8236325 Iterators: Removed NewAnyIterator(), added NewIterator() 2026-04-24 06:26:00 +02:00
c39ee7cec0 dict-iterator.go: fixed behaviour of NewDictIterator() with no args 2026-04-23 22:06:23 +02:00
9e4252173b Iterator interface now embeds fmt.Stringer 2026-04-23 22:04:41 +02:00
2b80ba6789 t_plugin_test.go: TestLoadPluginName() removed due to linkage mismatch 2026-04-23 22:02:41 +02:00
d7247f97c5 doc: added description of set() and unset() functions defined in the base buiiltin 2026-04-23 19:09:09 +02:00
02df7f1c1f Plugin file name extension adapted to the host OS convention (.so on linux, .dll on windows, .dylib on Darwin/MacOS) 2026-04-23 19:06:11 +02:00
b6b09b2fb1 builtin-os-file: add function fileReadIterator() that creates an iterator over lines of a text file 2026-04-23 19:01:07 +02:00
0677180456 builtin-iterator.go/run(): removed default operator (print item) 2026-04-23 18:55:03 +02:00
37 changed files with 965 additions and 138 deletions

View File

@ -23,10 +23,12 @@ func parseRunArgs(localCtx ExprContext, args map[string]any) (it Iterator, op Fu
return
}
if op, ok = args[iterParamOperator].(Functor); !ok && args[iterParamOperator] != nil {
if args[iterParamOperator] != nil {
if op, ok = args[iterParamOperator].(Functor); !ok || op == nil {
err = fmt.Errorf("paramter %q must be a function, passed %v [%s]", iterParamOperator, args[iterParamOperator], TypeName(args[iterParamOperator]))
return
}
}
var vars *DictType
if vars, ok = args[iterParamVars].(*DictType); !ok && args[iterParamVars] != nil {
@ -51,7 +53,7 @@ func runFunc(ctx ExprContext, name string, args map[string]any) (result any, err
var ok bool
var op Functor
var v any
var usingDefaultOp = false
// var usingDefaultOp = false
var params map[string]any
var item any
@ -60,18 +62,20 @@ func runFunc(ctx ExprContext, name string, args map[string]any) (result any, err
if it, op, err = parseRunArgs(localCtx, args); err != nil {
return
} else if op == nil {
op = NewGolangFunctor(printLnFunc)
usingDefaultOp = true
// } else if op == nil {
// op = NewGolangFunctor(printLnFunc)
// usingDefaultOp = true
}
for item, err = it.Next(); err == nil; item, err = it.Next() {
if usingDefaultOp {
params = map[string]any{ParamItem: []any{item}}
} else {
params = map[string]any{ParamIndex: it.Index(), ParamItem: item}
}
// if usingDefaultOp {
// params = map[string]any{ParamItem: []any{item}}
// } else {
// params = map[string]any{ParamIndex: it.Index(), ParamItem: item}
// }
if op != nil {
params = map[string]any{ParamIndex: it.Index(), ParamItem: item}
if v, err = op.InvokeNamed(localCtx, iterParamOperator, params); err != nil {
break
} else {
@ -81,11 +85,15 @@ func runFunc(ctx ExprContext, name string, args map[string]any) (result any, err
}
}
}
}
if err == io.EOF {
err = nil
}
if err == nil {
if op == nil {
ctx.UnsafeSetVar(iterVarStatus, it.Count())
}
result, _ = localCtx.GetVar(iterVarStatus)
}
return

149
builtin-os-file-iter.go Normal file
View File

@ -0,0 +1,149 @@
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// builtin-os-file.go
package expr
import (
"fmt"
"io"
"slices"
)
const paramHandleOrPath = "handle-or-path"
const fileReadTextIteratorType = "fileReadTextIterator"
type fileReadTextIterator struct {
osReader *osReader
index int
count int
line string
autoClose bool
}
func newReadTextIterator(r *osReader, autoClose bool) *fileReadTextIterator {
return &fileReadTextIterator{osReader: r, index: -1, autoClose: autoClose}
}
func (it *fileReadTextIterator) TypeName() string {
return fileReadTextIteratorType
}
func (it *fileReadTextIterator) String() string {
if it.osReader != nil && it.osReader.fh != nil {
return fmt.Sprintf("$(%s@%q)", fileReadTextIteratorType, it.osReader.fh.Name())
}
return fmt.Sprintf("$(%s@<nil>)", fileReadTextIteratorType)
}
func (it *fileReadTextIterator) Count() int {
return it.count
}
func (it *fileReadTextIterator) Next() (item any, err error) { // must return io.EOF after the last item
if it.osReader.fh != nil {
if it.line, err = it.osReader.reader.ReadString('\n'); err == nil {
it.index++
it.count++
item = it.line[0 : len(it.line)-1]
} else if it.autoClose {
it.Clean()
}
}
return
}
func (it *fileReadTextIterator) Current() (item any, err error) {
if len(it.line) > 0 {
item = it.line[0 : len(it.line)-1]
}
return
}
func (it *fileReadTextIterator) Index() int {
return it.index
}
func (it *fileReadTextIterator) Reset() (err error) {
if _, err = it.osReader.fh.Seek(0, io.SeekStart); err == nil {
it.index = -1
it.count = 0
it.line = ""
}
return
}
func (it *fileReadTextIterator) HasOperation(name string) bool {
return slices.Contains([]string{NextName, ResetName, IndexName, CountName, CurrentName, CleanName}, name)
}
func (it *fileReadTextIterator) Clean() (err error) {
if it.osReader.fh != nil {
if err = it.osReader.fh.Close(); err == nil {
it.osReader = nil
}
}
return nil
}
func (it *fileReadTextIterator) CallOperation(name string, args map[string]any) (v any, err error) {
switch name {
case NextName:
v, err = it.Next()
case ResetName:
err = it.Reset()
case CleanName:
err = it.Clean()
case IndexName:
v = int64(it.Index())
case CurrentName:
v, err = it.Current()
case CountName:
v = it.count
default:
err = errNoOperation(name)
}
return
}
func fileReadIteratorFunc(ctx ExprContext, name string, args map[string]any) (result any, err error) {
var handle *osReader
var invalidFileHandle any
var ok, autoClose bool
result = nil
if handle, ok = args[paramHandleOrPath].(*osReader); !ok {
if fileName, ok := args[paramHandleOrPath].(string); ok && len(fileName) > 0 {
var handleAny any
if handleAny, err = openFileFunc(ctx, name, map[string]any{ParamFilepath: fileName}); err != nil {
return
}
if handleAny != nil {
handle = handleAny.(*osReader)
autoClose = true
}
} else {
invalidFileHandle = args[paramHandleOrPath]
}
}
if handle != nil {
result = newReadTextIterator(handle, autoClose)
}
if err == nil && (handle == nil || invalidFileHandle != nil) {
err = errInvalidFileHandle(name, invalidFileHandle)
}
return
}
// func ImportOsIterFuncs(ctx ExprContext) {
// ctx.RegisterFunc("fileReadIterator", NewGolangFunctor(fileReadIteratorFunc), TypeIterator, []ExprFuncParam{
// NewFuncParam(paramHandleOrPath),
// })
// }
// func init() {
// RegisterBuiltinModule("os.file", ImportOsIterFuncs, "Operating system file iterator functions")
// }

View File

@ -250,6 +250,10 @@ func ImportOsFuncs(ctx ExprContext) {
ctx.RegisterFunc("fileReadTextAll", NewGolangFunctor(fileReadTextAllFunc), TypeString, []ExprFuncParam{
NewFuncParam(ParamHandle),
})
ctx.RegisterFunc("fileReadIterator", NewGolangFunctor(fileReadIteratorFunc), TypeIterator, []ExprFuncParam{
NewFuncParam(paramHandleOrPath),
})
}
func init() {

View File

@ -13,6 +13,7 @@ const (
TypeFileHandle = "file-handle"
TypeInt = "integer"
TypeItem = "item"
TypeIterator = "iterator"
TypeNumber = "number"
TypePair = "pair"
TypeString = "string"

View File

@ -68,9 +68,15 @@ func (it *DictIterator) makeKeys(m map[any]any, sort sortType) {
func NewDictIterator(dict *DictType, args []any) (it *DictIterator, err error) {
var sortType = sortTypeNone
var s string
it = &DictIterator{a: dict, count: 0, index: -1, keys: nil, iterMode: dictIterModeKeys}
var argAny any
dictIt := &DictIterator{a: dict, count: 0, index: -1, keys: nil, iterMode: dictIterModeKeys}
if len(args) > 0 {
if s, err = ToGoString(args[0], "sort type"); err == nil {
argAny = args[0]
} else {
argAny = "default"
}
if s, err = ToGoString(argAny, "sort type"); err == nil {
switch strings.ToLower(s) {
case "a", "asc":
sortType = sortTypeAsc
@ -84,26 +90,32 @@ func NewDictIterator(dict *DictType, args []any) (it *DictIterator, err error) {
err = fmt.Errorf("invalid sort type %q", s)
}
if err == nil && len(args) > 1 {
if s, err = ToGoString(args[1], "iteration mode"); err == nil {
if err == nil {
if len(args) > 1 {
argAny = args[1]
} else {
argAny = "default"
}
if s, err = ToGoString(argAny, "iteration mode"); err == nil {
switch strings.ToLower(s) {
case "k", "key", "keys":
it.iterMode = dictIterModeKeys
dictIt.iterMode = dictIterModeKeys
case "v", "value", "values":
it.iterMode = dictIterModeValues
dictIt.iterMode = dictIterModeValues
case "i", "item", "items":
it.iterMode = dictIterModeItems
dictIt.iterMode = dictIterModeItems
case "", "default":
it.iterMode = dictIterModeKeys
dictIt.iterMode = dictIterModeKeys
default:
err = fmt.Errorf("invalid iteration mode %q", s)
}
}
}
}
}
it.makeKeys(*dict, sortType)
return
dictIt.makeKeys(*dict, sortType)
return dictIt, err
}
func NewMapIterator(m map[any]any) (it *DictIterator) {
@ -117,7 +129,7 @@ func (it *DictIterator) String() string {
if it.a != nil {
l = len(*it.a)
}
return fmt.Sprintf("$(#%d)", l)
return fmt.Sprintf("$({#%d})", l)
}
func (it *DictIterator) TypeName() string {

View File

@ -1087,6 +1087,8 @@ The "base" builtin module provides functions for type checking and type conversi
.Other functions
* <<_eval,eval()>>
* <<_set,set()>>
* <<_unset,unset()>>
* <<_var,var()>>
@ -1318,6 +1320,32 @@ This function allows you to define variables whose names must include special ch
`>>>` [blue]`var("gain%", var("$x")+1)` +
[green]`13`
===== set
Syntax: +
`{4sp}set(<string-expr>, <expr>) -> any`
This function allows you to set the value of a variable whose name can include special characters. The first parameter is the name of the variable and the second parameter is the new value to assign to that variable.
It is equivalent to the first form of the var() function, but it is more explicit about the intent of changing the value of an existing variable.
.Examples
`>>>` [blue]`set("$x", 100)` +
[green]`100` +
`>>>` [blue]`var("$x")` +
[green]`100` +
===== unset()
Syntax: +
`{4sp}unset(<string-expr>) -> any`
This function allows you to unset a variable whose name can include special characters. The parameter is the name of the variable to unset.
.Examples
`>>>` [blue]`unset("$x")` +
[green]`nil` +
`>>>` [blue]`var("$x")` +
[red]`Eval Error: var(): unknown variable "$x"`
==== Module "fmt"
===== print()
@ -1325,18 +1353,31 @@ This function allows you to define variables whose names must include special ch
===== println()
==== Module "import"
Module actiovation: +
===== _import()_
[blue]_import([grey]#<source-file>#)_ -- loads the multi-expression contained in the specified source and returns its value.
Syntax: +
`{4sp}import(<source-file>)`
Loads the multi-expression contained in the specified source and returns its value.
===== _importAll()_
==== Module "iterator"
===== run()
Syntax: +
`{4sp}run(<iterator>, <operator>, <vars>) -> any`
Iterates over the specified iterator and applies the specified operator to the current value of the iterator.
==== Module "math.arith"
Currently, the "math.arith" module provides two functions, add() and mul(), that perform addition and multiplication of an arbitrary number of parameters. More functions will be added in the future.
* <<_add,add()>>
* <<_mul,mul()>>
===== add()
Syntax: +
`{4sp}add(<num-expr1>, <num-expr2>, ...) -> any` +

View File

@ -76,6 +76,12 @@ func isFile(filePath string) bool {
func searchAmongPath(filename string, dirList []string) (filePath string) {
var err error
suffix := SHAREDLIBRARY_EXTENSION
if strings.HasSuffix(filename, ".debug") {
suffix += ".debug"
}
for _, dir := range dirList {
if dir, err = ExpandPath(dir); err != nil {
continue
@ -84,6 +90,12 @@ func searchAmongPath(filename string, dirList []string) (filePath string) {
filePath = fullPath
break
}
subdir := strings.TrimSuffix(filename, suffix)
if fullPath := path.Join(dir, subdir, filename); isFile(fullPath) {
filePath = fullPath
break
}
}
return
}

25
iter-factory.go Normal file
View File

@ -0,0 +1,25 @@
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// iter-factory.go
package expr
func NewIterator(value any) (it Iterator, err error) {
if value == nil {
return NewArrayIterator([]any{}), nil
}
switch v := value.(type) {
case *ListType:
it = NewListIterator(v, nil)
case *DictType:
it, err = NewDictIterator(v, nil)
case []any:
it = NewArrayIterator(v)
case Iterator:
it = v
default:
it = NewArrayIterator([]any{value})
}
return
}

View File

@ -27,6 +27,7 @@ const (
type Iterator interface {
Typer
fmt.Stringer
Next() (item any, err error) // must return io.EOF after the last item
Current() (item any, err error)
Index() int

9
lib-ext-darwin.go Normal file
View File

@ -0,0 +1,9 @@
//go:build darwin
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// lib-ext-darwin.go
package expr
const SHAREDLIBRARY_EXTENSION = ".dylib"

9
lib-ext-linux.go Normal file
View File

@ -0,0 +1,9 @@
//go:build linux
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// lib-ext-linux.go
package expr
const SHAREDLIBRARY_EXTENSION = ".so"

9
lib-ext-windows.go Normal file
View File

@ -0,0 +1,9 @@
//go:build windows
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// lib-ext-windows.go
package expr
const SHAREDLIBRARY_EXTENSION = ".dll"

View File

@ -62,27 +62,12 @@ func NewArrayIterator(array []any) (it *ListIterator) {
return
}
func NewAnyIterator(value any) (it *ListIterator) {
if value == nil {
it = NewArrayIterator([]any{})
} else if list, ok := value.(*ListType); ok {
it = NewListIterator(list, nil)
} else if array, ok := value.([]any); ok {
it = NewArrayIterator(array)
} else if it1, ok := value.(*ListIterator); ok {
it = it1
} else {
it = NewArrayIterator([]any{value})
}
return
}
func (it *ListIterator) String() string {
var l = 0
if it.a != nil {
l = len(*it.a)
}
return fmt.Sprintf("$(#%d)", l)
return fmt.Sprintf("$([#%d])", l)
}
func (it *ListIterator) TypeName() string {

View File

@ -26,9 +26,9 @@ func newList(listAny []any) (list *ListType) {
func NewList(listAny []any) (list *ListType) {
if listAny != nil {
ls := make(ListType, len(listAny))
// for i, item := range listAny {
// ls[i] = item
// }
// for i, item := range listAny {
// ls[i] = item
// }
copy(ls, listAny)
list = &ls
}
@ -129,6 +129,20 @@ func (ls *ListType) contains(t *ListType) (answer bool) {
return
}
func (ls1 *ListType) Equals(ls2 ListType) (answer bool) {
if ls2 != nil && len(*ls1) == len(ls2) {
answer = true
for index, i1 := range *ls1 {
// if i1 != (ls2)[index] {
if !reflect.DeepEqual(i1, ls2[index]) {
answer = false
break
}
}
}
return
}
func (list *ListType) indexDeepSameCmp(target any) (index int) {
var eq bool
var err error
@ -193,3 +207,6 @@ func (list *ListType) setItem(index int64, value any) (err error) {
return
}
func (list *ListType) appendItem(value any) {
*list = append(*list, value)
}

View File

@ -31,7 +31,10 @@ func evalBuiltin(ctx ExprContext, opTerm *term) (v any, err error) {
count, err = ImportInContextByGlobPattern(module)
} else {
var moduleSpec any
it := NewAnyIterator(childValue)
var it Iterator
if it, err = NewIterator(childValue); err != nil {
return
}
for moduleSpec, err = it.Next(); err == nil; moduleSpec, err = it.Next() {
if module, ok := moduleSpec.(string); ok {
if ImportInContext(module) {

70
operator-digest.go Normal file
View File

@ -0,0 +1,70 @@
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// operator-digest.go
package expr
import (
"fmt"
"io"
)
//-------- digest term
func newDigestTerm(tk *Token) (inst *term) {
return &term{
tk: *tk,
children: make([]*term, 0, 2),
position: posInfix,
priority: priIterOp,
evalFunc: evalDigest,
}
}
func evalDigest(ctx ExprContext, opTerm *term) (v any, err error) {
var leftValue, rightValue any
var it Iterator
var item, lastValue any
if err = opTerm.checkOperands(); err != nil {
return
}
if leftValue, err = opTerm.children[0].compute(ctx); err != nil {
return
}
if it, err = NewIterator(leftValue); err != nil {
return nil, fmt.Errorf("left operand of DIGEST must be an iterable data-source; got %s", TypeName(leftValue))
}
lastValue = nil
for item, err = it.Next(); err == nil; item, err = it.Next() {
ctx.SetVar("_", item)
ctx.SetVar("_index", it.Index())
ctx.SetVar("_count", it.Count())
if rightValue, err = opTerm.children[1].compute(ctx); err == nil {
if rightValue == nil {
break
} else {
lastValue = rightValue
}
}
ctx.DeleteVar("_count")
ctx.DeleteVar("_index")
ctx.DeleteVar("_")
if err != nil {
break
}
}
if err == io.EOF {
err = nil
}
v = lastValue
return
}
// init
func init() {
registerTermConstructor(SymKwDigest, newDigestTerm)
}

72
operator-filter.go Normal file
View File

@ -0,0 +1,72 @@
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// operator-filter.go
package expr
import (
"fmt"
"io"
)
//-------- map term
func newFilterTerm(tk *Token) (inst *term) {
return &term{
tk: *tk,
children: make([]*term, 0, 2),
position: posInfix,
priority: priIterOp,
evalFunc: evalFilter,
}
}
func evalFilter(ctx ExprContext, opTerm *term) (v any, err error) {
var leftValue, rightValue any
var it Iterator
var item any
if err = opTerm.checkOperands(); err != nil {
return
}
if leftValue, err = opTerm.children[0].compute(ctx); err != nil {
return
}
if it, err = NewIterator(leftValue); err != nil {
return nil, fmt.Errorf("left operand of FILTER must be an iterable data-source; got %s", TypeName(leftValue))
}
values := newListA()
for item, err = it.Next(); err == nil; item, err = it.Next() {
ctx.SetVar("_", item)
ctx.SetVar("_index", it.Index())
ctx.SetVar("_count", it.Count())
if rightValue, err = opTerm.children[1].compute(ctx); err == nil {
if success, valid := ToBool(rightValue); valid {
if success {
values.appendItem(item)
}
} else {
err = fmt.Errorf("filter expression must return a boolean or a castable to boolean, got %v [%T]", rightValue, rightValue)
}
}
ctx.DeleteVar("_count")
ctx.DeleteVar("_index")
ctx.DeleteVar("_")
if err != nil {
break
}
}
if err == io.EOF {
err = nil
}
v = values
return
}
// init
func init() {
registerTermConstructor(SymKwFilter, newFilterTerm)
}

65
operator-join.go Normal file
View File

@ -0,0 +1,65 @@
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// operator-join.go
package expr
import (
"fmt"
"io"
)
//-------- join term
func newJoinTerm(tk *Token) (inst *term) {
return &term{
tk: *tk,
children: make([]*term, 0, 2),
position: posInfix,
priority: priIterOp,
evalFunc: evalJoin,
}
}
func evalJoin(ctx ExprContext, opTerm *term) (v any, err error) {
var leftValue, rightValue any
var itLeft, itRight Iterator
var item any
if err = opTerm.checkOperands(); err != nil {
return
}
if leftValue, err = opTerm.children[0].compute(ctx); err != nil {
return
}
if rightValue, err = opTerm.children[1].compute(ctx); err != nil {
return
}
if itLeft, err = NewIterator(leftValue); err != nil {
return nil, fmt.Errorf("left operand of JOIN must be an iterable data-source; got %s", TypeName(leftValue))
}
if itRight, err = NewIterator(rightValue); err != nil {
return nil, fmt.Errorf("right operand of JOIN must be an iterable data-source; got %s", TypeName(rightValue))
}
values := newListA()
for _, it := range []Iterator{itLeft, itRight} {
for item, err = it.Next(); err == nil; item, err = it.Next() {
values.appendItem(item)
}
}
if err == io.EOF {
err = nil
}
v = values
return
}
// init
func init() {
registerTermConstructor(SymKwJoin, newJoinTerm)
}

66
operator-map.go Normal file
View File

@ -0,0 +1,66 @@
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// operator-map.go
package expr
import (
"fmt"
"io"
)
//-------- map term
func newMapTerm(tk *Token) (inst *term) {
return &term{
tk: *tk,
children: make([]*term, 0, 2),
position: posInfix,
priority: priIterOp,
evalFunc: evalMap,
}
}
func evalMap(ctx ExprContext, opTerm *term) (v any, err error) {
var leftValue, rightValue any
var it Iterator
var item any
if err = opTerm.checkOperands(); err != nil {
return
}
if leftValue, err = opTerm.children[0].compute(ctx); err != nil {
return
}
if it, err = NewIterator(leftValue); err != nil {
return nil, fmt.Errorf("left operand of MAP must be an iterable data-source; got %s", TypeName(leftValue))
}
values := newListA()
for item, err = it.Next(); err == nil; item, err = it.Next() {
ctx.SetVar("_", item)
ctx.SetVar("_index", it.Index())
ctx.SetVar("_count", it.Count())
if rightValue, err = opTerm.children[1].compute(ctx); err == nil {
values.appendItem(rightValue)
}
ctx.DeleteVar("_count")
ctx.DeleteVar("_index")
ctx.DeleteVar("_")
if err != nil {
break
}
}
if err == io.EOF {
err = nil
}
v = values
return
}
// init
func init() {
registerTermConstructor(SymKwMap, newMapTerm)
}

View File

@ -31,11 +31,11 @@ func pluginExists(name string) (exists bool) {
func makePluginName(name string) (decorated string) {
var template string
if execName, err := os.Executable(); err != nil || !strings.Contains(execName, "debug") {
template = "expr-%s-plugin.so"
template = "expr-%s-plugin%s"
} else {
template = "expr-%s-plugin.so.debug"
template = "expr-%s-plugin%s.debug"
}
decorated = fmt.Sprintf(template, name)
decorated = fmt.Sprintf(template, name, SHAREDLIBRARY_EXTENSION)
return
}
@ -93,10 +93,12 @@ func importPlugin( /*ctx ExprContext,*/ dirList []string, name string) (err erro
func importPluginFromSearchPath(name any) (count int, err error) {
var moduleSpec any
var it Iterator
dirList := buildSearchDirList("plugin", ENV_EXPR_PLUGIN_PATH)
count = 0
it := NewAnyIterator(name)
if it, err = NewIterator(name); err != nil {
return
}
for moduleSpec, err = it.Next(); err == nil; moduleSpec, err = it.Next() {
if module, ok := moduleSpec.(string); ok {
if err = importPlugin(dirList, module); err != nil {

View File

@ -210,14 +210,14 @@ func (scanner *scanner) fetchNextToken() (tk *Token) {
tk = scanner.makeToken(SymQuote, ch)
escape = false
} else {
tk = scanner.fetchString(ch)
tk = scanner.fetchString(ch, true)
}
case '"':
if escape {
tk = scanner.makeToken(SymDoubleQuote, ch)
escape = false
} else {
tk = scanner.fetchString(ch)
tk = scanner.fetchString(ch, true)
}
case '`':
tk = scanner.makeToken(SymBackTick, ch)
@ -315,6 +315,14 @@ func (scanner *scanner) fetchNextToken() (tk *Token) {
tk.source += ")"
} else if next == '$' {
tk = scanner.moveOn(SymDoubleDollar, ch, next)
} else if next == '{' {
scanner.readChar()
if tk = scanner.fetchString('}', false); tk != nil {
tk.Sym = SymIdentifier
}
} else if next == '_' || (next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z') {
scanner.readChar()
tk = scanner.fetchIdentifier(next)
} else {
tk = scanner.makeToken(SymDollar, ch)
}
@ -590,7 +598,7 @@ func (scanner *scanner) fetchUntil(sym Symbol, allowEos bool, endings ...byte) (
return
}
func (scanner *scanner) fetchString(termCh byte) (tk *Token) {
func (scanner *scanner) fetchString(termCh byte, addQuote bool) (tk *Token) {
var err error
var ch, prev byte
var sb strings.Builder
@ -628,7 +636,11 @@ func (scanner *scanner) fetchString(termCh byte) (tk *Token) {
}
} else {
txt := sb.String()
if addQuote {
tk = scanner.makeValueToken(SymString, `"`+txt+`"`, txt)
} else {
tk = scanner.makeValueToken(SymString, txt, txt)
}
}
return
}

View File

@ -135,6 +135,10 @@ func init() {
SymKwNot: {"not", symClassOperator, posInfix},
SymKwOr: {"or", symClassOperator, posInfix},
SymKwBut: {"but", symClassOperator, posInfix},
SymKwMap: {"map", symClassOperator, posInfix},
SymKwFilter: {"filter", symClassOperator, posInfix},
SymKwDigest: {"digest", symClassOperator, posInfix},
SymKwJoin: {"join", symClassOperator, posInfix},
SymKwFunc: {"func(", symClassDeclaration, posPrefix},
SymKwBuiltin: {"builtin", symClassOperator, posPrefix},
SymKwPlugin: {"plugin", symClassOperator, posPrefix},

View File

@ -119,6 +119,10 @@ const (
SymKwPlugin
SymKwIn
SymKwInclude
SymKwMap
SymKwFilter
SymKwDigest
SymKwJoin
SymKwNil
SymKwUnset
)
@ -135,9 +139,13 @@ func init() {
"FUNC": SymKwFunc,
"IN": SymKwIn,
"INCLUDE": SymKwInclude,
"MAP": SymKwMap,
"FILTER": SymKwFilter,
"NOT": SymKwNot,
"OR": SymKwOr,
"NIL": SymKwNil,
"UNSET": SymKwUnset,
"DIGEST": SymKwDigest,
"JOIN": SymKwJoin,
}
}

View File

@ -21,10 +21,11 @@ func TestFuncRun(t *testing.T) {
/* 7 */ {`builtin "iterator"; run($(1,2,3), func(){1}, "prrr")`, nil, `paramter "vars" must be a dictionary, passed prrr [string]`},
/* 8 */ {`builtin "iterator"; run($(1,2,3), operator=nil)`, nil, nil},
/* 9 */ {`builtin "iterator"; run($(1,2,3), operatorx=nil)`, nil, `run(): unknown param "operatorx"`},
/* 10 */ {`builtin ["os.file", "iterator"]; it = fileReadIterator("test-file.txt"); run(it)`, nil, nil},
}
//t.Setenv("EXPR_PATH", ".")
//runTestSuiteSpec(t, section, inputs, 1)
runTestSuite(t, section, inputs)
runTestSuiteSpec(t, section, inputs, 10)
// runTestSuite(t, section, inputs)
}

View File

@ -5,6 +5,7 @@
package expr
import (
"io"
"testing"
)
@ -26,10 +27,16 @@ func TestFuncOs(t *testing.T) {
/* 13 */ {`builtin "os.file"; handle=fileClose(123)`, nil, `fileClose(): invalid file handle`},
/* 14 */ {`builtin "os.file"; handle=fileOpen("/tmp/dummy"); c=fileReadTextAll(handle); fileClose(handle); c`, "bye-bye", nil},
/* 15 */ {`builtin "os.file"; c=fileReadTextAll(123)`, nil, `fileReadTextAll(): invalid file handle 123 [int64]`},
/* 16 */ {`builtin "os.file"; it=fileReadIterator("test-file.txt"); it++`, "uno", nil},
/* 17 */ {`builtin "os.file"; it=fileReadIterator("test-file.txt"); it++;it++;it++`, nil, io.EOF.Error()},
/* 18 */ {`builtin "os.file"; it=fileReadIterator("test-file.txt"); it.clean`, nil, nil},
/* 19 */ {`builtin "os.file"; it=fileReadIterator("test-file.txt"); string(it)`, `$(fileReadTextIterator@"test-file.txt")`, nil},
/* 20 */ {`builtin "os.file"; handle=fileOpen("/etc/hosts"); fileClose(handle); string(handle)`, `reader`, nil},
/* 21 */ {`builtin "os.file"; handle=fileCreate("/tmp/dummy"); fileClose(handle); string(handle)`, `writer`, nil},
}
// t.Setenv("EXPR_PATH", ".")
//runTestSuiteSpec(t, section, inputs, 2)
// runTestSuiteSpec(t, section, inputs, 19)
runTestSuite(t, section, inputs)
}

View File

@ -72,6 +72,7 @@ func doTest(t *testing.T, ctx ExprContext, section string, input *inputType, cou
var expr Expr
var gotResult any
var gotErr error
var eq, eqDone bool
wantErr := getWantedError(input)
@ -90,7 +91,18 @@ func doTest(t *testing.T, ctx ExprContext, section string, input *inputType, cou
gotResult, gotErr = expr.Eval(ctx)
}
eq := reflect.DeepEqual(gotResult, input.wantResult)
if input.wantResult != nil && gotResult != nil {
if ls1, ok := input.wantResult.(*ListType); ok {
if ls2, ok := gotResult.(*ListType); ok {
eq = ls1.Equals(*ls2)
eqDone = true
}
}
}
if !eqDone {
eq = reflect.DeepEqual(gotResult, input.wantResult)
}
if !eq /*gotResult != input.wantResult*/ {
t.Errorf("%d: `%s` -> result = %v [%s], want = %v [%s]", count, input.source, gotResult, TypeName(gotResult), input.wantResult, TypeName(input.wantResult))

View File

@ -43,9 +43,11 @@ func TestExpr(t *testing.T) {
it++;
it++
`, int64(1), nil},
/* 20 */ {`a=2; ${a}`, int64(2), nil},
/* 21 */ {`$_=2; $_`, int64(2), nil},
}
// t.Setenv("EXPR_PATH", ".")
// runTestSuiteSpec(t, section, inputs, 18)
// runTestSuiteSpec(t, section, inputs, 21)
runTestSuite(t, section, inputs)
}

View File

@ -59,7 +59,7 @@ func TestNewIterList2(t *testing.T) {
func TestNewIterList3(t *testing.T) {
list := []any{"a", "b", "c", "d"}
it := NewAnyIterator(list)
it, _ := NewIterator(list)
if item, err := it.Next(); err != nil {
t.Errorf("error: %v", err)
} else if item != "a" {
@ -71,7 +71,7 @@ func TestNewIterList3(t *testing.T) {
func TestNewIterList4(t *testing.T) {
list := any(nil)
it := NewAnyIterator(list)
it, _ := NewIterator(list)
if _, err := it.Next(); err != io.EOF {
t.Errorf("error: %v", err)
}
@ -79,7 +79,7 @@ func TestNewIterList4(t *testing.T) {
func TestNewIterList5(t *testing.T) {
list := "123"
it := NewAnyIterator(list)
it, _ := NewIterator(list)
if item, err := it.Next(); err != nil {
t.Errorf("error: %v", err)
} else if item != "123" {
@ -91,8 +91,8 @@ func TestNewIterList5(t *testing.T) {
func TestNewIterList6(t *testing.T) {
list := newListA("a", "b", "c", "d")
it1 := NewAnyIterator(list)
it := NewAnyIterator(it1)
it1, _ := NewIterator(list)
it, _ := NewIterator(it1)
if item, err := it.Next(); err != nil {
t.Errorf("error: %v", err)
} else if item != "a" {
@ -103,9 +103,9 @@ func TestNewIterList6(t *testing.T) {
}
func TestNewString(t *testing.T) {
list := "123"
it := NewAnyIterator(list)
if s := it.String(); s != "$(#1)" {
t.Errorf("expected $(#1), got %s", s)
it, _ := NewIterator(list)
if s := it.String(); s != "$([#1])" {
t.Errorf("expected $([#1]), got %s", s)
}
}

View File

@ -31,8 +31,11 @@ func TestIteratorParser(t *testing.T) {
/* 20 */ {`it=$({1:"one",2:"two",3:"three"}, "default", "value"); it++`, "one", nil},
/* 21 */ {`it=$({1:"one",2:"two",3:"three"}, "desc", "key"); it++`, int64(3), nil},
/* 22 */ {`it=$({1:"one",2:"two",3:"three"}, "asc", "item"); it++`, NewList([]any{int64(1), "one"}), nil},
/* 23 */ {`builtin "os.file"; fileReadIterator("test-file.txt") map ${_index}`, NewList([]any{int64(0), int64(1)}), nil},
/* 24 */ {`builtin "os.file"; #(fileReadIterator("test-file.txt") filter (#${_} == 2))`, int64(0), nil},
/* 25 */ {`builtin "os.file"; #(fileReadIterator("test-file.txt") filter (#${_} == 3))`, int64(2), nil},
}
// runTestSuiteSpec(t, section, inputs, 20)
// runTestSuiteSpec(t, section, inputs, 23)
runTestSuite(t, section, inputs)
}

View File

@ -30,13 +30,19 @@ func TestOperator(t *testing.T) {
/* 17 */ {`~true`, nil, `[1:2] prefix/postfix operator "~" do not support operand 'true' [bool]`},
/* 18 */ {`1^2`, int64(3), nil},
/* 19 */ {`3^2`, int64(1), nil},
/* 19 */ {`a=1; a^=2`, int64(3), nil},
/* 20 */ {`a=1; ++a`, int64(2), nil},
/* 21 */ {`a=1; --a`, int64(0), nil},
/* 20 */ {`a=1; a^=2`, int64(3), nil},
/* 21 */ {`a=1; ++a`, int64(2), nil},
/* 22 */ {`a=1; --a`, int64(0), nil},
/* 23 */ {`[1,2,3] map var("_")`, newList([]any{int64(1), int64(2), int64(3)}), nil},
/* 24 */ {`[1,2,3] map $_`, newList([]any{int64(1), int64(2), int64(3)}), nil},
/* 25 */ {`[1,2,3,4] filter ($_ % 2 == 0)`, newList([]any{int64(2), int64(4)}), nil},
/* 26 */ {`max=0; [2,3,1] digest max=(($_ > max) ? {$_} :: {max})`, int64(3), nil},
/* 27 */ {`["a","b"] join ["x"]`, newList([]any{"a", "b", "x"}), nil},
/* 28 */ {`["a","b"] join ["x"-true]`, nil, `[1:21] left operand 'x' [string] and right operand 'true' [bool] are not compatible with operator "-"`},
}
// t.Setenv("EXPR_PATH", ".")
// runTestSuiteSpec(t, section, inputs, 4)
// runTestSuiteSpec(t, section, inputs, 28)
runTestSuite(t, section, inputs)
}

View File

@ -30,9 +30,29 @@ func TestPluginExists(t *testing.T) {
func TestMakePluginName(t *testing.T) {
name := "json"
want := "expr-" + name + "-plugin.so"
want := "expr-" + name + "-plugin" + SHAREDLIBRARY_EXTENSION
if got := makePluginName(name); got != want {
t.Errorf("makePluginName(%q) failed: Got: %q, Want: %q", name, got, want)
}
}
// func TestLoadPluginName(t *testing.T) {
// name := "json"
// // want := "expr-" + name + "-plugin
// want := 1
// os.Setenv("PLUGINS", "${HOME}/go/src/git.portale-stac.it/go")
// // os.Setenv("EXPR_PLUGIN_PATH", "${PLUGINS}/expr-json-plugin:${PLUGINS}/expr-csv-plugin")
// os.Setenv("EXPR_PLUGIN_PATH", "${PLUGINS}")
// got, err := importPluginFromSearchPath(name)
// if err != nil {
// t.Errorf("importPluginFromSearchPath(%q) failed: %v", name, err)
// return
// }
// if got != want {
// t.Errorf("importPluginFromSearchPath(%q) failed: Got: %q, Want: %q", name, got, want)
// }
// }

118
t_utils-unix_test.go Normal file
View File

@ -0,0 +1,118 @@
//go:build unix
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// t_utils_test.go
package expr
import (
"errors"
"os/user"
"path"
"testing"
)
func TestExpandPathRootOk(t *testing.T) {
source := "~root"
wantValue := "/root"
// wantErr := errors.New(`test expected string, got list ([])`)
wantErr := error(nil)
gotValue, gotErr := ExpandPath(source)
if gotErr != nil && gotErr.Error() != wantErr.Error() {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
source, gotValue, gotErr, wantValue, wantErr)
} else if gotValue != wantValue {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
source, gotValue, gotErr, wantValue, wantErr)
}
}
func TestExpandPathRootSubDirOk(t *testing.T) {
source := "~root/test"
wantValue := "/root/test"
// wantErr := errors.New(`test expected string, got list ([])`)
wantErr := error(nil)
gotValue, gotErr := ExpandPath(source)
if gotErr != nil && gotErr.Error() != wantErr.Error() {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
source, gotValue, gotErr, wantValue, wantErr)
} else if gotValue != wantValue {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
source, gotValue, gotErr, wantValue, wantErr)
}
}
func TestExpandPathUser(t *testing.T) {
u, _ := user.Current()
source := "~"
wantValue := path.Join("/home", u.Username)
// wantErr := errors.New(`test expected string, got list ([])`)
wantErr := error(nil)
gotValue, gotErr := ExpandPath(source)
if gotErr != nil {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
source, gotValue, gotErr, wantValue, wantErr)
} else if gotValue != wantValue {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
source, gotValue, gotErr, wantValue, wantErr)
}
}
func TestExpandPathEnv(t *testing.T) {
u, _ := user.Current()
source := "$HOME"
wantValue := path.Join("/home", u.Username)
// wantErr := errors.New(`test expected string, got list ([])`)
wantErr := error(nil)
gotValue, gotErr := ExpandPath(source)
if gotErr != nil {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
source, gotValue, gotErr, wantValue, wantErr)
} else if gotValue != wantValue {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
source, gotValue, gotErr, wantValue, wantErr)
}
}
func TestExpandPathErr(t *testing.T) {
source := "~fake-user/test"
wantValue := "~fake-user/test"
wantErr := errors.New(`user: unknown user fake-user`)
gotValue, gotErr := ExpandPath(source)
if gotErr != nil && gotErr.Error() != wantErr.Error() {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
source, gotValue, gotErr, wantValue, wantErr)
} else if gotValue != wantValue {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
source, gotValue, gotErr, wantValue, wantErr)
}
}
func TestExpandPathUserErr(t *testing.T) {
u, _ := user.Current()
source := "~"
wantValue := path.Join("/home", u.Username)
// wantErr := errors.New(`test expected string, got list ([])`)
wantErr := error(nil)
gotValue, gotErr := ExpandPath(source)
if gotErr != nil {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
source, gotValue, gotErr, wantValue, wantErr)
} else if gotValue != wantValue {
t.Errorf(`ExpandPath(%v) gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
source, gotValue, gotErr, wantValue, wantErr)
}
}

View File

@ -164,3 +164,48 @@ func TestCopyMap(t *testing.T) {
t.Errorf("utils.CopyMap() failed")
}
}
func TestToStringOk(t *testing.T) {
source := "ciao"
wantValue := "ciao"
wantErr := error(nil)
gotValue, gotErr := ToGoString(source, "test")
if gotErr != nil {
t.Error(gotErr)
} else if gotValue != wantValue {
t.Errorf("toGoString(%v, \"test\") gotValue=%v, gotErr=%v -> wantValue=%v, wantErr=%v",
source, gotValue, gotErr, wantValue, wantErr)
}
}
func TestToStringErr(t *testing.T) {
source := newListA()
wantValue := ""
wantErr := errors.New(`test expected string, got list ([])`)
gotValue, gotErr := ToGoString(source, "test")
if gotErr != nil && gotErr.Error() != wantErr.Error() {
t.Errorf(`ToGoString(%v, "test") gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
source, gotValue, gotErr, wantValue, wantErr)
} else if gotValue != wantValue {
t.Errorf(`ToString(%v, "test") gotValue=%q, gotErr=%v -> wantValue=%q, wantErr=%v`,
source, gotValue, gotErr, wantValue, wantErr)
}
}
func TestIsFunctorSucc(t *testing.T) {
f := NewGolangFunctor(isNilFunc)
if !isFunctor(f) {
t.Errorf("isNilFunc() evalued as not a functor")
}
}
func TestIsFunctorFail(t *testing.T) {
f := int64(1)
if isFunctor(f) {
t.Errorf("int evalued as a functor")
}
}

View File

@ -13,6 +13,7 @@ type termPriority uint32
const (
priNone termPriority = iota
priRange
priIterOp // map, filter, digest, etc
priBut
priAssign
priInsert

48
utils-unix.go Normal file
View File

@ -0,0 +1,48 @@
//go:build unix
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// utils-unix.go
package expr
import (
"os"
"os/user"
"path"
"strings"
)
func ExpandPath(sourcePath string) (expandedPath string, err error) {
for expandedPath = os.ExpandEnv(sourcePath); expandedPath != sourcePath; expandedPath = os.ExpandEnv(sourcePath) {
sourcePath = expandedPath
}
if strings.HasPrefix(sourcePath, "~") {
var home, userName, remainder string
slashPos := strings.IndexRune(sourcePath, '/')
if slashPos > 0 {
userName = sourcePath[1:slashPos]
remainder = sourcePath[slashPos:]
} else {
userName = sourcePath[1:]
}
if len(userName) == 0 {
home, err = os.UserHomeDir()
if err != nil {
return
}
} else {
var userInfo *user.User
userInfo, err = user.Lookup(userName)
if err != nil {
return
}
home = userInfo.HomeDir
}
expandedPath = path.Join(home, remainder)
}
return
}

18
utils-windows.go Normal file
View File

@ -0,0 +1,18 @@
//go:build windows
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// utils-unix.go
package expr
import (
"os"
)
func ExpandPath(sourcePath string) (expandedPath string, err error) {
for expandedPath = os.ExpandEnv(sourcePath); expandedPath != sourcePath; expandedPath = os.ExpandEnv(sourcePath) {
sourcePath = expandedPath
}
return
}

View File

@ -6,11 +6,7 @@ package expr
import (
"fmt"
"os"
"os/user"
"path"
"reflect"
"strings"
)
func IsString(v any) (ok bool) {
@ -234,37 +230,3 @@ func ForAll[T, V any](ts []T, fn func(T) V) []V {
}
return result
}
func ExpandPath(sourcePath string) (expandedPath string, err error) {
for expandedPath = os.ExpandEnv(sourcePath); expandedPath != sourcePath; expandedPath = os.ExpandEnv(sourcePath) {
sourcePath = expandedPath
}
if strings.HasPrefix(sourcePath, "~") {
var home, userName, remainder string
slashPos := strings.IndexRune(sourcePath, '/')
if slashPos > 0 {
userName = sourcePath[1:slashPos]
remainder = sourcePath[slashPos:]
} else {
userName = sourcePath[1:]
}
if len(userName) == 0 {
home, err = os.UserHomeDir()
if err != nil {
return
}
} else {
var userInfo *user.User
userInfo, err = user.Lookup(userName)
if err != nil {
return
}
home = userInfo.HomeDir
}
expandedPath = path.Join(home, remainder)
}
return
}