192 lines
4.6 KiB
Go
192 lines
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
|
||
|
|
}
|