A CLI tool that shows the context of grep results
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. #!/usr/bin/env ruby
  2. require 'optparse'
  3. args = Array.new
  4. rg_args = Array.new
  5. add_to_rg_args = false
  6. ARGV.each do |arg|
  7. if arg == "--"
  8. add_to_rg_args = true
  9. else
  10. if add_to_rg_args
  11. rg_args << arg
  12. else
  13. args << arg
  14. end
  15. end
  16. end
  17. options = {}
  18. OptionParser.new do |opts|
  19. opts.banner = "Usage: grep-around.rb [options] -- [rg options]\n" \
  20. "All options after -- will be passed through to ripgrep"
  21. options[:stdin] = false
  22. options[:around] = 1
  23. opts.on("--stdin", "Read input from standard in") do |stdin|
  24. options[:stdin] = stdin
  25. end
  26. opts.on("--around [LINES]", "Lines around") do |around|
  27. options[:around] = around.to_i
  28. end
  29. opts.on("--up [LINES]", "Lines up") do |up|
  30. options[:up] = up.to_i
  31. end
  32. opts.on("--down [LINES]", "Lines down") do |down|
  33. options[:down] = down.to_i
  34. end
  35. end.parse!(args)
  36. %i[up down].each { |n| options[n] = options[:around] } if options[:around]
  37. GREEN = "\u001b[32m"
  38. RED = "\u001b[31m"
  39. RESET = "\u001b[0m"
  40. class GrepAround
  41. def initialize(options, rg_args)
  42. @options = options
  43. @rg_args = rg_args
  44. @hit = nil
  45. @current_line_number = nil
  46. @file_cache = Hash.new
  47. end
  48. def run
  49. results = run_ripgrep
  50. blocks = Array.new
  51. results.split("\n").each do |line|
  52. @hit = if line.match(/\A\d+:/)
  53. {
  54. :file_name => :stdin,
  55. :line_number => line.match(/\A\d+/)[0].to_i
  56. }
  57. else
  58. parse_metadata(line)
  59. end
  60. blocks << get_block
  61. end
  62. puts blocks.join("\n\n")
  63. end
  64. def run_ripgrep
  65. if @options[:stdin]
  66. contents = STDIN.read
  67. @file_cache[:stdin] = contents.split("\n")
  68. `echo '#{contents}' | rg -n #{@rg_args.join(' ')}`
  69. else
  70. `rg -n #{@rg_args.join(' ')}`
  71. end
  72. end
  73. def get_contents(key)
  74. if @file_cache.has_key?(key)
  75. @file_cache[key]
  76. else
  77. contents = File.readlines(@hit[:file_name])
  78. @file_cache[@hit[:file_name]] = contents
  79. contents
  80. end
  81. end
  82. def get_lines(contents, line_number)
  83. starting_line_number = line_number - @options[:up] - 1
  84. ending_line_number = line_number + @options[:down] - 1
  85. contents[starting_line_number..ending_line_number]
  86. end
  87. def parse_metadata(source)
  88. metadata = source.split(': ').first.split(':')
  89. file = metadata[0]
  90. line = metadata[1]
  91. {
  92. :file_name => file,
  93. :line_number => line.to_i,
  94. }
  95. end
  96. def get_block
  97. contents = get_contents(@hit[:file_name])
  98. lines = get_lines(contents, @hit[:line_number])
  99. block = Array.new
  100. block << block_header
  101. @current_line_number = @hit[:line_number] - @options[:up]
  102. lines.each do |line|
  103. block << build_output(line, @current_line_number)
  104. @current_line_number += 1
  105. end
  106. block.join("\n")
  107. end
  108. def block_header
  109. "#{RED}#{@hit[:file_name]}:#{RESET}"
  110. end
  111. def build_output(line, line_number)
  112. output = String.new
  113. output << line_number.to_s
  114. output << ' '
  115. output << GREEN if is_target_line
  116. output << line.gsub("\n", "")
  117. output << RESET if is_target_line
  118. output
  119. end
  120. def is_target_line
  121. @current_line_number == @hit[:line_number]
  122. end
  123. end
  124. GrepAround.new(options, rg_args).run