feat: add list command with global filters

This commit is contained in:
Rogee
2025-10-29 16:08:46 +08:00
parent 88563d48e2
commit fa57af8a26
29 changed files with 1892 additions and 25 deletions

View File

@@ -0,0 +1,143 @@
package traversal
import (
"errors"
"io/fs"
"os"
"path/filepath"
"strings"
)
// Walker streams filesystem entries relative to a working directory.
type Walker struct{}
// NewWalker constructs a new Walker with default behavior.
func NewWalker() *Walker {
return &Walker{}
}
// Walk traverses starting at root and invokes fn for each matching entry.
//
// The callback receives the relative path, os.DirEntry metadata, and depth.
// Directories that are symbolic links are not descended into when recursive is true.
func (w *Walker) Walk(
root string,
recursive bool,
includeDirs bool,
includeHidden bool,
maxDepth int,
fn func(relPath string, entry fs.DirEntry, depth int) error,
) error {
if root == "" {
return errors.New("walk root cannot be empty")
}
info, err := os.Stat(root)
if err != nil {
return err
}
if !info.IsDir() {
return errors.New("walk root must be a directory")
}
rootAbs, err := filepath.Abs(root)
if err != nil {
return err
}
walker := func(path string, d fs.DirEntry, err error) error {
if err != nil {
// propagate traversal errors to caller for logging/handling
return err
}
rel, relErr := filepath.Rel(rootAbs, path)
if relErr != nil {
return relErr
}
rel = filepath.Clean(rel)
depth := depthFor(rel)
if rel == "." {
// Skip emitting the root directory unless explicitly requested.
if includeDirs {
return fn(rel, d, depth)
}
return nil
}
if maxDepth > 0 && depth > maxDepth {
if d.IsDir() {
return fs.SkipDir
}
return nil
}
if !includeHidden && isHidden(rel) {
if d.IsDir() && recursive {
return fs.SkipDir
}
return nil
}
if d.IsDir() {
if !recursive && depth > 0 {
return fs.SkipDir
}
if !includeDirs {
// continue traversal but don't emit directory
return nil
}
}
if d.Type()&os.ModeSymlink != 0 && d.IsDir() {
// emit symlink entry but do not traverse into it
if err := fn(rel, d, depth); err != nil {
return err
}
if recursive {
return nil
}
return fs.SkipDir
}
return fn(rel, d, depth)
}
if recursive {
return filepath.WalkDir(rootAbs, walker)
}
entries, err := os.ReadDir(rootAbs)
if err != nil {
return err
}
for _, entry := range entries {
path := filepath.Join(rootAbs, entry.Name())
if err := walker(path, entry, nil); err != nil {
if errors.Is(err, fs.SkipDir) {
continue
}
return err
}
}
return nil
}
func depthFor(rel string) int {
if rel == "." || rel == "" {
return 0
}
return strings.Count(rel, string(filepath.Separator))
}
func isHidden(rel string) bool {
parts := strings.Split(rel, string(filepath.Separator))
for _, part := range parts {
if len(part) > 0 && part[0] == '.' {
return true
}
}
return false
}