grepfruit 2.0.3 → 3.0.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: 523ee40a32ea4bc5c0d6b91ca60d147985a8d79b84396b17c3fd4b49f629f121
4
- data.tar.gz: 84ee19de06fbb489a6dc9ac32365cc92a00b23fe11337b7d2c236f12ffa509c0
3
+ metadata.gz: 21bbe94a4fe8b462011334fbba7773cf570da96f5497d7b9ad7e125351b92b13
4
+ data.tar.gz: 2ad07b148d00350ed821d973c29675d9b91f3a99dc9a2f3e64f3acbce7018932
5
5
  SHA512:
6
- metadata.gz: 76821e2d3b8967d931491d66c7d25cbea24b8998d9c4d2603c13a8597c04484d6eb0cb046e5ad9470d9c546ca70ec729b3929447db66dbf7ccceba3767393575
7
- data.tar.gz: 766456777c31d8c58a4e2ee6d4f6891ac8c280130c9c671894dd56d6e7e1c64e65ad512f0a73497ac6f94ab19653a7267d394fea152aca9a451cbfa874754e83
6
+ metadata.gz: d8b5a7b272696fc8dd60993fcb10fc3bdf9bf527d51ba0ef36a7a86adb1bb152f834c1a0b9cefadae6d630acd87ce635028b07ae12cdb162caf68bc15a10b1eb
7
+ data.tar.gz: 0f497ea749fc6cb5740c1e8c16b7672970c75333e46cd1e95418aa0d701d8a3db7b08ffb1775b51c32b66297ce202458768276a8b3065e2b3e25fd4d97fe313d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## v3.0.0
2
+
3
+ - Dropped support for Ruby 3.1
4
+ - Optimized search algorithm for better performance
5
+ - Changed the interface: now use `grepfruit search` instead of just `grepfruit` to perform searches
6
+ - Added JSON output format for search results
7
+ - Added parallel processing and --jobs option to control worker count
8
+
9
+ ## v2.0.4
10
+
11
+ - Fixed path resolution bug where searching in relative directories such as `.`, `./`, or `..` did not work correctly
12
+
1
13
  ## v2.0.3
2
14
 
3
15
  - Updated gemspec metadata to include the correct homepage URL
