zutils/cmd/find.go
selamanapps 5b334071e0 feat: add user-friendly help system and new commands
New Commands:
- z read - Smart file reader with auto-detection (markdown, syntax highlighting)
- z sys - System overview (CPU, RAM, Disk, processes)
- z find - File search with pattern/size/time filters
- z search - User-friendly grep alternative
- z usage - Process resource usage monitor

Enhancements:
- Unified help system with consistent error messages
- Help on error shows usage and examples for each command
- "Command not found" suggests similar commands
- Directory info now detects .gitignore and shows ignored files
- File handler registry for extensible file type detection
- Added Italic, Dim, Underline color constants
2026-05-02 01:12:55 +03:00

192 lines
No EOL
4.6 KiB
Go

package cmd
import (
"flag"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/zemenawi/zutils/pkg/colors"
"github.com/zemenawi/zutils/pkg/formatter"
)
type FindOptions struct {
Pattern string
SizeMin int64
SizeMax int64
ModifiedDays int
FileType string
MaxDepth int
ShowHidden bool
}
type FindResult struct {
Path string
Size int64
Modified time.Time
IsDir bool
Permissions string
}
func FindCommand(args []string) error {
fs := flag.NewFlagSet("find", flag.ContinueOnError)
options := FindOptions{}
fs.StringVar(&options.Pattern, "name", "*", "File name pattern (glob)")
fs.StringVar(&options.FileType, "type", "", "File type: f (file), d (dir)")
sizeMin := fs.Int64("size-min", 0, "Minimum size in bytes")
sizeMax := fs.Int64("size-max", 0, "Maximum size in bytes")
modifiedDays := fs.Int("mtime", 0, "Modified within N days")
maxDepth := fs.Int("depth", -1, "Maximum directory depth")
showHidden := fs.Bool("hidden", false, "Show hidden files")
if err := fs.Parse(args); err != nil {
if err == flag.ErrHelp {
return nil
}
return fmt.Errorf("invalid arguments: %v\n\nUsage: z find [path] [options]\n\nOptions:\n%s\n\nExamples:\n z find . -name '*.go' Find all Go files\n z find . -mtime 7 Files modified in last 7 days\n z find . -size +1m Files larger than 1MB\n z find . -type d Directories only", err, getFlagDefaults(fs))
}
options.SizeMin = *sizeMin
options.SizeMax = *sizeMax
options.ModifiedDays = *modifiedDays
options.MaxDepth = *maxDepth
options.ShowHidden = *showHidden
path := "."
if fs.NArg() > 0 {
path = fs.Arg(0)
}
if path == "" {
return fmt.Errorf("missing search path\n\nUsage: z find [path] [options]")
}
results, err := findFiles(path, options)
if err != nil {
return fmt.Errorf("error finding files: %v", err)
}
if len(results) == 0 {
fmt.Printf("%sNo files found matching criteria%s\n", colors.Gray, colors.Reset)
return nil
}
sort.Slice(results, func(i, j int) bool {
return results[i].Modified.After(results[j].Modified)
})
PrintBoxHeader(fmt.Sprintf("FIND RESULTS (%d files)", len(results)), colors.Green)
headers := []string{"Path", "Size", "Modified"}
data := make([][]string, 0, len(results))
for _, result := range results {
relPath := result.Path
if strings.Contains(relPath, "/") {
parts := strings.Split(relPath, "/")
relPath = strings.Join(parts[len(parts)-2:], "/")
if len(parts) > 2 {
relPath = "..." + string(filepath.Separator) + strings.Join(parts[len(parts)-2:], "/")
}
}
modStr := result.Modified.Format("Jan 02 15:04")
if time.Since(result.Modified) > 30*24*time.Hour {
modStr = result.Modified.Format("Jan 02 2006")
}
row := []string{
relPath,
formatter.FormatSize(result.Size),
modStr,
}
data = append(data, row)
}
formatter.PrintTable(headers, data, colors.Cyan)
fmt.Println()
return nil
}
func getFlagDefaults(fs *flag.FlagSet) string {
var result []string
fs.VisitAll(func(f *flag.Flag) {
result = append(result, fmt.Sprintf(" -%s %s (default: %v)", f.Name, f.Usage, f.DefValue))
})
return strings.Join(result, "\n")
}
func findFiles(path string, opts FindOptions) ([]FindResult, error) {
var results []FindResult
err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if filePath == path {
return nil
}
if !opts.ShowHidden && filepath.Base(filePath)[0] == '.' {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
if opts.MaxDepth > 0 {
relPath, _ := filepath.Rel(path, filePath)
if strings.Count(relPath, string(filepath.Separator)) > opts.MaxDepth {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
}
if opts.FileType == "f" && info.IsDir() {
return nil
}
if opts.FileType == "d" && !info.IsDir() {
return nil
}
baseName := filepath.Base(filePath)
matched, err := filepath.Match(opts.Pattern, baseName)
if err != nil || !matched {
return nil
}
if opts.SizeMin > 0 && info.Size() < opts.SizeMin {
return nil
}
if opts.SizeMax > 0 && info.Size() > opts.SizeMax {
return nil
}
if opts.ModifiedDays > 0 {
cutoff := time.Now().AddDate(0, 0, -opts.ModifiedDays)
if info.ModTime().Before(cutoff) {
return nil
}
}
results = append(results, FindResult{
Path: filePath,
Size: info.Size(),
Modified: info.ModTime(),
IsDir: info.IsDir(),
Permissions: info.Mode().String(),
})
return nil
})
return results, err
}