Avatar of paulfioravanti

paulfioravanti's solution

to Grep in the Ruby Track

Published at Jun 18 2019 · 0 comments
Instructions
Test suite
Solution

Search a file for lines matching a regular expression pattern. Return the line number and contents of each matching line.

The Unix grep command can be used to search for lines in one or more files that match a user-provided search query (known as the pattern).

The grep command takes three arguments:

  1. The pattern used to match lines in a file.
  2. Zero or more flags to customize the matching behavior.
  3. One or more files in which to search for matching lines.

Your task is to implement the grep function, which should read the contents of the specified files, find the lines that match the specified pattern and then output those lines as a single string. Note that the lines should be output in the order in which they were found, with the first matching line in the first file being output first.

As an example, suppose there is a file named "input.txt" with the following contents:

hello
world
hello again

If we were to call grep "hello" input.txt, the returned string should be:

hello
hello again

Flags

As said earlier, the grep command should also support the following flags:

  • -n Print the line numbers of each matching line.
  • -l Print only the names of files that contain at least one matching line.
  • -i Match line using a case-insensitive comparison.
  • -v Invert the program -- collect all lines that fail to match the pattern.
  • -x Only match entire lines, instead of lines that contain a match.

If we run grep -n "hello" input.txt, the -n flag will require the matching lines to be prefixed with its line number:

1:hello
3:hello again

And if we run grep -i "HELLO" input.txt, we'll do a case-insensitive match, and the output will be:

hello
hello again

The grep command should support multiple flags at once.

For example, running grep -l -v "hello" file1.txt file2.txt should print the names of files that do not contain the string "hello".


For installation and learning resources, refer to the Ruby resources page.

For running the tests provided, you will need the Minitest gem. Open a terminal window and run the following command to install minitest:

gem install minitest

If you would like color output, you can require 'minitest/pride' in the test file, or note the alternative instruction, below, for running the test file.

Run the tests from the exercise directory using the following command:

ruby grep_test.rb

To include color from the command line:

ruby -r minitest/pride grep_test.rb

Source

Conversation with Nate Foster. http://www.cs.cornell.edu/Courses/cs3110/2014sp/hw/0/ps0.pdf

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

grep_test.rb

require 'minitest/autorun'
require_relative 'grep'

# Common test data version: 1.2.0 4f2efaa
class GrepTest < Minitest::Test
  def setup
    IO.write 'iliad.txt', <<~END
      Achilles sing, O Goddess! Peleus' son;
      His wrath pernicious, who ten thousand woes
      Caused to Achaia's host, sent many a soul
      Illustrious into Ades premature,
      And Heroes gave (so stood the will of Jove)
      To dogs and to all ravening fowls a prey,
      When fierce dispute had separated once
      The noble Chief Achilles from the son
      Of Atreus, Agamemnon, King of men.
    END

    IO.write 'midsummer-night.txt', <<~END
      I do entreat your grace to pardon me.
      I know not by what power I am made bold,
      Nor how it may concern my modesty,
      In such a presence here to plead my thoughts;
      But I beseech your grace that I may know
      The worst that may befall me in this case,
      If I refuse to wed Demetrius.
    END

    IO.write 'paradise-lost.txt', <<~END
      Of Mans First Disobedience, and the Fruit
      Of that Forbidden Tree, whose mortal tast
      Brought Death into the World, and all our woe,
      With loss of Eden, till one greater Man
      Restore us, and regain the blissful Seat,
      Sing Heav'nly Muse, that on the secret top
      Of Oreb, or of Sinai, didst inspire
      That Shepherd, who first taught the chosen Seed
    END
  end

  def teardown
    File.delete('iliad.txt')
    File.delete('midsummer-night.txt')
    File.delete('paradise-lost.txt')
  end

  def test_one_file_one_match_no_flags
    # skip
    pattern = "Agamemnon"
    flags = []
    files = ["iliad.txt"]
    expected = <<~EXPECTED.rstrip
      Of Atreus, Agamemnon, King of men.
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_one_file_one_match_print_line_numbers_flag
    skip
    pattern = "Forbidden"
    flags = ["-n"]
    files = ["paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip
      2:Of that Forbidden Tree, whose mortal tast
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_one_file_one_match_case_insensitive_flag
    skip
    pattern = "FORBIDDEN"
    flags = ["-i"]
    files = ["paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip
      Of that Forbidden Tree, whose mortal tast
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_one_file_one_match_print_file_names_flag
    skip
    pattern = "Forbidden"
    flags = ["-l"]
    files = ["paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip
      paradise-lost.txt
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_one_file_one_match_match_entire_lines_flag
    skip
    pattern = "With loss of Eden, till one greater Man"
    flags = ["-x"]
    files = ["paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip
      With loss of Eden, till one greater Man
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_one_file_one_match_multiple_flags
    skip
    pattern = "OF ATREUS, Agamemnon, KIng of MEN."
    flags = ["-n", "-i", "-x"]
    files = ["iliad.txt"]
    expected = <<~EXPECTED.rstrip
      9:Of Atreus, Agamemnon, King of men.
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_one_file_several_matches_no_flags
    skip
    pattern = "may"
    flags = []
    files = ["midsummer-night.txt"]
    expected = <<~EXPECTED.rstrip
      Nor how it may concern my modesty,
      But I beseech your grace that I may know
      The worst that may befall me in this case,
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_one_file_several_matches_print_line_numbers_flag
    skip
    pattern = "may"
    flags = ["-n"]
    files = ["midsummer-night.txt"]
    expected = <<~EXPECTED.rstrip
      3:Nor how it may concern my modesty,
      5:But I beseech your grace that I may know
      6:The worst that may befall me in this case,
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_one_file_several_matches_match_entire_lines_flag
    skip
    pattern = "may"
    flags = ["-x"]
    files = ["midsummer-night.txt"]
    expected = <<~EXPECTED.rstrip

    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_one_file_several_matches_case_insensitive_flag
    skip
    pattern = "ACHILLES"
    flags = ["-i"]
    files = ["iliad.txt"]
    expected = <<~EXPECTED.rstrip
      Achilles sing, O Goddess! Peleus' son;
      The noble Chief Achilles from the son
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_one_file_several_matches_inverted_flag
    skip
    pattern = "Of"
    flags = ["-v"]
    files = ["paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip
      Brought Death into the World, and all our woe,
      With loss of Eden, till one greater Man
      Restore us, and regain the blissful Seat,
      Sing Heav'nly Muse, that on the secret top
      That Shepherd, who first taught the chosen Seed
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_one_file_no_matches_various_flags
    skip
    pattern = "Gandalf"
    flags = ["-n", "-l", "-x", "-i"]
    files = ["iliad.txt"]
    expected = <<~EXPECTED.rstrip

    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_one_file_one_match_file_flag_takes_precedence_over_line_flag
    skip
    pattern = "ten"
    flags = ["-n", "-l"]
    files = ["iliad.txt"]
    expected = <<~EXPECTED.rstrip
      iliad.txt
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_one_file_several_matches_inverted_and_match_entire_lines_flags
    skip
    pattern = "Illustrious into Ades premature,"
    flags = ["-x", "-v"]
    files = ["iliad.txt"]
    expected = <<~EXPECTED.rstrip
      Achilles sing, O Goddess! Peleus' son;
      His wrath pernicious, who ten thousand woes
      Caused to Achaia's host, sent many a soul
      And Heroes gave (so stood the will of Jove)
      To dogs and to all ravening fowls a prey,
      When fierce dispute had separated once
      The noble Chief Achilles from the son
      Of Atreus, Agamemnon, King of men.
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_multiple_files_one_match_no_flags
    skip
    pattern = "Agamemnon"
    flags = []
    files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip
      iliad.txt:Of Atreus, Agamemnon, King of men.
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_multiple_files_several_matches_no_flags
    skip
    pattern = "may"
    flags = []
    files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip
      midsummer-night.txt:Nor how it may concern my modesty,
      midsummer-night.txt:But I beseech your grace that I may know
      midsummer-night.txt:The worst that may befall me in this case,
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_multiple_files_several_matches_print_line_numbers_flag
    skip
    pattern = "that"
    flags = ["-n"]
    files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip
      midsummer-night.txt:5:But I beseech your grace that I may know
      midsummer-night.txt:6:The worst that may befall me in this case,
      paradise-lost.txt:2:Of that Forbidden Tree, whose mortal tast
      paradise-lost.txt:6:Sing Heav'nly Muse, that on the secret top
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_multiple_files_one_match_print_file_names_flag
    skip
    pattern = "who"
    flags = ["-l"]
    files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip
      iliad.txt
      paradise-lost.txt
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_multiple_files_several_matches_case_insensitive_flag
    skip
    pattern = "TO"
    flags = ["-i"]
    files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip
      iliad.txt:Caused to Achaia's host, sent many a soul
      iliad.txt:Illustrious into Ades premature,
      iliad.txt:And Heroes gave (so stood the will of Jove)
      iliad.txt:To dogs and to all ravening fowls a prey,
      midsummer-night.txt:I do entreat your grace to pardon me.
      midsummer-night.txt:In such a presence here to plead my thoughts;
      midsummer-night.txt:If I refuse to wed Demetrius.
      paradise-lost.txt:Brought Death into the World, and all our woe,
      paradise-lost.txt:Restore us, and regain the blissful Seat,
      paradise-lost.txt:Sing Heav'nly Muse, that on the secret top
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_multiple_files_several_matches_inverted_flag
    skip
    pattern = "a"
    flags = ["-v"]
    files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip
      iliad.txt:Achilles sing, O Goddess! Peleus' son;
      iliad.txt:The noble Chief Achilles from the son
      midsummer-night.txt:If I refuse to wed Demetrius.
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_multiple_files_one_match_match_entire_lines_flag
    skip
    pattern = "But I beseech your grace that I may know"
    flags = ["-x"]
    files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip
      midsummer-night.txt:But I beseech your grace that I may know
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_multiple_files_one_match_multiple_flags
    skip
    pattern = "WITH LOSS OF EDEN, TILL ONE GREATER MAN"
    flags = ["-n", "-i", "-x"]
    files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip
      paradise-lost.txt:4:With loss of Eden, till one greater Man
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_multiple_files_no_matches_various_flags
    skip
    pattern = "Frodo"
    flags = ["-n", "-l", "-x", "-i"]
    files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip

    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_multiple_files_several_matches_file_flag_takes_precedence_over_line_number_flag
    skip
    pattern = "who"
    flags = ["-n", "-l"]
    files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip
      iliad.txt
      paradise-lost.txt
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end

  def test_multiple_files_several_matches_inverted_and_match_entire_lines_flags
    skip
    pattern = "Illustrious into Ades premature,"
    flags = ["-x", "-v"]
    files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]
    expected = <<~EXPECTED.rstrip
      iliad.txt:Achilles sing, O Goddess! Peleus' son;
      iliad.txt:His wrath pernicious, who ten thousand woes
      iliad.txt:Caused to Achaia's host, sent many a soul
      iliad.txt:And Heroes gave (so stood the will of Jove)
      iliad.txt:To dogs and to all ravening fowls a prey,
      iliad.txt:When fierce dispute had separated once
      iliad.txt:The noble Chief Achilles from the son
      iliad.txt:Of Atreus, Agamemnon, King of men.
      midsummer-night.txt:I do entreat your grace to pardon me.
      midsummer-night.txt:I know not by what power I am made bold,
      midsummer-night.txt:Nor how it may concern my modesty,
      midsummer-night.txt:In such a presence here to plead my thoughts;
      midsummer-night.txt:But I beseech your grace that I may know
      midsummer-night.txt:The worst that may befall me in this case,
      midsummer-night.txt:If I refuse to wed Demetrius.
      paradise-lost.txt:Of Mans First Disobedience, and the Fruit
      paradise-lost.txt:Of that Forbidden Tree, whose mortal tast
      paradise-lost.txt:Brought Death into the World, and all our woe,
      paradise-lost.txt:With loss of Eden, till one greater Man
      paradise-lost.txt:Restore us, and regain the blissful Seat,
      paradise-lost.txt:Sing Heav'nly Muse, that on the secret top
      paradise-lost.txt:Of Oreb, or of Sinai, didst inspire
      paradise-lost.txt:That Shepherd, who first taught the chosen Seed
    EXPECTED

    assert_equal expected, Grep.grep(pattern, flags, files)
  end
end
# frozen_string_literal: true

module Grep
  COLON = ":"
  private_constant :COLON
  ENTIRE_LINE = ->(pattern) { "\\A#{pattern}\n\\z" }
  private_constant :ENTIRE_LINE
  ENTIRE_LINES_ONLY = "-x"
  private_constant :ENTIRE_LINES_ONLY
  FILENAMES_ONLY = "-l"
  private_constant :FILENAMES_ONLY
  IGNORE_CASE = "-i"
  private_constant :IGNORE_CASE
  INVERT_PATTERN = "-v"
  private_constant :INVERT_PATTERN
  LINE_NUMBERS = "-n"
  private_constant :LINE_NUMBERS
  NEWLINE = "\n"
  private_constant :NEWLINE
  NO_OPTIONS = 0
  private_constant :NO_OPTIONS
  STARTING_INDEX = 1
  private_constant :STARTING_INDEX

  module_function

  def grep(pattern, flags, files)
    regexp = generate_regexp(pattern, flags)
    multiple_files = files.length > 1
    line_guard = generate_line_guard(flags)

    files
      .each
      .with_object([flags, regexp, multiple_files, line_guard])
      .each_with_object([], &method(:grep_file))
      .join(NEWLINE)
  end

  def generate_regexp(pattern, flags)
    string =
      flags.include?(ENTIRE_LINES_ONLY) ? ENTIRE_LINE.call(pattern) : pattern
    options =
      flags.include?(IGNORE_CASE) ? Regexp::IGNORECASE : NO_OPTIONS
    Regexp.new(string, options)
  end
  private_class_method :generate_regexp

  def generate_line_guard(flags)
    if flags.include?(INVERT_PATTERN)
      ->(line, regexp) { throw(:next) if line.match?(regexp) }
    else
      ->(line, regexp) { throw(:next) unless line.match?(regexp) }
    end
  end
  private_class_method :generate_line_guard

  def grep_file((filename, (flags, regexp, multiple_files, line_guard)), acc)
    File.open(filename) do |file|
      object = [filename, flags, regexp, multiple_files, line_guard, acc]
      file
        .each
        .with_index(STARTING_INDEX)
        .with_object(object, &method(:grep_line))
    end
  end
  private_class_method :grep_file

  def grep_line((line, index), object)
    filename, flags, regexp, multiple_files, line_guard, acc = object
    catch(:next) do
      line_guard.call(line, regexp)
      add_filename_only(flags, filename, acc)
      acc << add_line(flags, multiple_files, filename, index, line)
    end
  end
  private_class_method :grep_line

  def add_filename_only(flags, filename, acc)
    return unless flags.include?(FILENAMES_ONLY)

    throw(:next) if acc.include?(filename)
    acc << filename
    throw(:next)
  end
  private_class_method :add_filename_only

  def add_line(flags, multiple_files, filename, index, line)
    String.new.tap do |result|
      result << filename + COLON if multiple_files
      result << index.to_s + COLON if flags.include?(LINE_NUMBERS)
      result << line.strip
    end
  end
  private_class_method :add_line
end

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?