Skip to content

Commit e5eb5c7

Browse files
authored
Merge pull request #7 from lwileczek/f/pos-pattern
FEAT: Allow positional arguments
2 parents 1c84772 + 99a8eaa commit e5eb5c7

File tree

7 files changed

+254
-169
lines changed

7 files changed

+254
-169
lines changed

.github/workflows/go.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ jobs:
2222
- name: Set up Go
2323
uses: actions/setup-go@v4
2424
with:
25-
go-version: 1.21
25+
go-version: '>=1.21.0'
26+
cache: true
2627

2728
- name: Build the project
2829
run: go build -v ./...

.github/workflows/goreleaser.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ jobs:
1616
- uses: actions/checkout@v3
1717
with:
1818
fetch-depth: 0
19+
1920
- run: git fetch --force --tags
21+
2022
- uses: actions/setup-go@v4
2123
with:
2224
go-version: '>=1.21.0'
2325
cache: true
26+
2427
# More assembly might be required: Docker logins, GPG, etc. It all depends
2528
# on your needs.
2629
- uses: goreleaser/goreleaser-action@v4

Makefile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
# Generic build command removing CGO and debugging info for smaller release
22
build:
3-
CGO_ENABLED=0 go build -o gf -ldflags="-s -w" main.go
3+
CGO_ENABLED=0 go build -o gof -ldflags="-s -w" ./*.go
4+
5+
# Format code
6+
fmt:
7+
go fmt ./...
8+
9+
# check the code for mistakes
10+
lint:
11+
go vet ./...
412

513
#This will build the binary specifically for newer x86-64 machines. It assumes the CPU
614
#is newer like most modern desktop cpus, hence amd64 lvl v3
715
# Docs: https://github.com/golang/go/wiki/MinimumRequirements#amd64
816
# With a small program like this we likely won't see a speedup but I like the opporunity
917
# to go fast
1018
buildx86:
11-
GOARCH=amd64 GOAMD64="v3" CGO_ENABLED=0 go build -o gf -ldflags="-s -w" main.go
19+
GOARCH=amd64 GOAMD64="v3" CGO_ENABLED=0 go build -o gof -ldflags="-s -w" ./*.go

README.md

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,42 @@
11
# Go Find
2-
A file finder written in Go for my needs.
2+
A file finder written in Go.
33

4-
Uses a worker pool to search through directory trees quicker and take advantage of multi-core machines.
5-
Go concurrency allows it to run on single core machines.
6-
7-
If you're looking for something specific you can limit the number of responses
4+
Uses a worker pool to search through directory trees quicker by taking advantage of multi-core machines.
5+
Go concurrency allows it to run on single core machines.
86

97
### Features
10-
- Concurrency which may speed up or slow down the search depending on the tree structure.
8+
- Concurrency search directories for desired pattern
9+
- Exit early with a max results option
1110
- Common paths are ignored to save time and checks, such as `node_modules`, `.git`, and python `venv`.
1211
- Searches for a pattern using basic sub-string
12+
- Use a worker pool to avoid spinning up too many go routines if there are _many_ directories
1313

1414
## Warning
1515
> **warning**
1616
> Does not work on windows and I'm not trying too support this
1717
18-
## TODO
19-
- [ ] user cobra for for a better cli experience
20-
- [ ] sub command to print paths that are ignored
21-
- [ ] create case sensative/insensative searching
22-
- [ ] exclude additional directories or patters
23-
- [ ] include specific patterns in the search path
24-
- [ ] switch to ignore all hidden files/directories
25-
- [x] can we create a worker pool instead of the number of initial directories
26-
- [x] cap the number of returned responses, say 1 (quit after the first match!)
27-
- [x] Auto-generate releases
28-
- [ ] cap the depth of the search
29-
- [x] use select statement with a fallback queue to prevent deadlock from happening
30-
3118
## Using
19+
### Usage
20+
Customise your search with the following flags
21+
22+
```bash
23+
Usage: goFind [OPTIONS] <pattern> <path>
24+
set pattern & path positionally or via flags
25+
-c int
26+
The maximum number of results to find (default -1)
27+
-d string
28+
The starting directory to check for files (default ".")
29+
-i perform a case insensative search
30+
-p string
31+
A pattern to check for within the file names
32+
-q int
33+
The max work queue size (default 512)
34+
-v print the version and build information
35+
-w int
36+
Number of workers (default -1)
37+
```
38+
39+
## Contributing
3240
### Build
3341
build with `go build -o gf main.go` and run with `./gf -d <starting-path> -p <pattern-to-match-on>`
3442
#### Requirements
@@ -40,18 +48,16 @@ You can use the [makefile](./Makefile) to build a production release with `make
4048
- Make
4149
- Go 1.21+
4250
43-
### Flags
44-
Customise your search with the following flags
45-
```bash
46-
Usage of ./gf:
47-
-c int
48-
The maximum number of results to find (default -1)
49-
-d string
50-
The starting directory to check for files (default ".")
51-
-p string
52-
A pattern to check for within the file names
53-
-q int
54-
The max work queue size (default 2048)
55-
-w int
56-
Number of workers (default 8)
57-
```
51+
## Roadmap
52+
- [x] can we create a worker pool instead of the number of initial directories
53+
- [x] cap the number of returned responses, say 1 (quit after the first match!)
54+
- [x] Auto-generate releases
55+
- [x] cap the depth of the search
56+
- [x] use select statement with a fallback queue to prevent deadlock from happening
57+
- [x] use postional args or flags to set the pattern/path
58+
- [ ] Add flag to print which paths will be ignored
59+
- [ ] create case sensative/insensative searching
60+
- [ ] exclude additional directories or patters
61+
- [ ] include specific patterns in the search path
62+
- [ ] switch to ignore all hidden files/directories
63+
- [ ] look for `.gitignore` files and use contents to build ignored patterns

cli.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"os"
7+
"runtime"
8+
)
9+
10+
// Config the programs configuration used to set behavior
11+
type Config struct {
12+
QueueSize int
13+
MaxResults int
14+
Workers int
15+
Dir string
16+
Pattern string
17+
Insensative bool
18+
Version bool
19+
}
20+
21+
// myUsage overwrite default usage message from ./<bin> --help
22+
func myUsage() {
23+
bin := "gof"
24+
if len(os.Args) > 0 {
25+
bin = os.Args[0]
26+
}
27+
28+
fmt.Printf("Usage: %s [OPTIONS] <pattern> <path>\n", bin)
29+
fmt.Println(" set pattern & path positionally or via flags")
30+
flag.PrintDefaults()
31+
}
32+
33+
// parseArgs all arguments from the user to use with the program
34+
func parseArgs() *Config {
35+
flag.Usage = myUsage
36+
37+
//The number of workers. If there are more workers the system can read from
38+
//the work queue more often and a larger queue is not required.
39+
workers := flag.Int("w", -1, "Number of workers")
40+
//If the queue overflows we'll use a slice to store work which might slow the system
41+
queueSize := flag.Int("q", 512, "The max work queue size")
42+
maxResults := flag.Int("c", -1, "The maximum number of results to find")
43+
dir := flag.String("d", ".", "The starting directory to check for files")
44+
pattern := flag.String("p", "", "A pattern to check for within the file names")
45+
insensative := flag.Bool("i", false, "perform a case insensative search")
46+
v := flag.Bool("v", false, "print the version and build information")
47+
flag.Parse()
48+
49+
p := *pattern
50+
if p == "" {
51+
p = flag.Arg(0)
52+
}
53+
54+
w := *workers
55+
if *workers <= 0 {
56+
// magic 2, anecdotal evidence of better performance over NumCPU
57+
w = runtime.NumCPU() + 2
58+
}
59+
60+
if *dir == "." && flag.Arg(1) != "" {
61+
*dir = flag.Arg(1)
62+
}
63+
64+
//Only for OSX/Linux, sorry windows
65+
//Remove any trailing slashes in the path
66+
if (*dir)[len(*dir)-1:] == "/" {
67+
*dir = string((*dir)[0 : len(*dir)-1])
68+
}
69+
70+
return &Config{
71+
QueueSize: *queueSize,
72+
MaxResults: *maxResults,
73+
Workers: w,
74+
Dir: *dir,
75+
Pattern: p,
76+
Insensative: *insensative,
77+
Version: *v,
78+
}
79+
}

main.go

Lines changed: 11 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
package main
22

33
import (
4-
"flag"
54
"fmt"
6-
"os"
7-
"runtime"
8-
"strings"
9-
"sync"
105
)
116

127
var IGNORE_PATHS = [7]string{
@@ -31,41 +26,24 @@ func printBuildInfo() {
3126
}
3227

3328
func main() {
34-
//The number of workers. If there are more workers the system can read from
35-
//the work queue more often and a larger queue is not required.
36-
workers := flag.Int("w", -1, "Number of workers")
37-
//If the queue overflows we'll use a slice to store work which might slow the system
38-
queueSize := flag.Int("q", 512, "The max work queue size")
39-
maxResults := flag.Int("c", -1, "The maximum number of results to find")
40-
dir := flag.String("d", ".", "The starting directory to check for files")
41-
pattern := flag.String("p", "", "A pattern to check for within the file names")
42-
v := flag.Bool("v", false, "print the version and build information")
43-
flag.Parse()
44-
45-
if *v {
29+
cfg := parseArgs()
30+
if cfg.Version {
4631
printBuildInfo()
4732
return
4833
}
4934

50-
if *pattern == "" {
51-
fmt.Println("No pattern provided")
35+
if cfg.Pattern == "" {
36+
fmt.Println("No pattern found, please provide a search pattern")
5237
return
5338
}
5439

55-
w := *workers
56-
if *workers <= 0 {
57-
w = runtime.NumCPU() + 2
58-
}
59-
60-
//Only for OSX/Linux, sorry windows
61-
//Remove any trailing slashes in the path
62-
if (*dir)[len(*dir)-1:] == "/" {
63-
*dir = string((*dir)[0 : len(*dir)-1])
40+
if cfg.Insensative {
41+
fmt.Println("WARN: case insensative search is not yet supported")
6442
}
6543

66-
printCh := make(chan string, w)
44+
printCh := make(chan string, cfg.Workers)
6745
//The system will reach deadlock if the work queue reaches capacity
68-
workQ := make(chan string, *queueSize)
46+
workQ := make(chan string, cfg.QueueSize)
6947
//To avoid deadlock, send tasks here which will have a non-blocky retry
7048
//func to add tasks back to workQ
7149
failover := make(chan string)
@@ -77,110 +55,11 @@ func main() {
7755
//defer close(dirCount)
7856
//defer close(failover)
7957
go handleFailover(workQ, failover)
80-
go createWorkerPool(pattern, workQ, failover, printCh, dirCount, w)
58+
go createWorkerPool(cfg.Pattern, workQ, failover, printCh, dirCount, cfg.Workers)
8159

8260
//Send first work request
83-
workQ <- *dir
61+
workQ <- cfg.Dir
8462

8563
//Print all results
86-
showResults(printCh, maxResults)
87-
}
88-
89-
func showResults(ch chan string, limit *int) {
90-
if *limit > 0 {
91-
n := 0
92-
for item := range ch {
93-
fmt.Println(item)
94-
n++
95-
if n >= *limit {
96-
return
97-
}
98-
}
99-
} else {
100-
for item := range ch {
101-
fmt.Println(item)
102-
}
103-
}
104-
}
105-
106-
func dirChecker(in chan int, work chan string) {
107-
n := 1
108-
for i := range in {
109-
n += i
110-
if n <= 0 {
111-
close(work)
112-
return
113-
}
114-
}
115-
}
116-
117-
func createWorkerPool(p *string, in chan string, failover chan string, results chan string, cnt chan int, w int) {
118-
var wg sync.WaitGroup
119-
for i := 0; i < w; i++ {
120-
wg.Add(1)
121-
go func() {
122-
defer wg.Done()
123-
search(p, in, failover, results, cnt)
124-
}()
125-
}
126-
wg.Wait()
127-
close(results)
128-
}
129-
func search(pattern *string, in chan string, failover chan string, out chan string, cnt chan int) {
130-
for path := range in {
131-
items, err := os.ReadDir(path)
132-
if err != nil {
133-
fmt.Println("Error reading the directory", path)
134-
fmt.Println(err)
135-
cnt <- -1
136-
}
137-
ItemSearch:
138-
for _, item := range items {
139-
if item.IsDir() {
140-
//Don't dive into directories I don't care about
141-
for _, p := range IGNORE_PATHS {
142-
if p == item.Name() {
143-
continue ItemSearch
144-
}
145-
}
146-
subPath := fmt.Sprintf("%s/%s", path, item.Name())
147-
cnt <- 1
148-
select {
149-
case in <- subPath:
150-
case failover <- subPath:
151-
}
152-
}
153-
//Always check if the name of the thing matches pattern, including directory names
154-
if strings.Index(item.Name(), *pattern) >= 0 {
155-
//subPath is repeated but no point in creating an allocation if not required
156-
subPath := fmt.Sprintf("%s/%s", path, item.Name())
157-
out <- subPath
158-
}
159-
160-
}
161-
//We finished reading everything in the dir, tell the accounted we finished
162-
cnt <- -1
163-
}
164-
}
165-
166-
func handleFailover(work, fail chan string) {
167-
var q []string
168-
for {
169-
task := <-fail
170-
q = append(q, task)
171-
//TODO: Add verbose logging here so users can check if the failover was used
172-
for {
173-
select {
174-
case work <- q[0]:
175-
q = q[1:]
176-
case task := <-fail:
177-
q = append(q, task)
178-
default:
179-
}
180-
//I don't know if we'll get an issue with `work <- q[0]` unless we have this
181-
if len(q) == 0 {
182-
break
183-
}
184-
}
185-
}
64+
showResults(printCh, &cfg.MaxResults)
18665
}

0 commit comments

Comments
 (0)