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 +} diff --git a/options.go b/options.go index bcaf3e7..59e2d1a 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,11 +55,66 @@ 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 { + if o.log { + if _, err := fmt.Fprintf(o.logger, "error: %s\n", e); err != nil { + return fmt.Errorf("%w: %w", e, err) + } + } + + if o.skip { + return nil + } + + return e +} + +func (o *options) printOutput(str string) error { + if o.out { + if _, err := fmt.Fprintln(o.output, str); err != nil { + return err + } + } + + return nil +} + +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. func SearchFor(t uint8) optFunc { return Only(t) } @@ -97,30 +158,52 @@ 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. +// Defaults to [os.Stdout] and can be changed with [WithLogger]. 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]. +// Defaults to [os.Stdout] and can be changed with [WithWriter]. +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]. +// Also sets [WithOutput] to true. +// +// 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) { - o.max = i + o.output = out + o.out = true } } // 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) { o.logger = l + o.log = true + } +} + +// WithMaxIterator allows to set custom output channel buffer. +// +// Note: can be used only with [FindWithIterator] or has no effect. +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 } } @@ -150,28 +233,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 - } -} diff --git a/template.go b/template.go index b461f41..e1e3a2e 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 == "*" { - match = true - } else if strings.Contains(str, t.base) { + switch { + case t.base == "": + return false + case 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.