012 Project 12: Build a find Clone
012 Build a find Clone
Search a directory tree by name substring, entry type, and minimum size. Use filepath.WalkDir for efficient streaming walks without loading the whole tree into memory.
walk tree -> filter (name/type/size) -> print matching paths
Problem statement
gfind [root] [-name SUBSTR] [-type f|d] [-min-size BYTES]
Default root is .. Filters compose as AND. Name match is case-insensitive substring on the base name (not full path)—document this; classic find uses globs.
Acceptance criteria
- Walks recursively from root
-namefilters by basename contains (case-insensitive)-type ffiles only;-type ddirs only; empty = both-min-sizeapplies to files only- Permission errors: skip entry (or print to stderr) without aborting whole walk
- Stdlib only;
go buildclean
Setup
mkdir gfind && cd gfind
go mod init example.com/gfind
# go 1.27Full main.go
package main
import (
"flag"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)
func match(path string, d fs.DirEntry, name, typeFlag string, minSize int64) (bool, error) {
base := filepath.Base(path)
if name != "" && !strings.Contains(strings.ToLower(base), strings.ToLower(name)) {
return false, nil
}
switch typeFlag {
case "f":
if d.IsDir() {
return false, nil
}
case "d":
if !d.IsDir() {
return false, nil
}
case "":
// ok
default:
return false, fmt.Errorf("invalid -type %q (want f, d, or empty)", typeFlag)
}
if minSize > 0 && !d.IsDir() {
info, err := d.Info()
if err != nil {
return false, nil // skip unreadable
}
if info.Size() < minSize {
return false, nil
}
}
return true, nil
}
func main() {
name := flag.String("name", "", "substring to match in filename")
typeFlag := flag.String("type", "", "f=file d=dir")
minSize := flag.Int64("min-size", 0, "minimum file size in bytes")
flag.Parse()
root := "."
if flag.NArg() > 0 {
root = flag.Arg(0)
}
// Validate type early
if *typeFlag != "" && *typeFlag != "f" && *typeFlag != "d" {
fmt.Fprintf(os.Stderr, "invalid -type %q\n", *typeFlag)
os.Exit(2)
}
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
fmt.Fprintf(os.Stderr, "skip %s: %v\n", path, err)
return nil // keep walking
}
ok, mErr := match(path, d, *name, *typeFlag, *minSize)
if mErr != nil {
return mErr
}
if ok {
fmt.Println(path)
}
return nil
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}Step-by-step build path
- Flags + optional root argument.
WalkDircallback: handle walk errors without failing the whole run.- Centralize filters in
matchfor unit tests. - Print paths as you go (streaming).
- Later: add prune (
-maxdepth) and globfilepath.Match.
Run and verification
go run . /etc -name conf -type f
go run . . -name go -type f
go run . . -min-size 1048576 -type f # files ≥ 1MiB
# empty name matches everything (careful on huge trees)
go run . /tmp -type d | headTests
package main
import (
"os"
"path/filepath"
"testing"
)
func TestMatchNameAndType(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "App.LOG")
if err := os.WriteFile(f, []byte("hi"), 0o644); err != nil {
t.Fatal(err)
}
info, err := os.Stat(f)
if err != nil {
t.Fatal(err)
}
// DirEntry from WalkDir; use type from os.DirEntry via ReadDir
entries, _ := os.ReadDir(dir)
var d os.DirEntry
for _, e := range entries {
if e.Name() == "App.LOG" {
d = e
break
}
}
ok, err := match(f, d, "log", "f", 0)
if err != nil || !ok {
t.Fatalf("ok=%v err=%v size=%d", ok, err, info.Size())
}
}go test ./...Stretch goals
- Glob
-name '*.go'viafilepath.Match. -maxdepth Nby counting path separators relative to root.-execstyle: print null-terminated forxargs -0.- Concurrent walk with worker pool (careful with FS limits).
- Skip directories (
.git,node_modules) with a skip list.
Pitfalls
| Pitfall | Fix |
|---|---|
Returning walk err aborts tree |
Log and return nil |
Info() on every dir for size |
Only when min-size set and file |
| Symlink loops | WalkDir doesn’t follow symlinks by default—good |
| Case-sensitive surprise | Document case-insensitive policy |
Learning goals
filepath.WalkDirandfs.DirEntry- Filter composition for CLI tools
- Resilient walks over imperfect trees