grepfruit 3.1.0 → 3.1.1

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: 50c38c621aa4a7c34b262cd61c45ced836cca10158b33566d709fd76bdb18f9f
4
- data.tar.gz: f0a93887ab397d50a0a22e0600bb06925013632beab1040adf768c831bb6e5ac
3
+ metadata.gz: 51c0381a8449c749e2017cf4feac9cf35fb2c58e38f19a6a00fe994997d38b17
4
+ data.tar.gz: 61bca9861ce7d63331285d7b089e8521eac58f19465c293a50b07c5e4eb3cf01
5
5
  SHA512:
6
- metadata.gz: 50838340c7e1b6168675ee64bf0f0608beaa708237d12b40efc0fb8099afbde20ae5f18860747984e37e60cf090d4ff4b5e96667cd728dd51299bc5b44ebcdf2
7
- data.tar.gz: 66e4d2c8cda063641243eaad779c43bd5fba37ecaad1bda19c81de996986688ff2eb825d86260da1f6a2bda0b522de73c5e5af304b3fe166f2d0438d15fa270f
6
+ metadata.gz: e16bd1bc1c78d56b9253e2be32788f7dc5a03584de099e6ff6e7b78f581ea7ba9af018dc6c7b17dda06a7f55ab61488181237320b16fc3cd5768422a16d0cf8f
7
+ data.tar.gz: 948e8fadc208c058cfa943eeabb79a8b17d499fb78277e757b0655357dd028d8f312d008c52098c14bdcc2d1e94279fcefc1a821467e54020a26b209a72000d1
data/CHANGELOG.md CHANGED
@@ -1,7 +1,12 @@
1
+ ## v3.1.1
2
+
3
+ - Fixed JSON timestamp to reflect result generation time
4
+ - Minor optimization
5
+
1
6
  ## v3.1.0
2
7
 
3
8
  - Added --include option to specify files to include in the search
4
- - Both --exclude and --include options can now accept multiple patterns
9
+ - Both --exclude and --include options can now accept wildcard patterns
5
10
 
6
11
  ## v3.0.0
7
12
 
data/lib/grepfruit/cli.rb CHANGED
@@ -26,9 +26,9 @@ module Grepfruit
26
26
  exclude: options[:exclude] || [],
27
27
  include: options[:include] || [],
28
28
  truncate: options[:truncate]&.to_i,
29
- search_hidden: !!options[:search_hidden],
29
+ search_hidden: options[:search_hidden],
30
30
  jobs: options[:jobs]&.to_i,
31
- json_output: !!options[:json]
31
+ json_output: options[:json]
32
32
  ).run
33
33
  end
34
34
 
@@ -36,9 +36,7 @@ module Grepfruit
36
36
 
37
37
  def validate_options!(options)
38
38
  error_exit("You must specify a regex pattern using the -r or --regex option.") unless options[:regex]
39
-
40
- jobs = options[:jobs]&.to_i
41
- error_exit("Number of jobs must be at least 1") if jobs && jobs < 1
39
+ error_exit("Number of jobs must be at least 1") if (jobs = options[:jobs]&.to_i) && jobs < 1
42
40
  end
43
41
 
44
42
  def create_regex(pattern)
@@ -4,9 +4,10 @@ module Grepfruit
4
4
 
5
5
  private
6
6
 
7
- def green(text) = "#{COLORS[:green]}#{text}#{COLORS[:reset]}"
8
- def red(text) = "#{COLORS[:red]}#{text}#{COLORS[:reset]}"
9
- def cyan(text) = "#{COLORS[:cyan]}#{text}#{COLORS[:reset]}"
7
+ def colorize(text, color) = "#{COLORS[color]}#{text}#{COLORS[:reset]}"
8
+ def green(text) = colorize(text, :green)
9
+ def red(text) = colorize(text, :red)
10
+ def cyan(text) = colorize(text, :cyan)
10
11
 
11
12
  def number_of_files(num) = "#{num} file#{'s' if num != 1}"
12
13
  def number_of_matches(num) = "#{num} match#{'es' if num != 1}"
@@ -16,8 +17,8 @@ module Grepfruit
16
17
  end
17
18
 
18
19
  def processed_line(line)
19
- stripped_line = line.strip
20
- truncate && stripped_line.length > truncate ? "#{stripped_line[0...truncate]}..." : stripped_line
20
+ stripped = line.strip
21
+ truncate && stripped.length > truncate ? "#{stripped[0...truncate]}..." : stripped
21
22
  end
22
23
 
23
24
  def display_results(lines, files, files_with_matches)
@@ -41,7 +42,7 @@ module Grepfruit
41
42
  directory: dir,
42
43
  exclusions: (excluded_paths + excluded_lines).map { |path_parts| path_parts.join("/") },
