Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file removed commands/convert.go
Empty file.
339 changes: 339 additions & 0 deletions commands/fdetect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
package commands

import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"

"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
"github.com/urfave/cli/v3"
"golang.org/x/term"
)

// COLORS
var (
colorCyan = color.New(color.FgCyan, color.Bold)
colorYellow = color.New(color.FgYellow)
)

// TERMINAL WIDTH
func termWidth() int {
width, _, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil || width < 40 {
return 60
}
if width > 80 {
return 80
}
return width
}

// SECTION HEADER
func printSection(icon, title string) {
w := termWidth()
dashes := w - len(icon) - len(title) - 6
if dashes < 2 {
dashes = 2
}
fmt.Println()
colorCyan.Printf("┌─ %s %s %s\n\n", icon, title, strings.Repeat("─", dashes))
}

// DIVIDER
func printDivider() {
colorCyan.Println("└" + strings.Repeat("─", termWidth()-1))
}

// NO RESULT
func noResult(msg string) {
colorYellow.Println(" ⚠ " + msg)
}

// TABLE HELPER
func newTable(headers []string) *tablewriter.Table {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader(headers)
table.SetBorder(true)
table.SetCenterSeparator("┼")
table.SetColumnSeparator("│")
table.SetRowSeparator("─")
return table
}

func FileDetectCommand() *cli.Command {
return &cli.Command{
Name: "sift",
Usage: "Detect and scan files with various filters",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "emt", Aliases: []string{"e"}, Usage: "Find empty files"},
&cli.IntFlag{Name: "rec", Aliases: []string{"r"}, Usage: "Find files modified in last N days"},
&cli.Float64Flag{Name: "lrg", Aliases: []string{"l"}, Usage: "Find files larger than N GB"},
&cli.BoolFlag{Name: "ext", Aliases: []string{"x"}, Usage: "Show file extension summary"},
&cli.IntFlag{Name: "top", Aliases: []string{"t"}, Usage: "Show top N largest files"},
&cli.BoolFlag{Name: "dup", Aliases: []string{"d"}, Usage: "Find duplicate files by name"},
&cli.BoolFlag{Name: "log", Aliases: []string{"L"}, Usage: "Find all log files (.log)"},
&cli.StringFlag{Name: "p", Aliases: []string{"P"}, Value: ".", Usage: "Directory path to scan"},
},
Action: runDetect,
}
}

