From b6b09b2fb169141b53e05212476f10676662ff03 Mon Sep 17 00:00:00 2001 From: Celestino Amoroso Date: Thu, 23 Apr 2026 19:01:07 +0200 Subject: [PATCH] builtin-os-file: add function fileReadIterator() that creates an iterator over lines of a text file --- builtin-os-file-iter.go | 149 +++++++++++++++++++++++++++++++++++++ builtin-os-file.go | 4 + common-type-names.go | 1 + t_builtin-iterator_test.go | 5 +- t_builtin-os-file_test.go | 8 +- 5 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 builtin-os-file-iter.go diff --git a/builtin-os-file-iter.go b/builtin-os-file-iter.go new file mode 100644 index 0000000..ef736b1 --- /dev/null +++ b/builtin-os-file-iter.go @@ -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@)", 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") +// } diff --git a/builtin-os-file.go b/builtin-os-file.go index 1407dbc..bb12f37 100644 --- a/builtin-os-file.go +++ b/builtin-os-file.go @@ -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() { diff --git a/common-type-names.go b/common-type-names.go index 4bf78a8..0595a1d 100644 --- a/common-type-names.go +++ b/common-type-names.go @@ -13,6 +13,7 @@ const ( TypeFileHandle = "file-handle" TypeInt = "integer" TypeItem = "item" + TypeIterator = "iterator" TypeNumber = "number" TypePair = "pair" TypeString = "string" diff --git a/t_builtin-iterator_test.go b/t_builtin-iterator_test.go index 25a2b67..de24b8b 100644 --- a/t_builtin-iterator_test.go +++ b/t_builtin-iterator_test.go @@ -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) } diff --git a/t_builtin-os-file_test.go b/t_builtin-os-file_test.go index 187cca5..f828396 100644 --- a/t_builtin-os-file_test.go +++ b/t_builtin-os-file_test.go @@ -5,6 +5,7 @@ package expr import ( + "io" "testing" ) @@ -26,10 +27,13 @@ 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}, } // t.Setenv("EXPR_PATH", ".") - //runTestSuiteSpec(t, section, inputs, 2) - runTestSuite(t, section, inputs) + runTestSuiteSpec(t, section, inputs, 18) + // runTestSuite(t, section, inputs) }