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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1e692e9c73cb5e153c74e9f9dad30665f77eae2c1d12335b9cda68eb19cc27e
4
- data.tar.gz: be22dc571889bcd9e59378e8896949dfd792d966e2bb01d65ad3c4fec70329a6
3
+ metadata.gz: 9c0935c8c65aa93564bbac5b7e4c109ac46df6f359a61da5f20f0fb5d1c50941
4
+ data.tar.gz: 9a5b235ee6c08a82c9dd1e416b3fce19d192224182a59bcf6ce3a73bf45a1d50
5
5
  SHA512:
6
- metadata.gz: fb52ee14db47e883bedbfe53ab17aea9b032b87f6749910a6af441cda3fe9b413ba9e11d5baa8d6ff8b3f8271b76da61d45f4ea9db6aa67a4714124bea9c96dd
7
- data.tar.gz: 64052138a5df15c7e3ad5f9f6e2227d60016e9cfa6539e2a7b09c5166af8924d07eb03943af522fb2daa63baafb3fe5120aad0f315161c07ba7f93e0b02e63d1
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 file patterns to include (only these files will be searched) |
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
 
@@ -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
- @excluded_lines, @excluded_paths = exclude.partition { _1.split("/").last.include?(":") }
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.any? { "#{file_path}:#{line_num + 1}".end_with?(_1) }
108
+ next if exc_lines.include?(line_num + 1)
104
109
 
105
- file_results << [file_path, line_num + 1, line] unless count
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, excluded_lines, count])
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, file_path, file_path.delete_prefix("#{path}/"))
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
- yielder << file_path
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, file_path, rel_path)
152
- (is_file && inclusions.any? && !matches_pattern?(inclusions, rel_path)) ||
153
- matches_pattern?(excluded_paths, rel_path) ||
154
- (!search_hidden && File.basename(file_path).start_with?("."))
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 matches_pattern?(pattern_list, path)
158
- pattern_list.any? do |pattern|
159
- File.fnmatch?(pattern, path, File::FNM_PATHNAME) || File.fnmatch?(pattern, File.basename(path))
160
- end
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)
@@ -1,3 +1,3 @@
1
1
  module Grepfruit
2
- VERSION = "4.0.2".freeze
2
+ VERSION = "4.1.0".freeze
3
3
  end
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.2
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: 2026-07-19 00:00:00.000000000 Z
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.3
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: []