data/README.md CHANGED
@@ -1,97 +1,187 @@
1
- # Grepfruit
1
+ # Grepfruit: File Pattern Search Tool for Ruby
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/grepfruit.svg)](http://badge.fury.io/rb/grepfruit)
4
4
  [![Github Actions badge](https://github.com/brownboxdev/grepfruit/actions/workflows/ci.yml/badge.svg)](https://github.com/brownboxdev/grepfruit/actions/workflows/ci.yml)
5
5
 
6
- Grepfruit is a Ruby gem for searching files within a directory for a specified regular expression pattern, with options to exclude certain files or directories from the search and colorized output for better readability.
6
+ Grepfruit is a Ruby gem for searching files within a directory for specified regular expression patterns, with exclusion options and JSON-formatted or colorized output for enhanced readability. Originally designed for CI/CD pipelines to search for `TODO` comments in Ruby on Rails applications, Grepfruit provides more user-friendly output than the standard `grep` command while maintaining the flexibility for diverse search scenarios.
7
7
 
8
- <img width="431" alt="Screenshot 2024-12-26 at 18 01 39" src="https://github.com/user-attachments/assets/e3fdb4f7-c4d9-4c8d-9a5a-228f2be55d52" />
8
+ **Key Features:**
9
9
 
10
- Grepfruit was originally created to be used in CI/CD pipelines to search for `TODO` comments in Ruby on Rails applications and provide more user-friendly output than the standard `grep` command, but it is flexible enough to be used for other similar purposes as well.
10
+ - Parallel search using Ractors
11
+ - JSON output format for programmatic integration
12
+ - Colorized output for improved readability
13
+ - CI/CD pipeline friendly exit codes
14
+
15
+ ## Table of Contents
16
+
17
+ **Gem Usage:**
18
+ - [Installation](#installation)
19
+ - [Basic Usage](#basic-usage)
20
+ - [Command Line Options](#command-line-options)
21
+ - [Usage Examples](#usage-examples)
22
+ - [Exit Status](#exit-status)
23
+
24
+ **Community Resources:**
25
+ - [Contributing](#contributing)
26
+ - [License](#license)
27
+ - [Code of Conduct](#code-of-conduct)
11
28
 
12
29
  ## Installation
13
30
 
14
- Add this line to your application's Gemfile:
31
+ Install the gem:
15
32
 
16
- ```ruby
17
- gem "grepfruit"
33
+ ```bash
34
+ gem install grepfruit
18
35
  ```
19
36
 
20
- And then execute:
37
+ ## Basic Usage
38
+
39
+ Search for regex patterns within files in a specified directory:
21
40
 
22
- ```shell
23
- bundle install
41
+ ```bash
42
+ grepfruit search [options] [PATH]
24
43
  ```
25
44
 
26
- Or install it yourself as:
45
+ Or using shorthand `s` command:
27
46
 
28
- ```shell
29
- gem install grepfruit
47
+ ```bash
48
+ grepfruit s [options] [PATH]
30
49
  ```
31
50
 
32
- ## Usage
51
+ If no PATH is specified, Grepfruit searches the current directory.
52
+
53
+ ## Command Line Options
54
+
55
+ | Option | Description |
56
+ |--------|-------------|
57
+ | `-r, --regex REGEX` | Regex pattern to search for (required) |
58
+ | `-e, --exclude x,y,z` | Comma-separated list of files, directories, or lines to exclude |
59
+ | `-t, --truncate N` | Truncate search result output to N characters |
60
+ | `-j, --jobs N` | Number of parallel workers (default: number of CPU cores) |
61
+ | `--search-hidden` | Include hidden files and directories in search |
62
+ | `--json` | Output results in JSON format |
63
+
64
+ ## Usage Examples
65
+
66
+ ### Basic Pattern Search
33
67
 
34
- You can use Grepfruit from the command line to search for a regex pattern within files in a specified directory.
68
+ Search for `TODO` comments in the current directory:
35
69
 
36
- ```shell
37
- grepfruit [options] PATH
70
+ ```bash
71
+ grepfruit search -r 'TODO'
38
72
  ```
39
73
 
40
- If no matches are found, Grepfruit returns exit status 0; otherwise, it returns exit status 1.
74
+ ### Excluding Directories
41
75
 
42
- ### Options
76
+ Search for `TODO` patterns while excluding common build and dependency directories:
43
77
 
44
- - `-r, --regex REGEX`: Regex pattern to search for (required).
45
- - `-e, --exclude x,y,z`: Comma-separated list of files, directories, or lines to exclude from the search.
46
- - `-t, --truncate N`: Truncate the output of the search results to N characters.
47
- - `--search-hidden`: Search hidden files and directories.
78
+ ```bash
79
+ grepfruit search -r 'TODO' -e 'log,tmp,vendor,node_modules,assets'
80
+ ```
48
81
 
49
- ### Examples
82
+ ### Multiple Pattern Search Excluding Both Directories and Files
50
83
 
51
- Search for the pattern `/TODO/` in the current directory, excluding `log`, `tmp`, `vendor`, `node_modules`, and `assets` directories:
84
+ Search for both `FIXME` and `TODO` comments in a specific directory:
52
85
 
53
- ```shell
54
- grepfruit -r 'TODO' -e 'log,tmp,vendor,node_modules,assets'
86
+ ```bash
87
+ grepfruit search -r 'FIXME|TODO' -e 'bin,tmp/log,Gemfile.lock' dev/my_app
55
88
  ```
56
89
 
57
- Search for the pattern `/FIXME|TODO/` in `dev/grepfruit` directory, excluding `bin`, `tmp/log`, and `Gemfile.lock` files and directories:
90
+ ### Line-Specific Exclusion
91
+
92
+ Exclude specific lines from search results:
58
93
 
59
- ```shell
60
- grepfruit -r 'FIXME|TODO' -e 'bin,tmp/log,Gemfile.lock' dev/grepfruit
94
+ ```bash
95
+ grepfruit search -r 'FIXME|TODO' -e 'README.md:18'
61
96
  ```
62
97
 
63
- Search for the pattern `/FIXME|TODO/` in the current directory, excluding line 18 of `README.md`:
98
+ ### Output Truncation
99
+
100
+ Limit output length for cleaner results:
64
101
 
65
- ```shell
66
- grepfruit -r 'FIXME|TODO' -e 'README.md:18'
102
+ ```bash
103
+ grepfruit search -r 'FIXME|TODO' -t 50
67
104
  ```
68
105
 
69
- Search for the pattern `/FIXME|TODO/` in the current directory, truncating the output of the search results to 50 characters:
106
+ ### Including Hidden Files
70
107
 
71
- ```shell
72
- grepfruit -r 'FIXME|TODO' -t 50
108
+ Search hidden files and directories:
109
+
110
+ ```bash
111
+ grepfruit search -r 'FIXME|TODO' --search-hidden
73
112
  ```
74
113
 
75
- Search for the pattern `/FIXME|TODO/` in the current directory, including hidden files and directories:
114
+ ### JSON Output for Automation
115
+
116
+ Get structured JSON output for scripts and CI/CD pipelines:
76
117
 
77
- ```shell
78
- grepfruit -r 'FIXME|TODO' --search-hidden
118
+ ```bash
119
+ grepfruit search -r 'TODO' --json
79
120
  ```
80
121
 
81
- ## Problems?
122
+ This outputs a structured JSON response containing search metadata, summary statistics, and detailed match information:
123
+
124
+ ```json
125
+ {
126
+ "search": {
127
+ "pattern": "/TODO/",
128
+ "directory": "/path/to/search",
129
+ "exclusions": ["node_modules"],
130
+ "timestamp": "2025-01-16T10:30:00+00:00"
131
+ },
132
+ "summary": {
133
+ "files_checked": 42,
134
+ "files_with_matches": 8,
135
+ "total_matches": 23
136
+ },
137
+ "matches": [
138
+ {
139
+ "file": "src/main.js",
140
+ "line": 15,
141
+ "content": "// TODO: Implement error handling"
142
+ },
143
+ // ...
144
+ ]
145
+ }
146
+ ```
147
+
148
+ ### Parallel Processing
82
149
 
83
- Facing a problem or want to suggest an enhancement?
150
+ Control the number of parallel workers:
151
+
152
+ ```bash
153
+ grepfruit search -r 'TODO' -j 8 # Use 8 parallel workers
154
+ grepfruit search -r 'TODO' -j 1 # Sequential processing
155
+ ```
84
156
 
85
- - **Open a Discussion**: If you have a question, experience difficulties using the gem, or have a suggestion for improvements, feel free to use the Discussions section.
157
+ ## Exit Status
86
158
 
87
- Encountered a bug?
159
+ Grepfruit returns meaningful exit codes for CI/CD integration:
88
160
 
89
- - **Create an Issue**: If you've identified a bug, please create an issue. Be sure to provide detailed information about the problem, including the steps to reproduce it.
90
- - **Contribute a Solution**: Found a fix for the issue? Feel free to create a pull request with your changes.
161
+ - **Exit code 0**: No matches found (ideal for quality gates - code is clean)
162
+ - **Exit code 1**: Pattern matches were found (CI should fail - issues detected)
91
163
 
92
164
  ## Contributing
93
165
 
94
- Before creating an issue or a pull request, please read the [contributing guidelines](https://github.com/brownboxdev/grepfruit/blob/master/CONTRIBUTING.md).
166
+ ### Getting Help
167
+ Have a question or need assistance? Open a discussion in our [discussions section](https://github.com/brownboxdev/grepfruit/discussions) for:
168
+ - Usage questions
169
+ - Implementation guidance
170
+ - Feature suggestions
171
+
172
+ ### Reporting Issues
173
+ Found a bug? Please [create an issue](https://github.com/brownboxdev/grepfruit/issues) with:
174
+ - A clear description of the problem
175
+ - Steps to reproduce the issue
176
+ - Your environment details (Ruby version, OS, etc.)
177
+
178
+ ### Contributing Code
179
+ Ready to contribute? You can:
180
+ - Fix bugs by submitting pull requests
181
+ - Improve documentation
182
+ - Add new features (please discuss first in our [discussions section](https://github.com/brownboxdev/grepfruit/discussions))
183
+
184
+ Before contributing, please read the [contributing guidelines](https://github.com/brownboxdev/grepfruit/blob/master/CONTRIBUTING.md)
95
185
 
96
186
  ## License
97
187
 
data/exe/grepfruit CHANGED
@@ -1,31 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ RubyVM::YJIT.enable if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable)
4
+
3
5
  $LOAD_PATH.unshift("#{__dir__}/../lib")
4
6
 
5
- require "optparse"
6
7
  require "grepfruit"
7
8
 
8
- options = {
9
- dir: Dir.pwd,
10
- regex: nil,
11
- exclude: [],
12
- truncate: nil,
13
- search_hidden: false
14
- }
15
-
16
- OptionParser.new do |opts|
17
- opts.banner = "Usage: grepfruit [options] PATH"
18
- opts.on("-r", "--regex REGEX", Regexp, "Regex pattern to search for") { options[:regex] = _1 }
19
- opts.on("-e", "--exclude x,y,z", Array, "Comma-separated list of files and directories to exclude") { options[:exclude] = _1 }
20
- opts.on("-t", "--truncate N", Integer, "Truncate output to N characters") { options[:truncate] = _1 }
21
- opts.on("--search-hidden", TrueClass, "Search hidden files and directories") { options[:search_hidden] = _1 }
22
- end.parse!
23
-
24
- if options[:regex].nil?
25
- puts "Error: You must specify a regex pattern using the -r or --regex option."
26
- exit 1
27
- end
28
-
29
- options[:dir] = ARGV[0] if ARGV[0]
30
-
31
- Grepfruit::Search.new(**options).run
9
+ Grepfruit::CLI.start(ARGV)
data/grepfruit.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.metadata["rubygems_mfa_required"] = "true"
12
12
  spec.summary = "A Ruby gem for searching text patterns in files with colorized output"
13
13
  spec.license = "MIT"
14
- spec.required_ruby_version = ">= 3.1", "< 3.5"
14
+ spec.required_ruby_version = ">= 3.2", "< 3.5"
15
15
 
16
16
  spec.files = [
17
17
  "grepfruit.gemspec", "README.md", "CHANGELOG.md", "LICENSE.txt"
@@ -20,4 +20,6 @@ Gem::Specification.new do |spec|
20
20
  spec.bindir = "exe"
21
21
  spec.executables = ["grepfruit"]
22
22
  spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "dry-cli", "~> 1.2"
23
25
  end
@@ -0,0 +1,62 @@
1
+ require "dry/cli"
2
+
3
+ module Grepfruit
4
+ module Commands
5
+ extend Dry::CLI::Registry
6
+
7
+ class Search < Dry::CLI::Command
8
+ desc "Search for regex patterns in files"
9
+
10
+ argument :path, required: false, default: ".", desc: "Directory or file to search in"
11
+
12
+ option :regex, aliases: ["-r"], required: true, desc: "Regex pattern to search for"
13
+ option :exclude, aliases: ["-e"], type: :array, default: [], desc: "Comma-separated list of files and directories to exclude"
14
+ option :truncate, aliases: ["-t"], type: :integer, desc: "Truncate output to N characters"
15
+ option :search_hidden, type: :boolean, default: false, desc: "Search hidden files and directories"
16
+ option :jobs, aliases: ["-j"], type: :integer, desc: "Number of parallel workers (default: all CPU cores, use 1 for sequential)"
17
+ option :json, type: :boolean, default: false, desc: "Output results in JSON format"
18
+
19
+ def call(path: ".", **options)
20
+ validate_options!(options)
21
+
22
+ Grepfruit::Search.new(
23
+ dir: path,
24
+ regex: create_regex(options[:regex]),
25
+ exclude: options[:exclude] || [],
26
+ truncate: options[:truncate]&.to_i,
27
+ search_hidden: !!options[:search_hidden],
28
+ jobs: options[:jobs]&.to_i,
29
+ json_output: !!options[:json]
30
+ ).run
31
+ end
32
+
33
+ private
34
+
35
+ def validate_options!(options)
36
+ error_exit("You must specify a regex pattern using the -r or --regex option.") unless options[:regex]
37
+
38
+ jobs = options[:jobs]&.to_i
39
+ error_exit("Number of jobs must be at least 1") if jobs && jobs < 1
40
+ end
41
+
42
+ def create_regex(pattern)
43
+ Regexp.new(pattern)
44
+ rescue RegexpError => e
45
+ error_exit("Invalid regex pattern - #{e.message}")
46
+ end
47
+
48
+ def error_exit(message)
49
+ puts "Error: #{message}"
50
+ exit 1
51
+ end
52
+ end
53
+
54
+ register "search", Search, aliases: ["s"]
55
+ end
56
+
57
+ class CLI < Dry::CLI
58
+ def self.start(argv = ARGV)
59
+ Dry::CLI.new(Commands).call(arguments: argv)
60
+ end
61
+ end
62
+ end
@@ -1,45 +1,23 @@
1
1
  module Grepfruit
2
2
  module Decorator
3
- COLORS = { cyan: "\e[36m", red: "\e[31m", green: "\e[32m", reset: "\e[0m" }
4
- private_constant :COLORS
3
+ COLORS = { cyan: "\e[36m", red: "\e[31m", green: "\e[32m", reset: "\e[0m" }.freeze
5
4
 
6
5
  private
7
6
 
8
- def green(text)
9
- "#{COLORS[:green]}#{text}#{COLORS[:reset]}"
10
- end
11
-
12
- def red(text)
13
- "#{COLORS[:red]}#{text}#{COLORS[:reset]}"
14
- end
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]}"
15
10
 
16
- def cyan(text)
17
- "#{COLORS[:cyan]}#{text}#{COLORS[:reset]}"
18
- end
19
-
20
- def number_of_files(num)
21
- "#{num} file#{'s' if num > 1}"
22
- end
23
-
24
- def number_of_matches(num)
25
- "#{num} match#{'es' if num > 1}"
26
- end
11
+ def number_of_files(num) = "#{num} file#{'s' if num != 1}"
12
+ def number_of_matches(num) = "#{num} match#{'es' if num != 1}"
27
13
 
28
14
  def relative_path(path)
29
- Pathname.new(path).relative_path_from(Pathname.new(dir)).to_s
30
- end
31
-
32
- def relative_path_with_line_num(path, line_num)
33
- "#{relative_path(path)}:#{line_num + 1}"
15
+ path.delete_prefix("#{dir}/")
34
16
  end
35
17
 
36
18
  def processed_line(line)
37
19
  stripped_line = line.strip
38
- truncate && stripped_line.length > truncate ? "#{stripped_line[0..truncate - 1]}..." : stripped_line
39
- end
40
-
41
- def decorated_line(path, line_num, line)
42
- "#{cyan(relative_path_with_line_num(path, line_num))}: #{processed_line(line)}"
20
+ truncate && stripped_line.length > truncate ? "#{stripped_line[0...truncate]}..." : stripped_line
43
21
  end
44
22
 
45
23
  def display_results(lines, files, files_with_matches)
@@ -54,5 +32,40 @@ module Grepfruit
54
32
  exit(1)
55
33
  end
56
34
  end
35
+
36
+ def display_json_results(raw_matches, total_files, files_with_matches)
37
+ require "json"
38
+
39
+ search_info = {
40
+ pattern: regex.inspect,
41
+ directory: dir,
42
+ exclusions: (excluded_paths + excluded_lines).map { |path_parts| path_parts.join("/") },
43
+ timestamp: @start_time.strftime("%Y-%m-%dT%H:%M:%S%z")
44
+ }
45
+
46
+ summary = {
47
+ files_checked: total_files,
48
+ files_with_matches: files_with_matches,
49
+ total_matches: raw_matches.size
50
+ }
51
+
52
+ matches = raw_matches.map do |relative_path, line_num, line_content|
53
+ {
54
+ file: relative_path,
55
+ line: line_num,
56
+ content: line_content.strip
57
+ }
58
+ end
59
+
60
+ result = {
61
+ search: search_info,
62
+ summary: summary,
63
+ matches: matches
64
+ }
65
+
66
+ puts JSON.pretty_generate(result)
67
+
68
+ exit(raw_matches.empty? ? 0 : 1)
69
+ end
57
70
  end
58
71
  end
@@ -1,78 +1,142 @@
1
- require "pathname"
2
1
  require "find"
2
+ require "etc"
3
3
 
4
4
  require_relative "decorator"
5
5
 
6
+ Warning[:experimental] = false
7
+
6
8
  module Grepfruit
7
9
  class Search
8
10
  include Decorator
9
11
 
10
- attr_reader :dir, :regex, :excluded_paths, :excluded_lines, :truncate, :search_hidden
12
+ attr_reader :dir, :regex, :excluded_paths, :excluded_lines, :truncate, :search_hidden, :jobs, :json_output
11
13
 
12
- def initialize(dir:, regex:, exclude:, truncate:, search_hidden:)
13
- @dir = dir
14
+ def initialize(dir:, regex:, exclude:, truncate:, search_hidden:, jobs:, json_output: false)
15
+ @dir = File.expand_path(dir)
14
16
  @regex = regex
15
17
  @excluded_lines, @excluded_paths = exclude.map { _1.split("/") }.partition { _1.last.include?(":") }
16
18
  @truncate = truncate
17
19
  @search_hidden = search_hidden
20
+ @jobs = jobs || Etc.nprocessors
21
+ @json_output = json_output
22
+ @start_time = Time.now
18
23
  end
19
24
 
20
25
  def run
21
- lines, files, files_with_matches = [], 0, 0
26
+ puts "Searching for #{regex.inspect} in #{dir.inspect}...\n\n" unless json_output
22
27
 
23
- puts "Searching for #{regex.inspect} in #{dir.inspect}...\n\n"
28
+ all_lines, total_files_with_matches, total_files = [], 0, 0
29
+ raw_matches = []
30
+ workers = create_workers
31
+ file_enumerator = create_file_enumerator
32
+ active_workers = {}
24
33
 
25
- Find.find(dir) do |path|
26
- Find.prune if excluded_path?(path)
34
+ workers.each do |worker|
35
+ file_path = get_next_file(file_enumerator)
36
+ next unless file_path
27
37
 
28
- next if not_searchable?(path)
38
+ worker.send([file_path, regex, excluded_lines, dir])
39
+ active_workers[worker] = file_path
40
+ total_files += 1
41
+ end
29
42
 
30
- files += 1
31
- match = process_file(path, lines)
43
+ while active_workers.any?
44
+ ready_worker, (file_results, has_matches) = Ractor.select(*active_workers.keys)
45
+ active_workers.delete(ready_worker)
32
46
 
33
- if match
34
- files_with_matches += 1
35
- print red("M")
36
- else
37
- print green(".")
38
- end
47
+ total_files_with_matches += 1 if process_worker_result(file_results, has_matches, all_lines, raw_matches)
48
+
49
+ next_file = get_next_file(file_enumerator)
50
+ next unless next_file
51
+
52
+ ready_worker.send([next_file, regex, excluded_lines, dir])
53
+ active_workers[ready_worker] = next_file
54
+ total_files += 1
39
55
  end
40
56
 
41
- display_results(lines, files, files_with_matches)
57
+ workers.each(&:close_outgoing)
58
+
59
+ if json_output
60
+ display_json_results(raw_matches, total_files, total_files_with_matches)
61
+ else
62
+ display_results(all_lines, total_files, total_files_with_matches)
63
+ end
42
64
  end
43
65
 
44
66
  private
45
67
 
46
- def not_searchable?(path)
47
- File.directory?(path) || File.symlink?(path)
68
+ def get_next_file(enumerator)
69
+ enumerator.next
70
+ rescue StopIteration
71
+ nil
48
72
  end
49
73
 
50
- def process_file(path, lines)
51
- lines_size = lines.size
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)
52
83
 
53
- File.foreach(path).with_index do |line, line_num|
54
- next if !line.valid_encoding? || !line.match?(regex) || excluded_line?(path, line_num)
84
+ relative_path = file_path.delete_prefix("#{base_dir}/")
85
+ next if exc_lines.any? { |exc| "#{relative_path}:#{line_num + 1}".end_with?(exc.join("/")) }
55
86
 
56
- lines << decorated_line(path, line_num, line)
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
57
94
  end
95
+ end
96
+
97
+ def create_file_enumerator
98
+ Enumerator.new do |yielder|
99
+ Find.find(dir) do |path|
100
+ Find.prune if excluded_path?(path)
101
+
102
+ next unless File.file?(path)
58
103
 
59
- lines.size > lines_size
104
+ yielder << path
105
+ end
106
+ rescue Errno::ENOENT
107
+ puts "Error: Directory '#{dir}' does not exist."
108
+ exit 1
109
+ end
60
110
  end
61
111
 
62
- def excluded_path?(path)
63
- excluded?(excluded_paths, relative_path(path)) || !search_hidden && hidden?(path)
112
+ def process_worker_result(file_results, has_matches, all_lines, raw_matches)
113
+ if has_matches
114
+ raw_matches.concat(file_results) if json_output
115
+
116
+ unless json_output
117
+ colored_lines = file_results.map do |relative_path, line_num, line_content|
118
+ "#{cyan("#{relative_path}:#{line_num}")}: #{processed_line(line_content)}"
119
+ end
120
+ all_lines.concat(colored_lines)
121
+ print red("M")
122
+ end
123
+ true
124
+ else
125
+ print green(".") unless json_output
126
+ false
127
+ end
64
128
  end
65
129
 
66
- def excluded_line?(path, line_num)
67
- excluded?(excluded_lines, relative_path_with_line_num(path, line_num))
130
+ def excluded_path?(path)
131
+ excluded?(excluded_paths, relative_path(path)) || (!search_hidden && File.basename(path).start_with?("."))
68
132
  end
69
133
 
70
134
  def excluded?(list, path)
71
135
  list.any? { path.split("/").last(_1.length) == _1 }
72
136
  end
73
137
 
74
- def hidden?(path)
75
- File.basename(path).start_with?(".")
138
+ def relative_path(path)
139
+ path.delete_prefix("#{dir}/")
76
140
  end
77
141
  end
78
142
  end
@@ -1,3 +1,3 @@
1
1
  module Grepfruit
2
- VERSION = "2.0.3"
2
+ VERSION = "3.0.0".freeze
3
3
  end
data/lib/grepfruit.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require_relative "grepfruit/version"
2
2
  require_relative "grepfruit/search"
3
+ require_relative "grepfruit/cli"
3
4
 
4
5
  module Grepfruit
5
6
  class Error < StandardError; end
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grepfruit
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.3
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - enjaku4
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-05-16 00:00:00.000000000 Z
11
- dependencies: []
10
+ date: 2025-06-16 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dry-cli
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.2'
12
26
  executables:
13
27
  - grepfruit
14
28
  extensions: []
@@ -20,6 +34,7 @@ files:
20
34
  - exe/grepfruit
21
35
  - grepfruit.gemspec
22
36
  - lib/grepfruit.rb
37
+ - lib/grepfruit/cli.rb
23
38
  - lib/grepfruit/decorator.rb
24
39
  - lib/grepfruit/search.rb
25
40
  - lib/grepfruit/version.rb
@@ -38,7 +53,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
38
53
  requirements:
39
54
  - - ">="
40
55
  - !ruby/object:Gem::Version
41
- version: '3.1'
56
+ version: '3.2'
42
57
  - - "<"
43
58
  - !ruby/object:Gem::Version
44
59
  version: '3.5'
@@ -48,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
48
63
  - !ruby/object:Gem::Version
49
64
  version: '0'
50
65
  requirements: []
51
- rubygems_version: 3.6.2
66
+ rubygems_version: 3.6.3
52
67
  specification_version: 4
53
68
  summary: A Ruby gem for searching text patterns in files with colorized output
54
69
  test_files: []