grepfruit 4.0.1 → 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 +14 -0
- data/README.md +16 -3
- data/exe/grepfruit +2 -0
- data/lib/grepfruit/cli.rb +6 -2
- data/lib/grepfruit/cli_search.rb +4 -1
- data/lib/grepfruit/search.rb +39 -24
- 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,17 @@
|
|
|
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
|
+
|
|
9
|
+
## v4.0.2
|
|
10
|
+
|
|
11
|
+
- Fixed searches silently checking no files when the search path itself was hidden or matched an exclusion
|
|
12
|
+
- Fixed a crash backtrace being printed after the error message when the search path does not exist
|
|
13
|
+
- Fixed the whole search crashing when a file is deleted or becomes unreadable mid-search
|
|
14
|
+
|
|
1
15
|
## v4.0.1
|
|
2
16
|
|
|
3
17
|
- Restored accidentally removed CLI progress indicator
|
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/exe/grepfruit
CHANGED
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/cli_search.rb
CHANGED
|
@@ -5,7 +5,10 @@ module Grepfruit
|
|
|
5
5
|
include Grepfruit::CliDecorator
|
|
6
6
|
|
|
7
7
|
def execute
|
|
8
|
-
|
|
8
|
+
unless File.exist?(path)
|
|
9
|
+
puts "Error: Path '#{path}' does not exist."
|
|
10
|
+
exit 1
|
|
11
|
+
end
|
|
9
12
|
|
|
10
13
|
unless json
|
|
11
14
|
puts "Searching for #{regex.inspect} in #{path.inspect}..."
|
data/lib/grepfruit/search.rb
CHANGED
|
@@ -2,23 +2,26 @@ require "find"
|
|
|
2
2
|
require "etc"
|
|
3
3
|
require_relative "ractor_compat"
|
|
4
4
|
|
|
5
|
-
Warning[:experimental] = false
|
|
6
|
-
|
|
7
5
|
module Grepfruit
|
|
8
6
|
class Search
|
|
9
|
-
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
|
|
10
8
|
|
|
11
|
-
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)
|
|
12
10
|
@path = File.expand_path(path)
|
|
13
11
|
@regex = regex
|
|
14
12
|
@exclusions = exclude
|
|
15
13
|
@inclusions = include
|
|
16
|
-
|
|
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
|
|
17
19
|
@truncate = truncate
|
|
18
20
|
@search_hidden = search_hidden
|
|
19
21
|
@jobs = jobs || Etc.nprocessors
|
|
20
22
|
@json = json
|
|
21
23
|
@count = count
|
|
24
|
+
@relative_paths = relative_paths
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
private
|
|
@@ -95,17 +98,21 @@ module Grepfruit
|
|
|
95
98
|
work = Ractor.receive
|
|
96
99
|
break if work == :quit
|
|
97
100
|
|
|
98
|
-
file_path, pattern, exc_lines, count = work
|
|
101
|
+
file_path, match_path, pattern, exc_lines, count = work
|
|
99
102
|
file_results, has_matches, match_count = [], false, 0
|
|
100
103
|
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
begin
|
|
105
|
+
File.foreach(file_path).with_index do |line, line_num|
|
|
106
|
+
next unless line.valid_encoding? && line.match?(pattern)
|
|
103
107
|
|
|
104
|
-
|
|
108
|
+
next if exc_lines.include?(line_num + 1)
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
file_results << [match_path, line_num + 1, line] unless count
|
|
111
|
+
has_matches = true
|
|
112
|
+
match_count += 1
|
|
113
|
+
end
|
|
114
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::EISDIR
|
|
115
|
+
file_results, has_matches, match_count = [], false, 0
|
|
109
116
|
end
|
|
110
117
|
|
|
111
118
|
RactorCompat.yield_result(port, [file_results, has_matches, match_count])
|
|
@@ -114,10 +121,10 @@ module Grepfruit
|
|
|
114
121
|
end
|
|
115
122
|
|
|
116
123
|
def assign_file_to_worker(worker, port, file_enumerator, active_workers, results)
|
|
117
|
-
file_path = get_next_file(file_enumerator)
|
|
124
|
+
file_path, match_path, file_excluded_lines = get_next_file(file_enumerator)
|
|
118
125
|
return unless file_path
|
|
119
126
|
|
|
120
|
-
RactorCompat.send_work(worker, [file_path, regex,
|
|
127
|
+
RactorCompat.send_work(worker, [file_path, match_path, regex, file_excluded_lines, count])
|
|
121
128
|
active_workers[worker] = port
|
|
122
129
|
results.total_files += 1
|
|
123
130
|
end
|
|
@@ -133,29 +140,37 @@ module Grepfruit
|
|
|
133
140
|
end
|
|
134
141
|
|
|
135
142
|
def create_file_enumerator
|
|
143
|
+
relative_base = File.join(File.directory?(path) ? path : File.dirname(path), "")
|
|
144
|
+
|
|
136
145
|
Enumerator.new do |yielder|
|
|
137
146
|
Find.find(path) do |file_path|
|
|
138
147
|
is_file = File.file?(file_path)
|
|
148
|
+
rel_path = file_path.delete_prefix(relative_base)
|
|
149
|
+
name = File.basename(file_path)
|
|
139
150
|
|
|
140
|
-
Find.prune if excluded_path?(is_file,
|
|
151
|
+
Find.prune if file_path != path && excluded_path?(is_file, rel_path, name)
|
|
141
152
|
|
|
142
153
|
next unless is_file && File.readable?(file_path)
|
|
143
154
|
|
|
144
|
-
|
|
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]
|
|
145
158
|
end
|
|
146
159
|
end
|
|
147
160
|
end
|
|
148
161
|
|
|
149
|
-
def excluded_path?(is_file,
|
|
150
|
-
(is_file && inclusions.any? && !
|
|
151
|
-
|
|
152
|
-
(!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?("."))
|
|
153
166
|
end
|
|
154
167
|
|
|
155
|
-
def
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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)
|
|
159
174
|
end
|
|
160
175
|
|
|
161
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: []
|