func runDetect(ctx context.Context, c *cli.Command) error {

path := c.String("p")

// PATH VALIDATION
if _, err := os.Stat(path); os.IsNotExist(err) {
colorYellow.Println(" ⚠ Invalid path:", path)
return nil
}

// no flag check
if !c.Bool("emt") && !c.Bool("ext") && !c.Bool("dup") && !c.Bool("log") &&
c.Int("rec") == 0 && c.Float64("lrg") == 0 && c.Int("top") == 0 {
fmt.Println()
colorYellow.Println(" ⚠ No flag provided. Use --help to see all available flags.")
return nil
}

// EMPTY FILES
if c.Bool("emt") {
printSection("🔍", "Empty Files")
table := newTable([]string{"#", "FILE"})
count := 0

filepath.WalkDir(path, func(p string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
info, err := d.Info()
if err != nil {
return nil
}
if info.Size() == 0 {
count++
table.Append([]string{fmt.Sprintf("%d", count), p})
}
return nil
})

if count == 0 {
noResult("No empty files found.")
} else {
table.Render()
}
printDivider()
}

// RECENT FILES
if c.Int("rec") > 0 {
days := c.Int("rec")
cutoff := time.Now().AddDate(0, 0, -days)

printSection("🕐", fmt.Sprintf("Recently Modified — Last %d Day(s)", days))
table := newTable([]string{"#", "FILE", "MODIFIED"})
count := 0

filepath.WalkDir(path, func(p string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
info, err := d.Info()
if err != nil {
return nil
}
if info.ModTime().After(cutoff) {
count++
table.Append([]string{
fmt.Sprintf("%d", count),
p,
info.ModTime().Format("2006-01-02 15:04:05"),
})
}
return nil
})

if count == 0 {
noResult(fmt.Sprintf("No files modified in last %d day(s).", days))
} else {
table.Render()
}
printDivider()
}

// LARGE FILES
if c.Float64("lrg") > 0 {
sizeGB := c.Float64("lrg")
minBytes := int64(sizeGB * 1024 * 1024 * 1024)

printSection("💾", "Large Files")
table := newTable([]string{"#", "FILE", "SIZE"})
count := 0

filepath.WalkDir(path, func(p string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
info, err := d.Info()
if err != nil {
return nil
}
if info.Size() > minBytes {
count++
table.Append([]string{
fmt.Sprintf("%d", count),
p,
fmt.Sprintf("%.2f GB", float64(info.Size())/(1024*1024*1024)),
})
}
return nil
})

if count == 0 {
noResult("No large files found.")
} else {
table.Render()
}
printDivider()
}

// TOP FILES
if c.Int("top") > 0 {
n := c.Int("top")

type file struct {
path string
size int64
}
var files []file

filepath.WalkDir(path, func(p string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
info, err := d.Info()
if err != nil {
return nil
}
files = append(files, file{p, info.Size()})
return nil
})

sort.Slice(files, func(i, j int) bool {
return files[i].size > files[j].size
})

if n > len(files) {
n = len(files)
}

printSection("🏆", fmt.Sprintf("Top %d Files", n))
table := newTable([]string{"RANK", "FILE", "SIZE"})
for i := 0; i < n; i++ {
table.Append([]string{
fmt.Sprintf("#%d", i+1),
files[i].path,
fmt.Sprintf("%.2f MB", float64(files[i].size)/(1024*1024)),
})
}

if n == 0 {
noResult("No files found.")
} else {
table.Render()
}
printDivider()
}

// EXTENSIONS
if c.Bool("ext") {
printSection("📋", "Extensions")
extMap := make(map[string]int)

filepath.WalkDir(path, func(p string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
ext := strings.ToLower(filepath.Ext(p))
if ext == "" {
ext = "(none)"
}
extMap[ext]++
return nil
})

table := newTable([]string{"EXT", "COUNT"})
for k, v := range extMap {
table.Append([]string{k, fmt.Sprintf("%d", v)})
}

if len(extMap) == 0 {
noResult("No files found.")
} else {
table.Render()
}
printDivider()
}

// DUPLICATES
if c.Bool("dup") {
printSection("♊", "Duplicates")
nameMap := make(map[string][]string)

filepath.WalkDir(path, func(p string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
nameMap[d.Name()] = append(nameMap[d.Name()], p)
return nil
})

table := newTable([]string{"FILE", "PATHS"})
count := 0
for name, paths := range nameMap {
if len(paths) > 1 {
count++
table.Append([]string{name, strings.Join(paths, " | ")})
}
}

if count == 0 {
noResult("No duplicates found.")
} else {
table.Render()
}
printDivider()
}

// LOG FILES
if c.Bool("log") {
printSection("📄", "Log Files")
table := newTable([]string{"FILE"})
count := 0

filepath.WalkDir(path, func(p string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
if strings.HasSuffix(strings.ToLower(d.Name()), ".log") {
count++
table.Append([]string{p})
}
return nil
})

if count == 0 {
noResult("No log files found.")
} else {
table.Render()
}
printDivider()
}

return nil
}
15 changes: 14 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,17 @@ module funk

go 1.25.7

require github.com/urfave/cli/v3 v3.7.0 // indirect
require (
github.com/fatih/color v1.18.0
github.com/olekukonko/tablewriter v0.0.5
github.com/urfave/cli/v3 v3.7.0
golang.org/x/term v0.41.0
)

require (
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
golang.org/x/sys v0.42.0 // indirect
)
26 changes: 26 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,28 @@
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v3 v3.7.0 h1:AGSnbUyjtLiM+WJUb4dzXKldl/gL+F8OwmRDtVr6g2U=
github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading