grepfruit 4.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +16 -3
- data/lib/grepfruit/cli.rb +6 -2
- data/lib/grepfruit/search.rb +31 -18
- data/lib/grepfruit/version.rb +1 -1
- data/lib/grepfruit.rb +6 -2
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c0935c8c65aa93564bbac5b7e4c109ac46df6f359a61da5f20f0fb5d1c50941
|
|
4
|
+
data.tar.gz: 9a5b235ee6c08a82c9dd1e416b3fce19d192224182a59bcf6ce3a73bf45a1d50
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a2bc081cc743ec0441464fb5af636f93666e93e5510f0a3694895f47c23cccfc413631fe8498458890af9e91e2beecaa5ced0369f0bf68a628bb46a10ca75554
|
|
7
|
+
data.tar.gz: 8c728cca15c6ce8cfe07248f5622508407ce2e4dd41459a01bd801652651eeb7c360a76e8c8bb99e2ff7d34be43d78b5520a6f59b068c808c5f85a29bd5c4cb9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## v4.1.0
|
|
2
|
+
|
|
3
|
+
- Added --relative-paths option to return match paths relative to the search directory
|
|
4
|
+
- Fixed absolute paths in --exclude and --include being silently ignored instead of rejected
|
|
5
|
+
- Fixed line exclusions also applying to files whose path only ends with the given path
|
|
6
|
+
- Fixed wildcards in --exclude and --include not matching hidden files when --search-hidden is enabled
|
|
7
|
+
- Minor performance improvements
|
|
8
|
+
|
|
1
9
|
## v4.0.2
|
|
2
10
|
|
|
3
11
|
- Fixed searches silently checking no files when the search path itself was hidden or matched an exclusion
|
data/README.md
CHANGED
|
@@ -40,7 +40,8 @@ result = Grepfruit.search(
|
|
|
40
40
|
truncate: 80,
|
|
41
41
|
search_hidden: false,
|
|
42
42
|
jobs: 4,
|
|
43
|
-
count: false
|
|
43
|
+
count: false,
|
|
44
|
+
relative_paths: false
|
|
44
45
|
)
|
|
45
46
|
```
|
|
46
47
|
|
|
@@ -66,6 +67,8 @@ Returns a hash (when `count: true`, the `matches` key is omitted):
|
|
|
66
67
|
}
|
|
67
68
|
```
|
|
68
69
|
|
|
70
|
+
Match paths are absolute by default. With `relative_paths: true`, they are relative to the search directory, or just the file name when a single file is searched. `search.path` is always absolute.
|
|
71
|
+
|
|
69
72
|
All keyword arguments correspond to their CLI flag counterparts.
|
|
70
73
|
|
|
71
74
|
## CLI
|
|
@@ -89,11 +92,12 @@ If no PATH is specified, Grepfruit searches the current directory.
|
|
|
89
92
|
| Option | Description |
|
|
90
93
|
|--------|-------------|
|
|
91
94
|
| `-r, --regex REGEX` | Regex pattern to search for (required) |
|
|
92
|
-
| `-e, --exclude x,y,z` | Comma-separated list of files, directories, or lines to exclude |
|
|
93
|
-
| `-i, --include x,y,z` | Comma-separated list of
|
|
95
|
+
| `-e, --exclude x,y,z` | Comma-separated list of files, directories, or lines to exclude: names, wildcards, paths relative to the search directory, or `file:line` |
|
|
96
|
+
| `-i, --include x,y,z` | Comma-separated list of files to include: names, wildcards, or paths relative to the search directory |
|
|
94
97
|
| `-t, --truncate N` | Truncate search result output to N characters |
|
|
95
98
|
| `-j, --jobs N` | Number of parallel workers (default: number of CPU cores) |
|
|
96
99
|
| `-c, --count` | Show only match counts, not match details |
|
|
100
|
+
| `-p, --relative-paths` | Show match paths relative to the search directory |
|
|
97
101
|
| `--search-hidden` | Include hidden files and directories in search |
|
|
98
102
|
| `--json` | Output results in JSON format |
|
|
99
103
|
|
|
@@ -200,6 +204,15 @@ grepfruit search -r 'TODO' --count
|
|
|
200
204
|
grepfruit search -r 'FIXME|TODO' -c -e 'vendor,node_modules'
|
|
201
205
|
```
|
|
202
206
|
|
|
207
|
+
**Relative Paths**
|
|
208
|
+
|
|
209
|
+
Match paths are absolute by default. Show them relative to the search directory instead:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
grepfruit search -r 'TODO' --relative-paths dev/my_app
|
|
213
|
+
grepfruit search -r 'TODO' -p dev/my_app/README.md # Shows just the file name
|
|
214
|
+
```
|
|
215
|
+
|
|
203
216
|
**Parallel Processing**
|
|
204
217
|
|
|
205
218
|
Grepfruit uses ractors for true parallel processing across CPU cores. Control the number of parallel workers:
|
data/lib/grepfruit/cli.rb
CHANGED
|
@@ -17,13 +17,16 @@ module Grepfruit
|
|
|
17
17
|
option :jobs, aliases: ["-j"], type: :integer, desc: "Number of parallel workers (default: all CPU cores, use 1 for sequential)"
|
|
18
18
|
option :json, type: :flag, default: false, desc: "Output results in JSON format"
|
|
19
19
|
option :count, aliases: ["-c"], type: :flag, default: false, desc: "Show only counts, not match details"
|
|
20
|
+
option :relative_paths, aliases: ["-p"], type: :flag, default: false, desc: "Show match paths relative to the search directory"
|
|
20
21
|
|
|
21
22
|
def call(path: ".", **options)
|
|
22
23
|
error_exit("You must specify a regex pattern using the -r or --regex option.") unless options[:regex]
|
|
23
24
|
error_exit("Jobs must be at least 1") if options[:jobs] && options[:jobs].to_i < 1
|
|
25
|
+
error_exit("Exclude must contain only relative paths") if options[:exclude].any? { File.absolute_path?(_1) }
|
|
26
|
+
error_exit("Include must contain only relative paths") if options[:include].any? { File.absolute_path?(_1) }
|
|
24
27
|
|
|
25
28
|
begin
|
|
26
|
-
regex_pattern = Regexp.new(options[:regex])
|
|
29
|
+
regex_pattern = Regexp.new(options[:regex]).freeze
|
|
27
30
|
rescue RegexpError => e
|
|
28
31
|
error_exit("Invalid regex pattern - #{e.message}")
|
|
29
32
|
end
|
|
@@ -37,7 +40,8 @@ module Grepfruit
|
|
|
37
40
|
search_hidden: options[:search_hidden],
|
|
38
41
|
jobs: options[:jobs]&.to_i,
|
|
39
42
|
json: options[:json],
|
|
40
|
-
count: options[:count]
|
|
43
|
+
count: options[:count],
|
|
44
|
+
relative_paths: options[:relative_paths]
|
|
41
45
|
).execute
|
|
42
46
|
end
|
|
43
47
|
|
data/lib/grepfruit/search.rb
CHANGED
|
@@ -4,19 +4,24 @@ require_relative "ractor_compat"
|
|
|
4
4
|
|
|
5
5
|
module Grepfruit
|
|
6
6
|
class Search
|
|
7
|
-
attr_reader :path, :regex, :exclusions, :inclusions, :excluded_paths, :excluded_lines, :truncate, :search_hidden, :jobs, :json, :count
|
|
7
|
+
attr_reader :path, :regex, :exclusions, :inclusions, :excluded_paths, :excluded_lines, :truncate, :search_hidden, :jobs, :json, :count, :relative_paths
|
|
8
8
|
|
|
9
|
-
def initialize(path:, regex:, exclude:, include:, truncate:, search_hidden:, jobs:, json: false, count: false)
|
|
9
|
+
def initialize(path:, regex:, exclude:, include:, truncate:, search_hidden:, jobs:, json: false, count: false, relative_paths: false)
|
|
10
10
|
@path = File.expand_path(path)
|
|
11
11
|
@regex = regex
|
|
12
12
|
@exclusions = exclude
|
|
13
13
|
@inclusions = include
|
|
14
|
-
|
|
14
|
+
line_exclusions, @excluded_paths = exclude.partition { _1.split("/").last.include?(":") }
|
|
15
|
+
@excluded_lines = line_exclusions.map do |exclusion|
|
|
16
|
+
pattern, _, line = exclusion.rpartition(":")
|
|
17
|
+
[pattern, line.to_i]
|
|
18
|
+
end
|
|
15
19
|
@truncate = truncate
|
|
16
20
|
@search_hidden = search_hidden
|
|
17
21
|
@jobs = jobs || Etc.nprocessors
|
|
18
22
|
@json = json
|
|
19
23
|
@count = count
|
|
24
|
+
@relative_paths = relative_paths
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
private
|
|
@@ -93,16 +98,16 @@ module Grepfruit
|
|
|
93
98
|
work = Ractor.receive
|
|
94
99
|
break if work == :quit
|
|
95
100
|
|
|
96
|
-
file_path, pattern, exc_lines, count = work
|
|
101
|
+
file_path, match_path, pattern, exc_lines, count = work
|
|
97
102
|
file_results, has_matches, match_count = [], false, 0
|
|
98
103
|
|
|
99
104
|
begin
|
|
100
105
|
File.foreach(file_path).with_index do |line, line_num|
|
|
101
106
|
next unless line.valid_encoding? && line.match?(pattern)
|
|
102
107
|
|
|
103
|
-
next if exc_lines.
|
|
108
|
+
next if exc_lines.include?(line_num + 1)
|
|
104
109
|
|
|
105
|
-
file_results << [
|
|
110
|
+
file_results << [match_path, line_num + 1, line] unless count
|
|
106
111
|
has_matches = true
|
|
107
112
|
match_count += 1
|
|
108
113
|
end
|
|
@@ -116,10 +121,10 @@ module Grepfruit
|
|
|
116
121
|
end
|
|
117
122
|
|
|
118
123
|
def assign_file_to_worker(worker, port, file_enumerator, active_workers, results)
|
|
119
|
-
file_path = get_next_file(file_enumerator)
|
|
124
|
+
file_path, match_path, file_excluded_lines = get_next_file(file_enumerator)
|
|
120
125
|
return unless file_path
|
|
121
126
|
|
|
122
|
-
RactorCompat.send_work(worker, [file_path, regex,
|
|
127
|
+
RactorCompat.send_work(worker, [file_path, match_path, regex, file_excluded_lines, count])
|
|
123
128
|
active_workers[worker] = port
|
|
124
129
|
results.total_files += 1
|
|
125
130
|
end
|
|
@@ -135,29 +140,37 @@ module Grepfruit
|
|
|
135
140
|
end
|
|
136
141
|
|
|
137
142
|
def create_file_enumerator
|
|
143
|
+
relative_base = File.join(File.directory?(path) ? path : File.dirname(path), "")
|
|
144
|
+
|
|
138
145
|
Enumerator.new do |yielder|
|
|
139
146
|
Find.find(path) do |file_path|
|
|
140
147
|
is_file = File.file?(file_path)
|
|
148
|
+
rel_path = file_path.delete_prefix(relative_base)
|
|
149
|
+
name = File.basename(file_path)
|
|
141
150
|
|
|
142
|
-
Find.prune if file_path != path && excluded_path?(is_file,
|
|
151
|
+
Find.prune if file_path != path && excluded_path?(is_file, rel_path, name)
|
|
143
152
|
|
|
144
153
|
next unless is_file && File.readable?(file_path)
|
|
145
154
|
|
|
146
|
-
|
|
155
|
+
file_excluded_lines = excluded_lines.filter_map { |pattern, line| line if matches_pattern?(pattern, rel_path, name) }
|
|
156
|
+
|
|
157
|
+
yielder << [file_path, relative_paths ? rel_path : file_path, file_excluded_lines]
|
|
147
158
|
end
|
|
148
159
|
end
|
|
149
160
|
end
|
|
150
161
|
|
|
151
|
-
def excluded_path?(is_file,
|
|
152
|
-
(is_file && inclusions.any? && !
|
|
153
|
-
|
|
154
|
-
(!search_hidden &&
|
|
162
|
+
def excluded_path?(is_file, rel_path, name)
|
|
163
|
+
(is_file && inclusions.any? && !matches_any?(inclusions, rel_path, name)) ||
|
|
164
|
+
matches_any?(excluded_paths, rel_path, name) ||
|
|
165
|
+
(!search_hidden && name.start_with?("."))
|
|
155
166
|
end
|
|
156
167
|
|
|
157
|
-
def
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
168
|
+
def matches_any?(patterns, rel_path, name)
|
|
169
|
+
patterns.any? { matches_pattern?(_1, rel_path, name) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def matches_pattern?(pattern, rel_path, name)
|
|
173
|
+
File.fnmatch?(pattern, rel_path, File::FNM_PATHNAME | File::FNM_DOTMATCH) || File.fnmatch?(pattern, name, File::FNM_DOTMATCH)
|
|
161
174
|
end
|
|
162
175
|
|
|
163
176
|
def processed_line(line)
|
data/lib/grepfruit/version.rb
CHANGED
data/lib/grepfruit.rb
CHANGED
|
@@ -6,14 +6,17 @@ require_relative "grepfruit/cli"
|
|
|
6
6
|
require_relative "grepfruit/cli_search"
|
|
7
7
|
|
|
8
8
|
module Grepfruit
|
|
9
|
-
def self.search(regex:, path: ".", exclude: [], include: [], truncate: nil, search_hidden: false, jobs: nil, count: false)
|
|
9
|
+
def self.search(regex:, path: ".", exclude: [], include: [], truncate: nil, search_hidden: false, jobs: nil, count: false, relative_paths: false)
|
|
10
10
|
raise ArgumentError, "regex is required" unless regex.is_a?(Regexp)
|
|
11
11
|
raise ArgumentError, "path must be a string" unless path.is_a?(String)
|
|
12
12
|
raise ArgumentError, "exclude must be an array" unless exclude.is_a?(Array)
|
|
13
13
|
raise ArgumentError, "include must be an array" unless include.is_a?(Array)
|
|
14
|
+
raise ArgumentError, "exclude must contain only relative paths" if exclude.any? { File.absolute_path?(_1) }
|
|
15
|
+
raise ArgumentError, "include must contain only relative paths" if include.any? { File.absolute_path?(_1) }
|
|
14
16
|
raise ArgumentError, "truncate must be a positive integer" if truncate && (!truncate.is_a?(Integer) || truncate <= 0)
|
|
15
17
|
raise ArgumentError, "search_hidden must be a boolean" unless [true, false].include?(search_hidden)
|
|
16
18
|
raise ArgumentError, "count must be a boolean" unless [true, false].include?(count)
|
|
19
|
+
raise ArgumentError, "relative_paths must be a boolean" unless [true, false].include?(relative_paths)
|
|
17
20
|
raise ArgumentError, "jobs must be at least 1" if jobs && (!jobs.is_a?(Integer) || jobs < 1)
|
|
18
21
|
|
|
19
22
|
ProgrammaticSearch.new(
|
|
@@ -25,7 +28,8 @@ module Grepfruit
|
|
|
25
28
|
search_hidden: search_hidden,
|
|
26
29
|
jobs: jobs,
|
|
27
30
|
json: false,
|
|
28
|
-
count: count
|
|
31
|
+
count: count,
|
|
32
|
+
relative_paths: relative_paths
|
|
29
33
|
).execute
|
|
30
34
|
end
|
|
31
35
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: grepfruit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.0
|
|
4
|
+
version: 4.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- enjaku4
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: dry-cli
|
|
@@ -74,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
75
|
version: '0'
|
|
76
76
|
requirements: []
|
|
77
|
-
rubygems_version: 4.0.
|
|
77
|
+
rubygems_version: 4.0.16
|
|
78
78
|
specification_version: 4
|
|
79
79
|
summary: Tool for searching regex patterns in files
|
|
80
80
|
test_files: []
|