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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +77 -30
- data/exe/grepfruit +3 -25
- data/grepfruit.gemspec +3 -1
- data/lib/grepfruit/cli.rb +64 -0
- data/lib/grepfruit/decorator.rb +44 -30
- data/lib/grepfruit/search.rb +109 -33
- data/lib/grepfruit/version.rb +1 -1
- data/lib/grepfruit.rb +1 -0
- metadata +20 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 50c38c621aa4a7c34b262cd61c45ced836cca10158b33566d709fd76bdb18f9f
|
4
|
+
data.tar.gz: f0a93887ab397d50a0a22e0600bb06925013632beab1040adf768c831bb6e5ac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
[](http://badge.fury.io/rb/grepfruit)
|
4
4
|
[](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
|
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
|
-
-
|
11
|
-
-
|
10
|
+
- Parallel search using Ractors
|
11
|
+
- JSON output format for programmatic integration
|
12
12
|
- Colorized output for improved readability
|
13
|
-
-
|
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/
|
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
|
-
|
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.
|
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
|
data/lib/grepfruit/decorator.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
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
|
17
|
-
|
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
|
-
|
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
|
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
|
data/lib/grepfruit/search.rb
CHANGED
@@ -1,79 +1,155 @@
|
|
1
|
-
require "pathname"
|
2
1
|
require "find"
|
3
|
-
require "
|
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
|
-
|
27
|
+
puts "Searching for #{regex.inspect} in #{dir.inspect}...\n\n" unless json_output
|
23
28
|
|
24
|
-
|
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
|
-
|
27
|
-
|
35
|
+
workers.each do |worker|
|
36
|
+
assign_file_to_worker(worker, file_enumerator, active_workers) && total_files += 1
|
37
|
+
end
|
28
38
|
|
29
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
48
|
-
|
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
|
52
|
-
|
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
|
-
|
55
|
-
|
87
|
+
results << [relative_path, line_num + 1, line]
|
88
|
+
has_matches = true
|
89
|
+
end
|
56
90
|
|
57
|
-
|
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
|
-
|
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
|
-
|
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
|
68
|
-
|
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
|
72
|
-
|
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
|
76
|
-
|
151
|
+
def relative_path(path)
|
152
|
+
path.delete_prefix("#{dir}/")
|
77
153
|
end
|
78
154
|
end
|
79
155
|
end
|
data/lib/grepfruit/version.rb
CHANGED
data/lib/grepfruit.rb
CHANGED
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:
|
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-
|
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.
|
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.
|
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: []
|