From adac5067d75f2f454ccef48844ed1c02f6b1a049 Mon Sep 17 00:00:00 2001 From: Leonid Emar-Kar Date: Fri, 15 Dec 2023 12:39:00 +0000 Subject: [PATCH 1/5] update options with new parameters --- options.go | 128 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 90 insertions(+), 38 deletions(-) diff --git a/options.go b/options.go index bcaf3e7..6998e76 100644 --- a/options.go +++ b/options.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "os" + "path" "strings" ) @@ -30,17 +31,22 @@ type options struct { matchFunc matchFunc caseFunc caseFunc logger io.Writer - max int + output io.Writer orig string resOrig string + max int + maxIter int fType uint8 + iterCh chan string + errCh chan error rec bool name bool relative bool full bool skip bool log bool - output bool + iter bool + out bool } // defaultOptions default [Find] options. @@ -49,9 +55,63 @@ func defaultOptions() *options { matchFunc: MatchAny, caseFunc: sensitive, logger: os.Stdout, - fType: Both, + output: os.Stdout, + maxIter: 100, max: -1, + fType: Both, + } +} + +func defaultOptionsWithCustom(opts ...optFunc) *options { + opt := defaultOptions() + + for _, fn := range opts { + fn(opt) } + + return opt +} + +func (o *options) logError(e error) error { + var err error + + if o.log && !o.skip { + _, err = fmt.Fprintf(o.logger, "error: %s\n", e) + if err != nil { + return fmt.Errorf("%w: %w", e, err) + } + } + + return err +} + +func (o *options) printOutput(str string) error { + var err error + + if o.out { + _, err = fmt.Println(o.output, str) + } + + return err +} + +func (o *options) isSearchedType(isDir bool) bool { + switch { + case o.fType == Folder: + return isDir + case o.fType == File: + return !isDir + default: + return true + } +} + +func (o *options) match(ts Templates, fullPath string) bool { + if o.full { + return o.matchFunc(ts, o.caseFunc(fullPath)) + } + + return o.matchFunc(ts, o.caseFunc(path.Base(fullPath))) } // Deprecated: use [Only] instead. @@ -97,20 +157,20 @@ func RelativePaths(o *options) { o.relative = true } // only if the base path was resolved. func WithErrorsSkip(o *options) { o.skip = true } -// WithErrorsLog logs errors during find execution, -// should be used with [WithErrorsSkip], for clear output. +// WithErrorsLog logs errors during find execution. func WithErrorsLog(o *options) { o.log = true } -// WithOutput prints found paths as soon as they match. -// Follows all the previous path related options, -// such as names and relative paths. -func WithOutput(o *options) { o.output = true } +// WithOutput prints results as soon as they match given [Templates]. +func WithOutput(o *options) { o.out = true } -// Max set maximum ammount of searched objects. [Find] will stop as -// soon as reach the limitation. -func Max(i int) optFunc { +// WithWriter allows to set custom [io.Writer] for [WithPrint]. +// +// Note: write errors count as critical and will be returned +// even if [WithErrorsSkip] was set. +func WithWriter(out io.Writer) optFunc { return func(o *options) { - o.max = i + o.output = out + o.out = true } } @@ -124,6 +184,23 @@ func WithLogger(l io.Writer) optFunc { } } +// WithMaxIterator allows to set custom output channel buffer. +// +// Note: can be used only with [FindWithIterator]. +func WithMaxIterator(max int) optFunc { + return func(o *options) { + o.maxIter = max + } +} + +// Max set maximum ammount of searched objects. [Find] will stop as +// soon as reach the limitation. +func Max(i int) optFunc { + return func(o *options) { + o.max = i + } +} + // Insensitive sets case insensitive search. func Insensitive(o *options) { o.caseFunc = strings.ToLower @@ -150,28 +227,3 @@ func MatchAll(ts Templates, str string) bool { return true } - -func (o *options) logError(e error) error { - if o.skip { - return nil - } - - if o.log { - if _, err := o.logger.Write([]byte("error: " + e.Error() + "\n")); err != nil { - return fmt.Errorf("%w: %s", e, err) - } - } - - return nil -} - -func (o *options) isSearched(isDir bool) bool { - switch { - case o.fType == Folder: - return isDir - case o.fType == File: - return !isDir - default: - return true - } -} From 666508a99dc9a80017b778e64fcec450714e780c Mon Sep 17 00:00:00 2001 From: Leonid Emar-Kar Date: Fri, 15 Dec 2023 12:39:19 +0000 Subject: [PATCH 2/5] optimize template match --- template.go | 63 +++++++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/template.go b/template.go index b461f41..191b43c 100644 --- a/template.go +++ b/template.go @@ -94,35 +94,14 @@ func parse(str string) *Template { func (t *Template) Match(str string) bool { var match bool - if t.base == "" { - match = false - } else if t.base == "*" { + switch { + case t.base == "": + return match + case t.base == "*": match = true - } else if strings.Contains(str, t.base) { - match = true - sub := strings.Split(str, t.base) - - left := len(sub) == 1 || - sub[0] == "" || - strings.HasSuffix(sub[0], pathSeparator) - - right := len(sub) == 1 || - sub[1] == "" || - strings.HasPrefix(sub[1], pathSeparator) - - switch { - case t.strictLeft && t.strictRight: - match = left && right - case t.strictLeft: - match = left - case t.strictRight: - match = right - } - - if t.not { - match = !match - } - } else if t.not { + case strings.Contains(str, t.base): + match = t.match(str) + case t.not: match = true } @@ -141,6 +120,34 @@ func (t *Template) Match(str string) bool { return match } +func (t *Template) match(str string) bool { + match := true + sub := strings.Split(str, t.base) + + left := len(sub) == 1 || + sub[0] == "" || + strings.HasSuffix(sub[0], pathSeparator) + + right := len(sub) == 1 || + sub[1] == "" || + strings.HasPrefix(sub[1], pathSeparator) + + switch { + case t.strictLeft && t.strictRight: + match = left && right + case t.strictLeft: + match = left + case t.strictRight: + match = right + } + + if t.not { + match = !match + } + + return match +} + type Templates []*Template // NewTemplates parses slice of strings into slice of Templates. From e898a4cc229f20e4a9e9eca55596625a971033d1 Mon Sep 17 00:00:00 2001 From: Leonid Emar-Kar Date: Fri, 15 Dec 2023 12:39:47 +0000 Subject: [PATCH 3/5] simplify find funcs and add WithIterator --- find.go | 154 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 107 insertions(+), 47 deletions(-) diff --git a/find.go b/find.go index 2cf1613..86dc707 100644 --- a/find.go +++ b/find.go @@ -17,8 +17,61 @@ type Templater interface { ~string | ~[]string } -// Find parses given string or slice of strings into templates and -// searches for matching paths in where with given opts. +// FindWithIterator acts the same way as [Find] but returns channels instead. +// String channel will return every match. Error channel returns first occured +// error during search or if [WithErrorsSkip] was set, first critical error. +// As soon as search is over or interrupted, both channels will be closed. +// For example: +// +// outCh, errCh := FindWithIterator(ctx, where, ts, opts...) +// for f := range outCh { +// // do something here... +// } +// if err := <-errCh { +// // process error... +// } +func FindWithIterator[T Templater]( + ctx context.Context, + where string, + t T, + opts ...optFunc, +) (chan string, chan error) { + opt := defaultOptionsWithCustom(opts...) + + opt.iterCh = make(chan string, opt.maxIter) + opt.errCh = make(chan error, 1) + opt.iter = true + + go func() { + defer func() { + close(opt.iterCh) + close(opt.errCh) + }() + + resPath, err := resolvePath(where) + if err != nil { + opt.errCh <- err + return + } + + opt.orig = where + opt.resOrig = resPath + + ts, err := newTemplates(t, opt.caseFunc) + if err != nil { + opt.errCh <- err + return + } + + if _, err := find(ctx, resPath, ts, opt); err != nil { + opt.errCh <- err + } + }() + + return opt.iterCh, opt.errCh +} + +// Find searches for matches with the given templates in where. func Find[T Templater]( ctx context.Context, where string, @@ -32,32 +85,16 @@ func Find[T Templater]( return nil, err } - opt := defaultOptions() + opt := defaultOptionsWithCustom(opts...) // Pre-save location file and its resolved path, for further // usage if relative paths will be needed. opt.orig = where opt.resOrig = resPath - for _, fn := range opts { - fn(opt) - } - - var ts Templates - - switch any(t).(type) { - case string: - ts = Templates{NewTemplate(opt.caseFunc(any(t).(string)))} - case []string: - sl := make([]string, 0, len(any(t).([]string))) - - for _, str := range any(t).([]string) { - sl = append(sl, opt.caseFunc(str)) - } - - ts = NewTemplates(sl) - default: - return nil, fmt.Errorf("%w: %v", ErrTemplateType, t) + ts, err := newTemplates(t, opt.caseFunc) + if err != nil { + return nil, err } return find(ctx, resPath, ts, opt) @@ -69,7 +106,7 @@ func find( ts Templates, opt *options, ) ([]string, error) { - resPath, err := resolvePath(where) + resPath, data, err := readAndResolve(where) if err != nil { lErr := opt.logError(err) @@ -78,27 +115,20 @@ func find( res := make([]string, 0) - data, err := os.ReadDir(resPath) - if err != nil { - lErr := opt.logError(err) - - return nil, lErr - } - for _, f := range data { select { case <-ctx.Done(): return nil, ctx.Err() default: + if opt.max == 0 { + return res, nil + } + p := filepath.Join(resPath, f.Name()) var found string - // Check if current path matches searched object and if it does, - // use the match func to process it with the match function. - if (opt.isSearched(f.IsDir())) && - ((opt.full && opt.matchFunc(ts, opt.caseFunc(p))) || - (!opt.full && opt.matchFunc(ts, opt.caseFunc(f.Name())))) { + if opt.isSearchedType(f.IsDir()) && opt.match(ts, p) { switch { case opt.name: found = f.Name() @@ -108,14 +138,18 @@ func find( found = p } - if opt.output { - fmt.Println(found) + if err := opt.printOutput(found); err != nil { + return nil, err } - res = append(res, found) + if opt.iter { + opt.iterCh <- found + } else { + res = append(res, found) + } - if opt.max != -1 && len(res) >= opt.max { - return res, nil + if opt.max != -1 { + opt.max-- } } @@ -125,13 +159,7 @@ func find( return nil, err } - if opt.max != -1 && len(res)+len(recData) >= opt.max { - res = append(res, recData[:opt.max-len(res)]...) - - return res, nil - } else { - res = append(res, recData...) - } + res = append(res, recData...) } } } @@ -154,3 +182,35 @@ func resolvePath(p string) (string, error) { return filepath.Abs(p) } + +func readAndResolve(p string) (string, []os.DirEntry, error) { + resPath, err := resolvePath(p) + if err != nil { + return "", nil, err + } + + data, err := os.ReadDir(resPath) + + return resPath, data, err +} + +func newTemplates[T Templater](t T, fn caseFunc) (Templates, error) { + var ts Templates + + switch any(t).(type) { + case string: + ts = Templates{NewTemplate(fn(any(t).(string)))} + case []string: + sl := make([]string, 0, len(any(t).([]string))) + + for _, str := range any(t).([]string) { + sl = append(sl, fn(str)) + } + + ts = NewTemplates(sl) + default: + return nil, fmt.Errorf("%w: %v", ErrTemplateType, t) + } + + return ts, nil +} From 3bbdca6274a1120f742f6a856cc6fd87a21161aa Mon Sep 17 00:00:00 2001 From: Leonid Emar-Kar Date: Tue, 26 Dec 2023 11:15:55 +0000 Subject: [PATCH 4/5] update logger option processing and flag docs --- options.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/options.go b/options.go index 6998e76..02bde25 100644 --- a/options.go +++ b/options.go @@ -73,16 +73,18 @@ func defaultOptionsWithCustom(opts ...optFunc) *options { } func (o *options) logError(e error) error { - var err error - - if o.log && !o.skip { - _, err = fmt.Fprintf(o.logger, "error: %s\n", e) + if o.log { + _, err := fmt.Fprintf(o.logger, "error: %s\n", e) if err != nil { return fmt.Errorf("%w: %w", e, err) } } - return err + if o.skip { + return nil + } + + return e } func (o *options) printOutput(str string) error { @@ -158,12 +160,15 @@ func RelativePaths(o *options) { o.relative = true } func WithErrorsSkip(o *options) { o.skip = true } // WithErrorsLog logs errors during find execution. +// Defaults to [os.Stdout] and can be changed with [WithLogger]. func WithErrorsLog(o *options) { o.log = true } // WithOutput prints results as soon as they match given [Templates]. +// Defaults to [os.Stdout] and can be changed with [WithWriter]. func WithOutput(o *options) { o.out = true } // WithWriter allows to set custom [io.Writer] for [WithPrint]. +// Also sets [WithOutput] to true. // // Note: write errors count as critical and will be returned // even if [WithErrorsSkip] was set. @@ -175,12 +180,14 @@ func WithWriter(out io.Writer) optFunc { } // WithLogger allows to set custom logger for [WithErrorsLog]. +// Also sets [WithErrorsLog] to true. // // Note: write errors count as critical and will be returned // even if [WithErrorsSkip] was set. func WithLogger(l io.Writer) optFunc { return func(o *options) { o.logger = l + o.log = true } } From 26c6e42ff2e24e4d991fb130936abfb1ea919da6 Mon Sep 17 00:00:00 2001 From: Leonid Emar-Kar Date: Tue, 2 Jan 2024 17:57:51 +0000 Subject: [PATCH 5/5] fix printOutput and update docs --- options.go | 17 ++++++++--------- template.go | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/options.go b/options.go index 02bde25..59e2d1a 100644 --- a/options.go +++ b/options.go @@ -74,8 +74,7 @@ func defaultOptionsWithCustom(opts ...optFunc) *options { func (o *options) logError(e error) error { if o.log { - _, err := fmt.Fprintf(o.logger, "error: %s\n", e) - if err != nil { + if _, err := fmt.Fprintf(o.logger, "error: %s\n", e); err != nil { return fmt.Errorf("%w: %w", e, err) } } @@ -88,13 +87,13 @@ func (o *options) logError(e error) error { } func (o *options) printOutput(str string) error { - var err error - if o.out { - _, err = fmt.Println(o.output, str) + if _, err := fmt.Fprintln(o.output, str); err != nil { + return err + } } - return err + return nil } func (o *options) isSearchedType(isDir bool) bool { @@ -170,7 +169,7 @@ func WithOutput(o *options) { o.out = true } // WithWriter allows to set custom [io.Writer] for [WithPrint]. // Also sets [WithOutput] to true. // -// Note: write errors count as critical and will be returned +// Note: write error counts as critical and will be returned // even if [WithErrorsSkip] was set. func WithWriter(out io.Writer) optFunc { return func(o *options) { @@ -182,7 +181,7 @@ func WithWriter(out io.Writer) optFunc { // WithLogger allows to set custom logger for [WithErrorsLog]. // Also sets [WithErrorsLog] to true. // -// Note: write errors count as critical and will be returned +// Note: write error counts as critical and will be returned // even if [WithErrorsSkip] was set. func WithLogger(l io.Writer) optFunc { return func(o *options) { @@ -193,7 +192,7 @@ func WithLogger(l io.Writer) optFunc { // WithMaxIterator allows to set custom output channel buffer. // -// Note: can be used only with [FindWithIterator]. +// Note: can be used only with [FindWithIterator] or has no effect. func WithMaxIterator(max int) optFunc { return func(o *options) { o.maxIter = max diff --git a/template.go b/template.go index 191b43c..e1e3a2e 100644 --- a/template.go +++ b/template.go @@ -96,7 +96,7 @@ func (t *Template) Match(str string) bool { switch { case t.base == "": - return match + return false case t.base == "*": match = true case strings.Contains(str, t.base):