43
44
  inclusions: included_paths.map { |path_parts| path_parts.join("/") },
44
- timestamp: @start_time.strftime("%Y-%m-%dT%H:%M:%S%z")
45
+ timestamp: Time.now.strftime("%Y-%m-%dT%H:%M:%S%z")
45
46
  }
46
47
 
47
48
  summary = {
@@ -1,8 +1,6 @@
1
1
  require "find"
2
2
  require "etc"
3
3
 
4
- require_relative "decorator"
5
-
6
4
  Warning[:experimental] = false
7
5
 
8
6
  module Grepfruit
@@ -20,49 +18,77 @@ module Grepfruit
20
18
  @search_hidden = search_hidden
21
19
  @jobs = jobs || Etc.nprocessors
22
20
  @json_output = json_output
23
- @start_time = Time.now
24
21
  end
25
22
 
26
23
  def run
27
24
  puts "Searching for #{regex.inspect} in #{dir.inspect}...\n\n" unless json_output
28
25
 
29
- all_lines, total_files_with_matches, total_files = [], 0, 0
30
- raw_matches = []
31
- workers = create_workers
26
+ display_final_results(execute_search)
27
+ end
28
+
29
+ private
30
+
31
+ def execute_search
32
+ results = SearchResults.new
33
+ workers = Array.new(jobs) { create_persistent_worker }
32
34
  file_enumerator = create_file_enumerator
33
35
  active_workers = {}
34
36
 
35
37
  workers.each do |worker|
36
- assign_file_to_worker(worker, file_enumerator, active_workers) && total_files += 1
38
+ assign_file_to_worker(worker, file_enumerator, active_workers, results)
37
39
  end
38
40
 
39
41
  while active_workers.any?
40
- ready_worker, (file_results, has_matches) = Ractor.select(*active_workers.keys)
42
+ ready_worker, worker_result = Ractor.select(*active_workers.keys)
41
43
  active_workers.delete(ready_worker)
42
44
 
43
- total_files_with_matches += 1 if process_worker_result(file_results, has_matches, all_lines, raw_matches)
44
-
45
- assign_file_to_worker(ready_worker, file_enumerator, active_workers) && total_files += 1
45
+ results.increment_files_with_matches if process_worker_result(worker_result, results)
46
+ assign_file_to_worker(ready_worker, file_enumerator, active_workers, results)
46
47
  end
47
48
 
48
- workers.each(&:close_outgoing)
49
+ shutdown_workers(workers)
50
+ results
51
+ end
49
52
 
53
+ def display_final_results(results)
50
54
  if json_output
51
- display_json_results(raw_matches, total_files, total_files_with_matches)
55
+ display_json_results(results.raw_matches, results.total_files, results.total_files_with_matches)
52
56
  else
53
- display_results(all_lines, total_files, total_files_with_matches)
57
+ display_results(results.all_lines, results.total_files, results.total_files_with_matches)
54
58
  end
55
59
  end
56
60
 
57
- private
61
+ def create_persistent_worker
62
+ Ractor.new do
63
+ loop do
64
+ work = Ractor.receive
65
+ break if work == :quit
66
+
67
+ file_path, pattern, exc_lines, base_dir = work
68
+ file_results, has_matches = [], false
69
+
70
+ File.foreach(file_path).with_index do |line, line_num|
71
+ next unless line.valid_encoding? && line.match?(pattern)
72
+
73
+ relative_path = file_path.delete_prefix("#{base_dir}/")
74
+ next if exc_lines.any? { "#{relative_path}:#{line_num + 1}".end_with?(_1.join("/")) }
75
+
76
+ file_results << [relative_path, line_num + 1, line]
77
+ has_matches = true
78
+ end
79
+
80
+ Ractor.yield([file_results, has_matches])
81
+ end
82
+ end
83
+ end
58
84
 
59
- def assign_file_to_worker(worker, file_enumerator, active_workers)
85
+ def assign_file_to_worker(worker, file_enumerator, active_workers, results)
60
86
  file_path = get_next_file(file_enumerator)
61
- return false unless file_path
87
+ return unless file_path
62
88
 
63
89
  worker.send([file_path, regex, excluded_lines, dir])
64
90
  active_workers[worker] = file_path
65
- true
91
+ results.total_files += 1
66
92
  end
67
93
 
68
94
  def get_next_file(enumerator)
@@ -71,27 +97,9 @@ module Grepfruit
71
97
  nil
72
98
  end
73
99
 
74
- def create_workers
75
- Array.new(jobs) do
76
- Ractor.new do
77
- loop do
78
- file_path, pattern, exc_lines, base_dir = Ractor.receive
79
- results, has_matches = [], false
80
-
81
- File.foreach(file_path).with_index do |line, line_num|
82
- next unless line.valid_encoding? && line.match?(pattern)
83
-
84
- relative_path = file_path.delete_prefix("#{base_dir}/")
85
- next if exc_lines.any? { "#{relative_path}:#{line_num + 1}".end_with?(_1.join("/")) }
86
-
87
- results << [relative_path, line_num + 1, line]
88
- has_matches = true
89
- end
90
-
91
- Ractor.yield([results, has_matches])
92
- end
93
- end
94
- end
100
+ def shutdown_workers(workers)
101
+ workers.each { |worker| worker.send(:quit) }
102
+ workers.each(&:close_outgoing)
95
103
  end
96
104
 
97
105
  def create_file_enumerator
@@ -109,36 +117,32 @@ module Grepfruit
109
117
  end
110
118
  end
111
119
 
112
- def process_worker_result(file_results, has_matches, all_lines, raw_matches)
120
+ def process_worker_result(worker_result, results)
121
+ file_results, has_matches = worker_result
122
+
113
123
  if has_matches
114
- raw_matches.concat(file_results) if json_output
124
+ results.add_raw_matches(file_results) if json_output
115
125
 
116
126
  unless json_output
117
127
  colored_lines = file_results.map do |relative_path, line_num, line_content|
118
128
  "#{cyan("#{relative_path}:#{line_num}")}: #{processed_line(line_content)}"
119
129
  end
120
- all_lines.concat(colored_lines)
130
+ results.add_lines(colored_lines)
121
131
  print red("M")
122
132
  end
123
- true
124
133
  else
125
134
  print green(".") unless json_output
126
- false
127
135
  end
128
- end
129
-
130
- def excluded_path?(path)
131
- rel_path = relative_path(path)
132
136
 
133
- not_included_path?(path, rel_path) || matches_pattern?(excluded_paths, rel_path) || excluded_hidden?(path)
137
+ has_matches
134
138
  end
135
139
 
136
- def not_included_path?(path, rel_path)
137
- File.file?(path) && included_paths.any? && !matches_pattern?(included_paths, rel_path)
138
- end
140
+ def excluded_path?(path)
141
+ rel_path = path.delete_prefix("#{dir}/")
139
142
 
140
- def excluded_hidden?(path)
141
- !search_hidden && File.basename(path).start_with?(".")
143
+ (File.file?(path) && included_paths.any? && !matches_pattern?(included_paths, rel_path)) ||
144
+ matches_pattern?(excluded_paths, rel_path) ||
145
+ (!search_hidden && File.basename(path).start_with?("."))
142
146
  end
143
147
 
144
148
  def matches_pattern?(pattern_list, path)
@@ -147,9 +151,5 @@ module Grepfruit
147
151
  File.fnmatch?(pattern, path, File::FNM_PATHNAME) || File.fnmatch?(pattern, File.basename(path))
148
152
  end
149
153
  end
150
-
151
- def relative_path(path)
152
- path.delete_prefix("#{dir}/")
153
- end
154
154
  end
155
155
  end
@@ -0,0 +1,25 @@
1
+ module Grepfruit
2
+ class SearchResults
3
+ attr_reader :all_lines, :raw_matches, :total_files_with_matches
4
+ attr_accessor :total_files
5
+
6
+ def initialize
7
+ @all_lines = []
8
+ @raw_matches = []
9
+ @total_files = 0
10
+ @total_files_with_matches = 0
11
+ end
12
+
13
+ def increment_files_with_matches
14
+ @total_files_with_matches += 1
15
+ end
16
+
17
+ def add_lines(lines)
18
+ @all_lines.concat(lines)
19
+ end
20
+
21
+ def add_raw_matches(matches)
22
+ @raw_matches.concat(matches)
23
+ end
24
+ end
25
+ end
@@ -1,3 +1,3 @@
1
1
  module Grepfruit
2
- VERSION = "3.1.0".freeze
2
+ VERSION = "3.1.1".freeze
3
3
  end
data/lib/grepfruit.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require_relative "grepfruit/version"
2
+ require_relative "grepfruit/decorator"
3
+ require_relative "grepfruit/search_results"
2
4
  require_relative "grepfruit/search"
3
5
  require_relative "grepfruit/cli"
4
6
 
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: 3.1.0
4
+ version: 3.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - enjaku4
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-07-07 00:00:00.000000000 Z
10
+ date: 2025-07-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dry-cli
@@ -37,6 +37,7 @@ files:
37
37
  - lib/grepfruit/cli.rb
38
38
  - lib/grepfruit/decorator.rb
39
39
  - lib/grepfruit/search.rb
40
+ - lib/grepfruit/search_results.rb
40
41
  - lib/grepfruit/version.rb
41
42
  homepage: https://github.com/brownboxdev/grepfruit
42
43
  licenses: