grepfruit 2.0.4 → 3.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: 510715dadc6ac5afba47b5a02b122eb6ae53e46dcb4454a3d83adf7374615b1e
4
- data.tar.gz: 4797c7612d6f0ab3e0ec3c15a3c48941984f9eba4837a919ba98f10551d3c4fb
3
+ metadata.gz: 50c38c621aa4a7c34b262cd61c45ced836cca10158b33566d709fd76bdb18f9f
4
+ data.tar.gz: f0a93887ab397d50a0a22e0600bb06925013632beab1040adf768c831bb6e5ac
5
5
  SHA512:
6
- metadata.gz: a62a669cb08f455f3105ee73b4f70d991216c7b34e68477adf6546d922e43e5994ff9a9cfca38671adf69b90cb89edf1960019b8d5dd2fb3bb8df5602f6ccc2c
7
- data.tar.gz: b85bd902107f90713cd14be4ce8662250975c76dfd95e7757a12f36da4004261098545e7929abe103c9b8204b7c457c011d129ff5daff5cc5dafa5e4aa163b34
6
+ metadata.gz: 50838340c7e1b6168675ee64bf0f0608beaa708237d12b40efc0fb8099afbde20ae5f18860747984e37e60cf090d4ff4b5e96667cd728dd51299bc5b44ebcdf2
7
+ data.tar.gz: 66e4d2c8cda063641243eaad779c43bd5fba37ecaad1bda19c81de996986688ff2eb825d86260da1f6a2bda0b522de73c5e5af304b3fe166f2d0438d15fa270f
data/CHANGELOG.md CHANGED
@@ -1,4 +1,18 @@
1
+ ## v3.1.0
2
+
3
+ - Added --include option to specify files to include in the search
4
+ - Both --exclude and --include options can now accept multiple patterns
5
+
6
+ ## v3.0.0
7
+
8
+ - Dropped support for Ruby 3.1
9
+ - Optimized search algorithm for better performance
10
+ - Changed the interface: now use `grepfruit search` instead of just `grepfruit` to perform searches
11
+ - Added JSON output format for search results
12
+ - Added parallel processing and --jobs option to control worker count
13
+
1
14
  ## v2.0.4
15
+
2
16
  - Fixed path resolution bug where searching in relative directories such as `.`, `./`, or `..` did not work correctly
3
17
 
4
18
  ## v2.0.3
data/README.md CHANGED
@@ -3,17 +3,14 @@
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 specified regular expression patterns, with intelligent exclusion options and 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.
6
+ Grepfruit is a Ruby gem for searching files within a directory for specified regular expression patterns, with exclusion and inclusion 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
8
  **Key Features:**
9
9
 
10
- - Regular expression search within files and directories
11
- - Intelligent file and directory exclusion capabilities
10
+ - Parallel search using Ractors
11
+ - JSON output format for programmatic integration
12
12
  - Colorized output for improved readability
13
- - Hidden file and directory search support
14
- - Configurable output truncation
15
- - CI/CD pipeline friendly with meaningful exit codes
16
- - Line-specific exclusion for precise control
13
+ - CI/CD pipeline friendly exit codes
17
14
 
18
15
  ## Table of Contents
19
16
 
