Skip to content
Merged
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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
A small and [fast](#benchmark) recursive grep written in Go. Install with `go github.com/bep/grrep@latest`.

```
usage: grrep [-q] [-F] [-i] [-w] [-v] [-d N] [--no-ignore] PATTERN [PATH]
usage: grrep [-q] [-F] [-i] [-w] [-v] [-d N] [--hidden] [--no-ignore] PATTERN [PATH]

Flags:
-F treat PATTERN as a fixed string, not a regex
-i case-insensitive match
-d, --max-depth=N search at most N directory levels (1 = root only, 0 = nothing)
--hidden search hidden files and directories (.git is always skipped)
--no-ignore do not respect .gitignore/.ignore files
-q quiet: suppress match output
-v select non-matching lines
Expand All @@ -25,7 +26,7 @@ The other motivation was curiosity: how far Go and the standard library can take
* Honors `.gitignore` and `.ignore` files in the search tree.
* Does **not** honor `~/.gitignore_global`.
* Skips the `# Managed by gitjoin … # End gitjoin managed section` block when reading any `.gitignore`.
* Skips hidden files and directories.
* Skips hidden files and directories by default; pass `--hidden` to include them (`.git` is always skipped).
* Skips files whose first 8 KiB contain a NUL byte (binary heuristic).

## Benchmark
Expand Down
21 changes: 16 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type grepper struct {
root string
quiet bool
invert bool // -v: emit non-matching lines instead
hidden bool // --hidden: descend into dot-dirs/files (.git is always skipped)
maxDepth int // 0 = unlimited; passed through to fastwalk.Config
ctx context.Context
paths chan string
Expand All @@ -97,6 +98,7 @@ func run() (bool, error) {
noIgnore bool
opts internal.MatchOpts
invert bool
hidden bool
maxDepth int
cpuProfile string
memProfile string
Expand All @@ -108,6 +110,7 @@ func run() (bool, error) {
flag.BoolVar(&opts.CaseInsensitive, "i", false, "case-insensitive match")
flag.BoolVar(&opts.WordBoundary, "w", false, "match only at word boundaries")
flag.BoolVar(&invert, "v", false, "select non-matching lines")
flag.BoolVar(&hidden, "hidden", false, "search hidden files and directories (.git is always skipped)")
flag.IntVar(&maxDepth, "max-depth", -1, "search at most N directory levels (1 = root only, 0 = nothing)")
flag.IntVar(&maxDepth, "d", -1, "") // alias for --max-depth; suppressed in -h, paired with it below
// Hidden profiling flags — registered but suppressed in -h via flag.Usage below.
Expand All @@ -116,7 +119,7 @@ func run() (bool, error) {
flag.StringVar(&mutexProfile, "profile-mutex", "", "")
flag.Usage = func() {
out := flag.CommandLine.Output()
fmt.Fprintln(out, "usage: grrep [-q] [-F] [-i] [-w] [-v] [-d N] [--no-ignore] PATTERN [PATH]")
fmt.Fprintln(out, "usage: grrep [-q] [-F] [-i] [-w] [-v] [-d N] [--hidden] [--no-ignore] PATTERN [PATH]")
fmt.Fprintln(out)
fmt.Fprintln(out, "Flags:")
flag.VisitAll(func(f *flag.Flag) {
Expand All @@ -139,7 +142,7 @@ func run() (bool, error) {

args := flag.Args()
if len(args) < 1 {
return false, fmt.Errorf("usage: grrep [-q] [-F] [-i] [-w] [-v] [-d N] [--no-ignore] PATTERN [PATH]")
return false, fmt.Errorf("usage: grrep [-q] [-F] [-i] [-w] [-v] [-d N] [--hidden] [--no-ignore] PATTERN [PATH]")
}
root := "."
if len(args) >= 2 {
Expand Down Expand Up @@ -190,6 +193,7 @@ func run() (bool, error) {
root: root,
quiet: quiet,
invert: invert,
hidden: hidden,
maxDepth: maxDepth,
ctx: gCtx,
paths: make(chan string, 256),
Expand Down Expand Up @@ -254,8 +258,15 @@ func (g *grepper) walk() error {
}
name := d.Name()
if d.IsDir() {
if path != g.root && strings.HasPrefix(name, ".") {
return fs.SkipDir
if path != g.root {
// .git is always skipped, even with --hidden: it's a VCS
// internals directory, not something users want to grep.
if name == ".git" {
return fs.SkipDir
}
if !g.hidden && strings.HasPrefix(name, ".") {
return fs.SkipDir
}
}
if g.ignores != nil && path != g.root {
rel, err := filepath.Rel(g.root, path)
Expand All @@ -270,7 +281,7 @@ func (g *grepper) walk() error {
}
return nil
}
if strings.HasPrefix(name, ".") {
if !g.hidden && strings.HasPrefix(name, ".") {
return nil
}
if !d.Type().IsRegular() {
Expand Down
23 changes: 23 additions & 0 deletions testscripts/hidden.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# By default, dot-files and dot-directories are skipped.
grrep HIT
stdout './visible.txt:1:HIT'
! stdout '\.hidden\.txt'
! stdout '\.github/'
! stdout '\.git/'

# --hidden descends into dot-directories and matches dot-files,
# but .git is still skipped.
grrep --hidden HIT
stdout './visible.txt:1:HIT'
stdout './\.hidden\.txt:1:HIT'
stdout './\.github/workflows/ci\.yml:1:HIT'
! stdout '\.git/'

-- visible.txt --
HIT
-- .hidden.txt --
HIT
-- .github/workflows/ci.yml --
HIT
-- .git/config --
HIT
Loading