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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d27a11eff0190c85e32b6081219b6db2b3f79e92170f85443e7e422df011bd5e
4
- data.tar.gz: 7d1ab993aa73df36b9cfd1653721757d0e6f0473413562900231da351f9dbc89
3
+ metadata.gz: 9c0935c8c65aa93564bbac5b7e4c109ac46df6f359a61da5f20f0fb5d1c50941
4
+ data.tar.gz: 9a5b235ee6c08a82c9dd1e416b3fce19d192224182a59bcf6ce3a73bf45a1d50
5
5
  SHA512:
6
- metadata.gz: a8f66005b0f1ddc8a75747fa4c24b16e6401332fad5061b57220f766eba2a64691a46f9686c349dce4f7b7b42b308e5e7141da58177b338a5104386573f5322e
7
- data.tar.gz: 8ed7dc08c649a3becacc813a9e6f7bfbb0722c14a5b63cf3749e9a00cf2333fa75b8990ac2656d82c47dea5422bcb06e48ebc00eb413aca362fdb6d6e91b754a
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 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/exe/grepfruit CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  RubyVM::YJIT.enable if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable)
4
4
 
5
+ Warning[:experimental] = false
6
+
5
7
  $LOAD_PATH.unshift("#{__dir__}/../lib")
6
8
 
7
9
  require "grepfruit"
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
 
@@ -5,7 +5,10 @@ module Grepfruit
5
5
  include Grepfruit::CliDecorator
6
6
 
7
7
  def execute
8
- puts "Error: Path '#{path}' does not exist." and exit 1 unless File.exist?(path)
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}..."
@@ -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
- @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
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
- File.foreach(file_path).with_index do |line, line_num|
102
- next unless line.valid_encoding? && line.match?(pattern)
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
- next if exc_lines.any? { "#{file_path}:#{line_num + 1}".end_with?(_1) }
108
+ next if exc_lines.include?(line_num + 1)
105
109
 
106
- file_results << [file_path, line_num + 1, line] unless count
107
- has_matches = true
108
- match_count += 1
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, excluded_lines, count])
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, file_path, file_path.delete_prefix("#{path}/"))
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
- 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]
145
158
  end
146
159
  end
147
160
  end
148
161
 
149
- def excluded_path?(is_file, file_path, rel_path)
150
- (is_file && inclusions.any? && !matches_pattern?(inclusions, rel_path)) ||
151
- matches_pattern?(excluded_paths, rel_path) ||
152
- (!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?("."))
153
166
  end
154
167
 
155
- def matches_pattern?(pattern_list, path)
156
- pattern_list.any? do |pattern|
157
- File.fnmatch?(pattern, path, File::FNM_PATHNAME) || File.fnmatch?(pattern, File.basename(path))
158
- 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)
159
174
  end
160
175
 
161
176
  def processed_line(line)
@@ -1,3 +1,3 @@
1
1
  module Grepfruit
2
- VERSION = "4.0.1".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.1
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-05 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.8
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: []