@@ -25,26 +22,14 @@ Grepfruit is a Ruby gem for searching files within a directory for specified reg
25
22
  - [Exit Status](#exit-status)
26
23
 
27
24
  **Community Resources:**
28
- - [Contributing](#contributing)
25
+ - [Getting Help and Contributing](#getting-help-and-contributing)
29
26
  - [License](#license)
30
27
  - [Code of Conduct](#code-of-conduct)
31
28
 
32
29
  ## Installation
33
30
 
34
- Add Grepfruit to your Gemfile:
35
-
36
- ```rb
37
- gem "grepfruit"
38
- ```
39
-
40
31
  Install the gem:
41
32
 
42
- ```bash
43
- bundle install
44
- ```
45
-
46
- Or install it directly:
47
-
48
33
  ```bash
49
34
  gem install grepfruit
50
35
  ```
@@ -54,7 +39,13 @@ gem install grepfruit
54
39
  Search for regex patterns within files in a specified directory:
55
40
 
56
41
  ```bash
57
- grepfruit [options] PATH
42
+ grepfruit search [options] [PATH]
43
+ ```
44
+
45
+ Or using shorthand `s` command:
46
+
47
+ ```bash
48
+ grepfruit s [options] [PATH]
58
49
  ```
59
50
 
60
51
  If no PATH is specified, Grepfruit searches the current directory.
@@ -65,8 +56,11 @@ If no PATH is specified, Grepfruit searches the current directory.
65
56
  |--------|-------------|
66
57
  | `-r, --regex REGEX` | Regex pattern to search for (required) |
67
58
  | `-e, --exclude x,y,z` | Comma-separated list of files, directories, or lines to exclude |
59
+ | `-i, --include x,y,z` | Comma-separated list of file patterns to include (only these files will be searched) |
68
60
  | `-t, --truncate N` | Truncate search result output to N characters |
61
+ | `-j, --jobs N` | Number of parallel workers (default: number of CPU cores) |
69
62
  | `--search-hidden` | Include hidden files and directories in search |
63
+ | `--json` | Output results in JSON format |
70
64
 
71
65
  ## Usage Examples
72
66
 
@@ -75,7 +69,7 @@ If no PATH is specified, Grepfruit searches the current directory.
75
69
  Search for `TODO` comments in the current directory:
76
70
 
77
71
  ```bash
78
- grepfruit -r 'TODO'
72
+ grepfruit search -r 'TODO'
79
73
  ```
80
74
 
81
75
  ### Excluding Directories
@@ -83,7 +77,7 @@ grepfruit -r 'TODO'
83
77
  Search for `TODO` patterns while excluding common build and dependency directories:
84
78
 
85
79
  ```bash
86
- grepfruit -r 'TODO' -e 'log,tmp,vendor,node_modules,assets'
80
+ grepfruit search -r 'TODO' -e 'log,tmp,vendor,node_modules,assets'
87
81
  ```
88
82
 
89
83
  ### Multiple Pattern Search Excluding Both Directories and Files
@@ -91,7 +85,7 @@ grepfruit -r 'TODO' -e 'log,tmp,vendor,node_modules,assets'
91
85
  Search for both `FIXME` and `TODO` comments in a specific directory:
92
86
 
93
87
  ```bash
94
- grepfruit -r 'FIXME|TODO' -e 'bin,tmp/log,Gemfile.lock' dev/grepfruit
88
+ grepfruit search -r 'FIXME|TODO' -e 'bin,*.md,tmp/log,Gemfile.lock' dev/my_app
95
89
  ```
96
90
 
97
91
  ### Line-Specific Exclusion
@@ -99,7 +93,16 @@ grepfruit -r 'FIXME|TODO' -e 'bin,tmp/log,Gemfile.lock' dev/grepfruit
99
93
  Exclude specific lines from search results:
100
94
 
101
95
  ```bash
102
- grepfruit -r 'FIXME|TODO' -e 'README.md:18'
96
+ grepfruit search -r 'FIXME|TODO' -e 'README.md:18'
97
+ ```
98
+
99
+ ### Including Specific File Types
100
+
101
+ Search only in specific file types using patterns:
102
+
103
+ ```bash
104
+ grepfruit search -r 'TODO' -i '*.rb,*.js'
105
+ grepfruit search -r 'FIXME' -i '*.py'
103
106
  ```
104
107
 
105
108
  ### Output Truncation
@@ -107,7 +110,7 @@ grepfruit -r 'FIXME|TODO' -e 'README.md:18'
107
110
  Limit output length for cleaner results:
108
111
 
109
112
  ```bash
110
- grepfruit -r 'FIXME|TODO' -t 50
113
+ grepfruit search -r 'FIXME|TODO' -t 50
111
114
  ```
112
115
 
113
116
  ### Including Hidden Files
@@ -115,17 +118,61 @@ grepfruit -r 'FIXME|TODO' -t 50
115
118
  Search hidden files and directories:
116
119
 
117
120
  ```bash
118
- grepfruit -r 'FIXME|TODO' --search-hidden
121
+ grepfruit search -r 'FIXME|TODO' --search-hidden
122
+ ```
123
+
124
+ ### JSON Output
125
+
126
+ Get structured JSON output:
127
+
128
+ ```bash
129
+ grepfruit search -r 'TODO' -e 'node_modules' -i '*.rb,*.js' --json /path/to/search
130
+ ```
131
+
132
+ This outputs a JSON response containing search metadata, summary statistics, and detailed match information:
133
+
134
+ ```jsonc
135
+ {
136
+ "search": {
137
+ "pattern": "/TODO/",
138
+ "directory": "/path/to/search",
139
+ "exclusions": ["node_modules"],
140
+ "inclusions": ["*.rb", "*.js"],
141
+ "timestamp": "2025-01-16T10:30:00+00:00"
142
+ },
143
+ "summary": {
144
+ "files_checked": 42,
145
+ "files_with_matches": 8,
146
+ "total_matches": 23
147
+ },
148
+ "matches": [
149
+ {
150
+ "file": "src/main.js",
151
+ "line": 15,
152
+ "content": "// TODO: Implement error handling"
153
+ },
154
+ // ...
155
+ ]
156
+ }
157
+ ```
158
+
159
+ ### Parallel Processing
160
+
161
+ Control the number of parallel workers:
162
+
163
+ ```bash
164
+ grepfruit search -r 'TODO' -j 8 # Use 8 parallel workers
165
+ grepfruit search -r 'TODO' -j 1 # Sequential processing
119
166
  ```
120
167
 
121
168
  ## Exit Status
122
169
 
123
170
  Grepfruit returns meaningful exit codes for CI/CD integration:
124
171
 
125
- - **Exit code 0**: No matches found
126
- - **Exit code 1**: Pattern matches were found
172
+ - **Exit code 0**: No matches found (ideal for quality gates - code is clean)
173
+ - **Exit code 1**: Pattern matches were found (CI should fail - issues detected)
127
174
 
128
- ## Contributing
175
+ ## Getting Help and Contributing
129
176
 
130
177
  ### Getting Help
131
178
  Have a question or need assistance? Open a discussion in our [discussions section](https://github.com/brownboxdev/grepfruit/discussions) for:
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: ".",
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,64 @@
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 :include, aliases: ["-i"], type: :array, default: [], desc: "Comma-separated list of file patterns to include (only these files will be searched)"
15
+ option :truncate, aliases: ["-t"], type: :integer, desc: "Truncate output to N characters"
16
+ option :search_hidden, type: :boolean, default: false, desc: "Search hidden files and directories"
17
+ option :jobs, aliases: ["-j"], type: :integer, desc: "Number of parallel workers (default: all CPU cores, use 1 for sequential)"
18
+ option :json, type: :boolean, default: false, desc: "Output results in JSON format"
19
+
20
+ def call(path: ".", **options)
21
+ validate_options!(options)
22
+
23
+ Grepfruit::Search.new(
24
+ dir: path,
25
+ regex: create_regex(options[:regex]),
26
+ exclude: options[:exclude] || [],
27
+ include: options[:include] || [],
28
+ truncate: options[:truncate]&.to_i,
29
+ search_hidden: !!options[:search_hidden],
30
+ jobs: options[:jobs]&.to_i,
31
+ json_output: !!options[:json]
32
+ ).run
33
+ end
34
+
35
+ private
36
+
37
+ def validate_options!(options)
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
42
+ end
43
+
44
+ def create_regex(pattern)
45
+ Regexp.new(pattern)
46
+ rescue RegexpError => e
47
+ error_exit("Invalid regex pattern - #{e.message}")
48
+ end
49
+
50
+ def error_exit(message)
51
+ puts "Error: #{message}"
52
+ exit 1
53
+ end
54
+ end
55
+
56
+ register "search", Search, aliases: ["s"]
57
+ end
58
+
59
+ class CLI < Dry::CLI
60
+ def self.start(argv = ARGV)
61
+ Dry::CLI.new(Commands).call(arguments: argv)
62
+ end
63
+ end
64
+ 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,41 @@ 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
+ inclusions: included_paths.map { |path_parts| path_parts.join("/") },
44
+ timestamp: @start_time.strftime("%Y-%m-%dT%H:%M:%S%z")
45
+ }
46
+
47
+ summary = {
48
+ files_checked: total_files,
49
+ files_with_matches: files_with_matches,
50
+ total_matches: raw_matches.size
51
+ }
52
+
53
+ matches = raw_matches.map do |relative_path, line_num, line_content|
54
+ {
55
+ file: relative_path,
56
+ line: line_num,
57
+ content: line_content.strip
58
+ }
59
+ end
60
+
61
+ result = {
62
+ search: search_info,
63
+ summary: summary,
64
+ matches: matches
65
+ }
66
+
67
+ puts JSON.pretty_generate(result)
68
+
69
+ exit(raw_matches.empty? ? 0 : 1)
70
+ end
57
71
  end
58
72
  end
@@ -1,79 +1,155 @@
1
- require "pathname"
2
1
  require "find"
3
- require "byebug"
2
+ require "etc"
4
3
 
5
4
  require_relative "decorator"
6
5
 
6
+ Warning[:experimental] = false
7
+
7
8
  module Grepfruit
8
9
  class Search
9
10
  include Decorator
10
11
 
11
- attr_reader :dir, :regex, :excluded_paths, :excluded_lines, :truncate, :search_hidden
12
+ attr_reader :dir, :regex, :excluded_paths, :excluded_lines, :included_paths, :truncate, :search_hidden, :jobs, :json_output
12
13
 
13
- def initialize(dir:, regex:, exclude:, truncate:, search_hidden:)
14
+ def initialize(dir:, regex:, exclude:, include:, truncate:, search_hidden:, jobs:, json_output: false)
14
15
  @dir = File.expand_path(dir)
15
16
  @regex = regex
16
17
  @excluded_lines, @excluded_paths = exclude.map { _1.split("/") }.partition { _1.last.include?(":") }
18
+ @included_paths = include.map { _1.split("/") }
17
19
  @truncate = truncate
18
20
  @search_hidden = search_hidden
21
+ @jobs = jobs || Etc.nprocessors
22
+ @json_output = json_output
23
+ @start_time = Time.now
19
24
  end
20
25
 
21
26
  def run
22
- lines, files, files_with_matches = [], 0, 0
27
+ puts "Searching for #{regex.inspect} in #{dir.inspect}...\n\n" unless json_output
23
28
 
24
- puts "Searching for #{regex.inspect} in #{dir.inspect}...\n\n"
29
+ all_lines, total_files_with_matches, total_files = [], 0, 0
30
+ raw_matches = []
31
+ workers = create_workers
32
+ file_enumerator = create_file_enumerator
33
+ active_workers = {}
25
34
 
26
- Find.find(dir) do |path|
27
- Find.prune if excluded_path?(path)
35
+ workers.each do |worker|
36
+ assign_file_to_worker(worker, file_enumerator, active_workers) && total_files += 1
37
+ end
28
38
 
29
- next if not_searchable?(path)
39
+ while active_workers.any?
40
+ ready_worker, (file_results, has_matches) = Ractor.select(*active_workers.keys)
41
+ active_workers.delete(ready_worker)
30
42
 
31
- files += 1
32
- match = process_file(path, lines)
43
+ total_files_with_matches += 1 if process_worker_result(file_results, has_matches, all_lines, raw_matches)
33
44
 
34
- if match
35
- files_with_matches += 1
36
- print red("M")
37
- else
38
- print green(".")
39
- end
45
+ assign_file_to_worker(ready_worker, file_enumerator, active_workers) && total_files += 1
40
46
  end
41
47
 
42
- display_results(lines, files, files_with_matches)
48
+ workers.each(&:close_outgoing)
49
+
50
+ if json_output
51
+ display_json_results(raw_matches, total_files, total_files_with_matches)
52
+ else
53
+ display_results(all_lines, total_files, total_files_with_matches)
54
+ end
43
55
  end
44
56
 
45
57
  private
46
58
 
47
- def not_searchable?(path)
48
- File.directory?(path) || File.symlink?(path)
59
+ def assign_file_to_worker(worker, file_enumerator, active_workers)
60
+ file_path = get_next_file(file_enumerator)
61
+ return false unless file_path
62
+
63
+ worker.send([file_path, regex, excluded_lines, dir])
64
+ active_workers[worker] = file_path
65
+ true
49
66
  end
50
67
 
51
- def process_file(path, lines)
52
- lines_size = lines.size
68
+ def get_next_file(enumerator)
69
+ enumerator.next
70
+ rescue StopIteration
71
+ nil
72
+ end
73
+
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("/")) }
53
86
 
54
- File.foreach(path).with_index do |line, line_num|
55
- next if !line.valid_encoding? || !line.match?(regex) || excluded_line?(path, line_num)
87
+ results << [relative_path, line_num + 1, line]
88
+ has_matches = true
89
+ end
56
90
 
57
- lines << decorated_line(path, line_num, line)
91
+ Ractor.yield([results, has_matches])
92
+ end
93
+ end
58
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)
59
101
 
60
- lines.size > lines_size
102
+ next unless File.file?(path)
103
+
104
+ yielder << path
105
+ end
106
+ rescue Errno::ENOENT
107
+ puts "Error: Directory '#{dir}' does not exist."
108
+ exit 1
109
+ end
110
+ end
111
+
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
61
128
  end
62
129
 
63
130
  def excluded_path?(path)
64
- excluded?(excluded_paths, relative_path(path)) || !search_hidden && hidden?(path)
131
+ rel_path = relative_path(path)
132
+
133
+ not_included_path?(path, rel_path) || matches_pattern?(excluded_paths, rel_path) || excluded_hidden?(path)
65
134
  end
66
135
 
67
- def excluded_line?(path, line_num)
68
- excluded?(excluded_lines, relative_path_with_line_num(path, line_num))
136
+ def not_included_path?(path, rel_path)
137
+ File.file?(path) && included_paths.any? && !matches_pattern?(included_paths, rel_path)
69
138
  end
70
139
 
71
- def excluded?(list, path)
72
- list.any? { path.split("/").last(_1.length) == _1 }
140
+ def excluded_hidden?(path)
141
+ !search_hidden && File.basename(path).start_with?(".")
142
+ end
143
+
144
+ def matches_pattern?(pattern_list, path)
145
+ pattern_list.any? do |pattern_parts|
146
+ pattern = pattern_parts.join("/")
147
+ File.fnmatch?(pattern, path, File::FNM_PATHNAME) || File.fnmatch?(pattern, File.basename(path))
148
+ end
73
149
  end
74
150
 
75
- def hidden?(path)
76
- File.basename(path).start_with?(".")
151
+ def relative_path(path)
152
+ path.delete_prefix("#{dir}/")
77
153
  end
78
154
  end
79
155
  end
@@ -1,3 +1,3 @@
1
1
  module Grepfruit
2
- VERSION = "2.0.4"
2
+ VERSION = "3.1.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.4
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - enjaku4
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-05-30 00:00:00.000000000 Z
11
- dependencies: []
10
+ date: 2025-07-07 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: []