├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── SECURITY.md ├── bin ├── console ├── rspec ├── rubocop └── setup ├── exe └── grepfruit ├── grepfruit.gemspec ├── lib ├── grepfruit.rb └── grepfruit │ ├── decorator.rb │ ├── search.rb │ └── version.rb └── spec ├── grepfruit └── search_spec.rb ├── spec_helper.rb └── test_dataset ├── .hidden ├── bar.txt ├── baz.py ├── folder └── bad.yml └── foo.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [brownboxdev] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "bundler" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | todo: 8 | runs-on: ubuntu-latest 9 | 10 | name: Todo 11 | permissions: 12 | contents: read 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Ruby 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: 3.1 19 | - name: Bundle install 20 | run: bundle install 21 | - name: Run Todo Search 22 | run: ./exe/grepfruit -r 'TODO' -e 'vendor,.yardoc,.git,ci.yml:22,README.md,spec/grepfruit/search_spec.rb,spec/test_dataset' --search-hidden 23 | 24 | 25 | linting: 26 | runs-on: ubuntu-latest 27 | 28 | name: RuboCop 29 | permissions: 30 | contents: read 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Set up Ruby 34 | uses: ruby/setup-ruby@v1 35 | with: 36 | ruby-version: 3.1 37 | - name: Bundle install 38 | run: bundle install 39 | - name: Run RuboCop 40 | run: bundle exec rubocop --color 41 | 42 | tests: 43 | runs-on: ubuntu-latest 44 | 45 | name: Ruby ${{ matrix.ruby }} 46 | permissions: 47 | contents: read 48 | strategy: 49 | matrix: 50 | ruby: 51 | - "3.1" 52 | - "3.2" 53 | - "3.3" 54 | - "3.4" 55 | 56 | steps: 57 | - uses: actions/checkout@v4 58 | - name: Set up Ruby 59 | uses: ruby/setup-ruby@v1 60 | with: 61 | ruby-version: ${{ matrix.ruby }} 62 | - name: Bundle install 63 | run: bundle install 64 | - name: Run RSpec 65 | run: bundle exec rspec 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | .rspec_status 11 | .DS_Store 12 | .ruby-version 13 | .byebug_history 14 | .idea 15 | .vscode 16 | .solargraph.yml 17 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --tty 2 | --color 3 | --require spec_helper 4 | --order random 5 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - rubocop-md 3 | - rubocop-packaging 4 | - rubocop-performance 5 | - rubocop-rake 6 | - rubocop-rspec 7 | - rubocop-thread_safety 8 | 9 | AllCops: 10 | TargetRubyVersion: 3.1 11 | NewCops: enable 12 | 13 | Layout/LineLength: 14 | Enabled: false 15 | 16 | Lint/AmbiguousOperatorPrecedence: 17 | Enabled: false 18 | 19 | Metrics/AbcSize: 20 | Enabled: false 21 | 22 | Metrics/CyclomaticComplexity: 23 | Enabled: false 24 | 25 | Metrics/MethodLength: 26 | Enabled: false 27 | 28 | Metrics/PerceivedComplexity: 29 | Enabled: false 30 | 31 | RSpec/ExampleWording: 32 | Enabled: false 33 | 34 | RSpec/NamedSubject: 35 | Enabled: false 36 | 37 | Style/Documentation: 38 | Enabled: false 39 | 40 | Style/FrozenStringLiteralComment: 41 | Enabled: false 42 | 43 | Style/MutableConstant: 44 | Enabled: false 45 | 46 | Style/ParallelAssignment: 47 | Enabled: false 48 | 49 | Style/StringLiterals: 50 | EnforcedStyle: double_quotes 51 | 52 | Style/WordArray: 53 | EnforcedStyle: brackets 54 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v2.0.4 2 | 3 | - Fixed path resolution bug where searching in relative directories such as `.`, `./`, or `..` did not work correctly 4 | 5 | ## v2.0.3 6 | 7 | - Updated gemspec metadata to include the correct homepage URL 8 | 9 | ## v2.0.2 10 | 11 | - Replaced `git ls-files` with `Dir.glob` in gemspec for improved portability and compatibility 12 | 13 | ## v2.0.1 14 | 15 | - Enhanced output to include the number of files with matches 16 | 17 | ## v2.0.0 18 | 19 | - Added support for Ruby 3.4 20 | - Dropped support for Ruby 3.0 21 | 22 | ## v1.1.2 23 | 24 | - Refactored code significantly for improved search efficiency and easier maintenance 25 | - Enhanced search result output for better readability 26 | 27 | ## v1.1.1 28 | 29 | - Added test dataset to make testing and development easier 30 | - Updated gemspec file to include missing metadata 31 | 32 | ## v1.1.0 33 | 34 | - Added `--truncate` option to truncate the output of the search results 35 | - Added the ability to exclude lines from the search results 36 | 37 | ## v1.0.0 38 | 39 | - Removed default values for `--exclude` and `--regex` options 40 | - Made `--regex` option required 41 | - Fixed an error that was raised when a symbolic link was encountered during the search 42 | 43 | ## v0.2.0 44 | 45 | - Added `--search-hidden` option to search hidden files and directories 46 | 47 | ## v0.1.0 48 | 49 | - Initial release 50 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at https://www.contributor-covenant.org/version/1/3/0/code-of-conduct.html 46 | 47 | [homepage]: https://www.contributor-covenant.org 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Issues 4 | 5 | Issues should be created only for bugs. 6 | 7 | When opening an issue: 8 | 9 | - Describe the steps to reproduce the problem. 10 | - Include the stack trace if there is an error. 11 | - Specify the versions of Grepfruit, Ruby, etc. 12 | 13 | Don't hesitate to add any other information that you think might be relevant. 14 | 15 | ## Pull Requests 16 | 17 | If you want to contribute an enhancement or a fix: 18 | 19 | - Fork the project on GitHub. 20 | - Make your changes, including tests. 21 | - Update the documentation if necessary. 22 | - Commit the changes and submit a pull request. 23 | 24 | By submitting a pull request, you indicate that you waive any rights or claims to the modifications made in the Grepfruit project and transfer the copyright of those changes to the Grepfruit gem copyright owners. 25 | 26 | If you are unable or unwilling to transfer these rights, please refrain from submitting a pull request. 27 | 28 | ## Local Development 29 | 30 | Use the Ruby version specified in the `grepfruit.gemspec` file. 31 | 32 | To install the development dependencies, run: `bin/setup` 33 | 34 | To start the developer console, run: `bin/console` 35 | 36 | To run the tests: `bin/rspec` 37 | 38 | To run the linter: `bin/rubocop` 39 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "byebug" 6 | gem "rake" 7 | gem "rspec" 8 | gem "rubocop" 9 | gem "rubocop-md" 10 | gem "rubocop-packaging" 11 | gem "rubocop-performance" 12 | gem "rubocop-rake" 13 | gem "rubocop-rspec" 14 | gem "rubocop-thread_safety" 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 enjaku4 (https://github.com/enjaku4) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grepfruit: File Pattern Search Tool for Ruby 2 | 3 | [![Gem Version](https://badge.fury.io/rb/grepfruit.svg)](http://badge.fury.io/rb/grepfruit) 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 | 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. 7 | 8 | **Key Features:** 9 | 10 | - Regular expression search within files and directories 11 | - Intelligent file and directory exclusion capabilities 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 | 17 | ## Table of Contents 18 | 19 | **Gem Usage:** 20 | - [Installation](#installation) 21 | - [Basic Usage](#basic-usage) 22 | - [Command Line Options](#command-line-options) 23 | - [Usage Examples](#usage-examples) 24 | - [Exit Status](#exit-status) 25 | 26 | **Community Resources:** 27 | - [Contributing](#contributing) 28 | - [License](#license) 29 | - [Code of Conduct](#code-of-conduct) 30 | 31 | ## Installation 32 | 33 | Add Grepfruit to your Gemfile: 34 | 35 | ```rb 36 | gem "grepfruit" 37 | ``` 38 | 39 | Install the gem: 40 | 41 | ```bash 42 | bundle install 43 | ``` 44 | 45 | Or install it directly: 46 | 47 | ```bash 48 | gem install grepfruit 49 | ``` 50 | 51 | ## Basic Usage 52 | 53 | Search for regex patterns within files in a specified directory: 54 | 55 | ```bash 56 | grepfruit [options] PATH 57 | ``` 58 | 59 | If no PATH is specified, Grepfruit searches the current directory. 60 | 61 | ## Command Line Options 62 | 63 | | Option | Description | 64 | |--------|-------------| 65 | | `-r, --regex REGEX` | Regex pattern to search for (required) | 66 | | `-e, --exclude x,y,z` | Comma-separated list of files, directories, or lines to exclude | 67 | | `-t, --truncate N` | Truncate search result output to N characters | 68 | | `--search-hidden` | Include hidden files and directories in search | 69 | 70 | ## Usage Examples 71 | 72 | ### Basic Pattern Search 73 | 74 | Search for `TODO` comments in the current directory: 75 | 76 | ```bash 77 | grepfruit -r 'TODO' 78 | ``` 79 | 80 | ### Excluding Directories 81 | 82 | Search for `TODO` patterns while excluding common build and dependency directories: 83 | 84 | ```bash 85 | grepfruit -r 'TODO' -e 'log,tmp,vendor,node_modules,assets' 86 | ``` 87 | 88 | ### Multiple Pattern Search Excluding Both Directories and Files 89 | 90 | Search for both `FIXME` and `TODO` comments in a specific directory: 91 | 92 | ```bash 93 | grepfruit -r 'FIXME|TODO' -e 'bin,tmp/log,Gemfile.lock' dev/grepfruit 94 | ``` 95 | 96 | ### Line-Specific Exclusion 97 | 98 | Exclude specific lines from search results: 99 | 100 | ```bash 101 | grepfruit -r 'FIXME|TODO' -e 'README.md:18' 102 | ``` 103 | 104 | ### Output Truncation 105 | 106 | Limit output length for cleaner results: 107 | 108 | ```bash 109 | grepfruit -r 'FIXME|TODO' -t 50 110 | ``` 111 | 112 | ### Including Hidden Files 113 | 114 | Search hidden files and directories: 115 | 116 | ```bash 117 | grepfruit -r 'FIXME|TODO' --search-hidden 118 | ``` 119 | 120 | ## Exit Status 121 | 122 | Grepfruit returns meaningful exit codes for CI/CD integration: 123 | 124 | - **Exit code 0**: No matches found 125 | - **Exit code 1**: Pattern matches were found 126 | 127 | ## Contributing 128 | 129 | ### Getting Help 130 | Have a question or need assistance? Open a discussion in our [discussions section](https://github.com/brownboxdev/grepfruit/discussions) for: 131 | - Usage questions 132 | - Implementation guidance 133 | - Feature suggestions 134 | 135 | ### Reporting Issues 136 | Found a bug? Please [create an issue](https://github.com/brownboxdev/grepfruit/issues) with: 137 | - A clear description of the problem 138 | - Steps to reproduce the issue 139 | - Your environment details (Ruby version, OS, etc.) 140 | 141 | ### Contributing Code 142 | Ready to contribute? You can: 143 | - Fix bugs by submitting pull requests 144 | - Improve documentation 145 | - Add new features (please discuss first in our [discussions section](https://github.com/brownboxdev/grepfruit/discussions)) 146 | 147 | Before contributing, please read the [contributing guidelines](https://github.com/brownboxdev/grepfruit/blob/master/CONTRIBUTING.md) 148 | 149 | ## License 150 | 151 | The gem is available as open source under the terms of the [MIT License](https://github.com/brownboxdev/grepfruit/blob/master/LICENSE.txt). 152 | 153 | ## Code of Conduct 154 | 155 | Everyone interacting in the Grepfruit project is expected to follow the [code of conduct](https://github.com/brownboxdev/grepfruit/blob/master/CODE_OF_CONDUCT.md). 156 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | require "rubocop/rake_task" 7 | 8 | RuboCop::RakeTask.new 9 | 10 | task default: %i[spec rubocop] 11 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Grepfruit provides security updates only for the latest major version. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | If you discover a vulnerability in Grepfruit, you can report it via the [GitHub security advisories page](https://github.com/brownboxdev/grepfruit/security/advisories) 10 | 11 | To ensure user safety, please do not publicly disclose the vulnerability until it has been resolved and a patched version released. 12 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "grepfruit" 5 | 6 | require "irb" 7 | IRB.start(__FILE__) 8 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rspec' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 10 | 11 | bundle_binstub = File.expand_path("bundle", __dir__) 12 | 13 | if File.file?(bundle_binstub) 14 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 15 | load(bundle_binstub) 16 | else 17 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 18 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 19 | end 20 | end 21 | 22 | require "rubygems" 23 | require "bundler/setup" 24 | 25 | load Gem.bin_path("rspec-core", "rspec") 26 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rubocop' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 10 | 11 | bundle_binstub = File.expand_path("bundle", __dir__) 12 | 13 | if File.file?(bundle_binstub) 14 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 15 | load(bundle_binstub) 16 | else 17 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 18 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 19 | end 20 | end 21 | 22 | require "rubygems" 23 | require "bundler/setup" 24 | 25 | load Gem.bin_path("rubocop", "rubocop") 26 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | -------------------------------------------------------------------------------- /exe/grepfruit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH.unshift("#{__dir__}/../lib") 4 | 5 | require "optparse" 6 | require "grepfruit" 7 | 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 32 | -------------------------------------------------------------------------------- /grepfruit.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/grepfruit/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "grepfruit" 5 | spec.version = Grepfruit::VERSION 6 | spec.authors = ["enjaku4"] 7 | spec.homepage = "https://github.com/brownboxdev/grepfruit" 8 | spec.metadata["homepage_uri"] = spec.homepage 9 | spec.metadata["source_code_uri"] = spec.homepage 10 | spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" 11 | spec.metadata["rubygems_mfa_required"] = "true" 12 | spec.summary = "A Ruby gem for searching text patterns in files with colorized output" 13 | spec.license = "MIT" 14 | spec.required_ruby_version = ">= 3.1", "< 3.5" 15 | 16 | spec.files = [ 17 | "grepfruit.gemspec", "README.md", "CHANGELOG.md", "LICENSE.txt" 18 | ] + Dir.glob("{exe,lib}/**/*") 19 | 20 | spec.bindir = "exe" 21 | spec.executables = ["grepfruit"] 22 | spec.require_paths = ["lib"] 23 | end 24 | -------------------------------------------------------------------------------- /lib/grepfruit.rb: -------------------------------------------------------------------------------- 1 | require_relative "grepfruit/version" 2 | require_relative "grepfruit/search" 3 | 4 | module Grepfruit 5 | class Error < StandardError; end 6 | end 7 | -------------------------------------------------------------------------------- /lib/grepfruit/decorator.rb: -------------------------------------------------------------------------------- 1 | module Grepfruit 2 | module Decorator 3 | COLORS = { cyan: "\e[36m", red: "\e[31m", green: "\e[32m", reset: "\e[0m" } 4 | private_constant :COLORS 5 | 6 | private 7 | 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 15 | 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 27 | 28 | 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}" 34 | end 35 | 36 | def processed_line(line) 37 | 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)}" 43 | end 44 | 45 | def display_results(lines, files, files_with_matches) 46 | puts "\n\n" if files.positive? 47 | 48 | if lines.empty? 49 | puts "#{number_of_files(files)} checked, #{green('no matches found')}" 50 | exit(0) 51 | else 52 | puts "Matches:\n\n#{lines.join("\n")}\n\n" 53 | puts "#{number_of_files(files)} checked, #{red("#{number_of_matches(lines.size)} found in #{number_of_files(files_with_matches)}")}" 54 | exit(1) 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/grepfruit/search.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | require "find" 3 | require "byebug" 4 | 5 | require_relative "decorator" 6 | 7 | module Grepfruit 8 | class Search 9 | include Decorator 10 | 11 | attr_reader :dir, :regex, :excluded_paths, :excluded_lines, :truncate, :search_hidden 12 | 13 | def initialize(dir:, regex:, exclude:, truncate:, search_hidden:) 14 | @dir = File.expand_path(dir) 15 | @regex = regex 16 | @excluded_lines, @excluded_paths = exclude.map { _1.split("/") }.partition { _1.last.include?(":") } 17 | @truncate = truncate 18 | @search_hidden = search_hidden 19 | end 20 | 21 | def run 22 | lines, files, files_with_matches = [], 0, 0 23 | 24 | puts "Searching for #{regex.inspect} in #{dir.inspect}...\n\n" 25 | 26 | Find.find(dir) do |path| 27 | Find.prune if excluded_path?(path) 28 | 29 | next if not_searchable?(path) 30 | 31 | files += 1 32 | match = process_file(path, lines) 33 | 34 | if match 35 | files_with_matches += 1 36 | print red("M") 37 | else 38 | print green(".") 39 | end 40 | end 41 | 42 | display_results(lines, files, files_with_matches) 43 | end 44 | 45 | private 46 | 47 | def not_searchable?(path) 48 | File.directory?(path) || File.symlink?(path) 49 | end 50 | 51 | def process_file(path, lines) 52 | lines_size = lines.size 53 | 54 | File.foreach(path).with_index do |line, line_num| 55 | next if !line.valid_encoding? || !line.match?(regex) || excluded_line?(path, line_num) 56 | 57 | lines << decorated_line(path, line_num, line) 58 | end 59 | 60 | lines.size > lines_size 61 | end 62 | 63 | def excluded_path?(path) 64 | excluded?(excluded_paths, relative_path(path)) || !search_hidden && hidden?(path) 65 | end 66 | 67 | def excluded_line?(path, line_num) 68 | excluded?(excluded_lines, relative_path_with_line_num(path, line_num)) 69 | end 70 | 71 | def excluded?(list, path) 72 | list.any? { path.split("/").last(_1.length) == _1 } 73 | end 74 | 75 | def hidden?(path) 76 | File.basename(path).start_with?(".") 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/grepfruit/version.rb: -------------------------------------------------------------------------------- 1 | module Grepfruit 2 | VERSION = "2.0.4" 3 | end 4 | -------------------------------------------------------------------------------- /spec/grepfruit/search_spec.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | 3 | RSpec.describe Grepfruit::Search do 4 | context "when no parameters are specified" do 5 | subject { `./exe/grepfruit` } 6 | 7 | it { is_expected.to include("Error: You must specify a regex pattern using the -r or --regex option.") } 8 | end 9 | 10 | context "when path is not specified" do 11 | subject { `./exe/grepfruit -r TODO` } 12 | 13 | it { is_expected.to include("Searching for /TODO/ in #{Dir.pwd.inspect}...") } 14 | end 15 | 16 | context "when curent directory is specified as ." do 17 | subject { `./exe/grepfruit -r TODO .` } 18 | 19 | it { is_expected.to include("Searching for /TODO/ in #{Dir.pwd.inspect}...") } 20 | it { is_expected.to include("matches found") } 21 | end 22 | 23 | context "when path is specified" do 24 | subject { `./exe/grepfruit -r 'TODO' ./spec/test_dataset` } 25 | 26 | it { is_expected.to include(%r{Searching for /TODO/ in ".+/spec/test_dataset"...}) } 27 | it { is_expected.to include("bar.txt:7") } 28 | it { is_expected.to include("folder/bad.yml:21") } 29 | it { is_expected.to include("TODO: Add more details about feature 3.") } 30 | it { is_expected.to include("TODO: Refactor this function to improve readability") } 31 | it { is_expected.to include("4 files checked") } 32 | it { is_expected.to include("16 matches found") } 33 | it { is_expected.to include("in 4 files") } 34 | it { is_expected.not_to include(".hidden") } 35 | end 36 | 37 | context "when full option name --regex is used" do 38 | subject { `./exe/grepfruit --regex 'TODO' ./spec/test_dataset` } 39 | 40 | it { is_expected.to include(%r{Searching for /TODO/ in ".+/spec/test_dataset"...}) } 41 | it { is_expected.to include("bar.txt:7") } 42 | it { is_expected.to include("folder/bad.yml:21") } 43 | it { is_expected.to include("TODO: Add more details about feature 3.") } 44 | it { is_expected.to include("TODO: Refactor this function to improve readability") } 45 | it { is_expected.to include("4 files checked") } 46 | it { is_expected.to include("16 matches found") } 47 | it { is_expected.to include("in 4 files") } 48 | it { is_expected.not_to include(".hidden") } 49 | end 50 | 51 | context "when more complex regex is specified" do 52 | subject { `./exe/grepfruit -r 'TODO|FIXME' ./spec/test_dataset` } 53 | 54 | it { is_expected.to include(%r{Searching for /TODO|FIXME/ in ".+/spec/test_dataset"...}) } 55 | it { is_expected.to include("baz.py:42") } 56 | it { is_expected.to include("This function is not working as expected") } 57 | it { is_expected.to include("bar.txt:7") } 58 | it { is_expected.to include("Update the user permissions module.") } 59 | it { is_expected.to include("17 matches found") } 60 | it { is_expected.to include("in 4 files") } 61 | end 62 | 63 | context "when only one match is found and only one file is checked" do 64 | subject { `./exe/grepfruit -r 'FIXME' ./spec/test_dataset/baz.py` } 65 | 66 | it { is_expected.to include("1 file checked") } 67 | it { is_expected.to include("1 match found") } 68 | it { is_expected.to include("in 1 file") } 69 | end 70 | 71 | context "when no matches are found" do 72 | subject { `./exe/grepfruit -r FOOBAR ./spec/test_dataset` } 73 | 74 | it { is_expected.to include("4 files checked") } 75 | it { is_expected.to include("no matches found") } 76 | end 77 | 78 | context "when no matches are found and 1 file is checked" do 79 | subject { `./exe/grepfruit -r FOOBAR ./spec/test_dataset/folder` } 80 | 81 | it { is_expected.to include("1 file checked") } 82 | it { is_expected.to include("no matches found") } 83 | end 84 | 85 | context "when multiple directories and files are excluded" do 86 | subject { `./exe/grepfruit -e 'folder,bar.txt' -r TODO ./spec/test_dataset` } 87 | 88 | it { is_expected.not_to include("folder/") } 89 | it { is_expected.not_to include("bar.txt") } 90 | end 91 | 92 | context "when full option name --exclude is used" do 93 | subject { `./exe/grepfruit --exclude 'folder,bar.txt' -r TODO ./spec/test_dataset` } 94 | 95 | it { is_expected.not_to include("folder/") } 96 | it { is_expected.not_to include("bar.txt") } 97 | end 98 | 99 | context "when nothing is excluded" do 100 | subject { `./exe/grepfruit -r TODO ./spec/test_dataset` } 101 | 102 | it { is_expected.to include("folder/bad.yml") } 103 | it { is_expected.to include("bar.txt") } 104 | it { is_expected.to include("baz.py") } 105 | it { is_expected.to include("foo.md") } 106 | end 107 | 108 | context "when a relative path is excluded" do 109 | subject { `./exe/grepfruit -r 'TODO' -e 'folder/bad.yml' ./spec/test_dataset` } 110 | 111 | it { is_expected.not_to include("bad.yml") } 112 | end 113 | 114 | context "when a specific line is excluded" do 115 | subject { `./exe/grepfruit -r 'TODO' -e 'bar.txt:14' ./spec/test_dataset` } 116 | 117 | it { is_expected.not_to include("bar.txt:14") } 118 | end 119 | 120 | context "when only a part of the file name is excluded" do 121 | subject { `./exe/grepfruit -e '.txt' -r TODO ./spec/test_dataset` } 122 | 123 | it { is_expected.to include("bar.txt") } 124 | end 125 | 126 | context "when truncation is enabled" do 127 | subject { `./exe/grepfruit -r 'TODO' -t 15 ./spec/test_dataset` } 128 | 129 | it { is_expected.to include("TODO: Add unit ...") } 130 | it { is_expected.to include("TODO: Update th...") } 131 | end 132 | 133 | context "when full option name --truncate is used" do 134 | subject { `./exe/grepfruit -r 'TODO' --truncate 15 ./spec/test_dataset` } 135 | 136 | it { is_expected.to include("TODO: Add unit ...") } 137 | it { is_expected.to include("TODO: Update th...") } 138 | end 139 | 140 | context "when hidden files search is enabled" do 141 | subject { `./exe/grepfruit -r 'TODO' --search-hidden ./spec/test_dataset` } 142 | 143 | it { is_expected.to include(".hidden:2") } 144 | it { is_expected.to include("Verify if the data needs to be encrypted.") } 145 | end 146 | 147 | context "when hidden files search is enabled and a hidden file is excluded" do 148 | subject { `./exe/grepfruit -r 'TODO' -e '.hidden' --search-hidden ./spec/test_dataset` } 149 | 150 | it { is_expected.not_to include(".hidden") } 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "grepfruit" 2 | require "byebug" 3 | 4 | RSpec.configure do |config| 5 | # Enable flags like --only-failures and --next-failure 6 | config.example_status_persistence_file_path = ".rspec_status" 7 | 8 | # Disable RSpec exposing methods globally on `Module` and `main` 9 | config.disable_monkey_patching! 10 | 11 | config.expect_with :rspec do |c| 12 | c.syntax = :expect 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/test_dataset/.hidden: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 2 | TODO: Update the placeholder text with actual content. 3 | The quick brown fox jumps over the lazy dog. 4 | TODO: Verify if the data needs to be encrypted. 5 | A journey of a thousand miles begins with a single step. 6 | -------------------------------------------------------------------------------- /spec/test_dataset/bar.txt: -------------------------------------------------------------------------------- 1 | The quick brown fox jumps over the lazy dog. 2 | She sells seashells by the seashore. 3 | TODO: Fix the alignment issue in the header. 4 | An apple a day keeps the doctor away. 5 | He went to the store to buy some milk. 6 | The rain in Spain stays mainly in the plain. 7 | TODO: Update the user permissions module. 8 | The cat sat on the mat. 9 | He reads books every night before bed. 10 | The early bird catches the worm. 11 | She loves to paint landscapes and portraits. 12 | Birds of a feather flock together. 13 | Jack and Jill went up the hill. 14 | TODO: Review the new design specifications. 15 | All that glitters is not gold. 16 | A picture is worth a thousand words. 17 | Actions speak louder than words. 18 | Better late than never. 19 | A watched pot never boils. 20 | Curiosity killed the cat. 21 | -------------------------------------------------------------------------------- /spec/test_dataset/baz.py: -------------------------------------------------------------------------------- 1 | import os 2 | def calculate_sum(a, b): 3 | return a + b 4 | 5 | for i in range(10): 6 | print(i) 7 | 8 | TODO: Implement the error handling for file operations 9 | 10 | class MyClass: 11 | def __init__(self, value): 12 | self.value = value 13 | 14 | def main(): 15 | my_obj = MyClass(10) 16 | print(my_obj.value) 17 | 18 | if __name__ == "__main__": 19 | main() 20 | 21 | # TODO: Optimize the algorithm for better performance 22 | 23 | with open('file.txt', 'r') as file: 24 | content = file.read() 25 | 26 | TODO: Add unit tests for the calculate_sum function 27 | 28 | try: 29 | result = calculate_sum(5, 7) 30 | print(result) 31 | except Exception as e: 32 | print(e) 33 | 34 | # Initialize the dictionary 35 | my_dict = {'key1': 'value1', 'key2': 'value2'} 36 | 37 | TODO: Refactor this function to improve readability 38 | 39 | def multiply(x, y): 40 | return x * y 41 | 42 | # FIXME: This function is not working as expected 43 | 44 | result = multiply(3, 4) 45 | print(result) 46 | -------------------------------------------------------------------------------- /spec/test_dataset/folder/bad.yml: -------------------------------------------------------------------------------- 1 | server: 2 | host: localhost 3 | port: 8080 4 | 5 | database: 6 | type: mysql 7 | host: db.example.com 8 | port: 3306 9 | username: user 10 | password: pass 11 | name: mydatabase 12 | 13 | logging: 14 | level: info 15 | file: /var/log/myapp.log 16 | 17 | api: 18 | version: v1 19 | base_url: /api/v1 20 | 21 | # TODO: Add configuration for cache settings 22 | 23 | features: 24 | enable_feature_x: true 25 | enable_feature_y: false 26 | 27 | security: 28 | enable_https: true 29 | allowed_ips: 30 | - 192.168.1.1 31 | - 192.168.1.2 32 | 33 | notifications: 34 | email: true 35 | sms: false 36 | -------------------------------------------------------------------------------- /spec/test_dataset/foo.md: -------------------------------------------------------------------------------- 1 | # Project Title 2 | 3 | ## Introduction 4 | This project aims to solve problem X by implementing solution Y. 5 | 6 | ## Features 7 | - Feature 1: Description of feature 1. 8 | - Feature 2: Description of feature 2. 9 | - Feature 3: Description of feature 3. 10 | - TODO: Add more details about feature 3. 11 | 12 | ## Installation 13 | 1. Clone the repository. 14 | 2. Run `npm install`. 15 | 3. TODO: Include additional setup instructions. 16 | 17 | ## Usage 18 | To use the application, run the following command: 19 | 20 | ```bash 21 | npm start 22 | ``` 23 | 24 | Contributing 25 | 26 | TODO: Write contribution guidelines. 27 | 28 | License 29 | 30 | This project is licensed under the MIT License. 31 | 32 | TODOs 33 | 34 | TODO: Update the README with latest screenshots. 35 | TODO: Verify the installation steps on a clean system. 36 | TODO: Add instructions for deployment. 37 | TODO: Review the usage section for completeness. 38 | Contact 39 | 40 | For more information, contact us at email@example.com. 41 | --------------------------------------------------------------------------------