diff --git a/bin/test-exercises.sh b/bin/test-exercises.sh index 0fa6d50b..23ddb29a 100755 --- a/bin/test-exercises.sh +++ b/bin/test-exercises.sh @@ -7,10 +7,21 @@ echo $(crystal -v) test_run() { echo "Testing $1" cat "$1/.meta/src/$2.cr" > "${dic}/src/$2.cr" + if [ -d "$1/assets/." ]; then + cp -a "$1/assets/." "./assets/" + fi spec_file="$1/$(jq -r '.files.test[0]' $1/.meta/config.json)" cat "${spec_file}" > "${dic}/spec/spec.cr" sed -i -e 's/pending/it/g' ${dic}/spec/spec.cr - crystal spec ${dic}/spec/spec.cr || exit 1 + crystal spec ${dic}/spec/spec.cr + code=$? + if [ -d "$1/assets/." ]; then + rm -rf ./assets/ + fi + if [ $code -ne 0 ]; then + echo "Tests failed for $1" + exit $code + fi } for exercise in ./exercises/practice/*; do diff --git a/config.json b/config.json index 4a708598..338c760c 100644 --- a/config.json +++ b/config.json @@ -1265,6 +1265,14 @@ "prerequisites": [], "difficulty": 4 }, + { + "slug": "grep", + "name": "Grep", + "uuid": "de1af862-3118-428e-8590-72806a700780", + "practices": [], + "prerequisites": [], + "difficulty": 4 + }, { "slug": "complex-numbers", "name": "Complex Numbers", diff --git a/exercises/practice/grep/.docs/instructions.md b/exercises/practice/grep/.docs/instructions.md new file mode 100644 index 00000000..004f28ac --- /dev/null +++ b/exercises/practice/grep/.docs/instructions.md @@ -0,0 +1,27 @@ +# Instructions + +Search files for lines matching a search string and return all matching lines. + +The Unix [`grep`][grep] command searches files for lines that match a regular expression. +Your task is to implement a simplified `grep` command, which supports searching for fixed strings. + +The `grep` command takes three arguments: + +1. The string to search for. +2. Zero or more flags for customizing the command's behavior. +3. One or more files to search in. + +It then reads the contents of the specified files (in the order specified), finds the lines that contain the search string, and finally returns those lines in the order in which they were found. +When searching in multiple files, each matching line is prepended by the file name and a colon (':'). + +## Flags + +The `grep` command supports the following flags: + +- `-n` Prepend the line number and a colon (':') to each line in the output, placing the number after the filename (if present). +- `-l` Output only the names of the files that contain at least one matching line. +- `-i` Match using a case-insensitive comparison. +- `-v` Invert the program -- collect all lines that fail to match. +- `-x` Search only for lines where the search string matches the entire line. + +[grep]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/grep.html diff --git a/exercises/practice/grep/.meta/config.json b/exercises/practice/grep/.meta/config.json new file mode 100644 index 00000000..bacdeb8e --- /dev/null +++ b/exercises/practice/grep/.meta/config.json @@ -0,0 +1,24 @@ +{ + "authors": [ + "meatball133" + ], + "files": { + "solution": [ + "src/grep.cr" + ], + "test": [ + "spec/grep_spec.cr" + ], + "example": [ + ".meta/src/example.cr" + ], + "editor": [ + "assets/iliad.txt", + "assets/midsummer-night.txt", + "assets/paradise-lost.txt" + ] + }, + "blurb": "Search a file for lines matching a regular expression pattern. Return the line number and contents of each matching line.", + "source": "Conversation with Nate Foster.", + "source_url": "https://www.cs.cornell.edu/Courses/cs3110/2014sp/hw/0/ps0.pdf" +} diff --git a/exercises/practice/grep/.meta/src/example.cr b/exercises/practice/grep/.meta/src/example.cr new file mode 100644 index 00000000..b5773b50 --- /dev/null +++ b/exercises/practice/grep/.meta/src/example.cr @@ -0,0 +1,46 @@ +module Grep + def self.search(pattern : String, flags : Array(String), files : Array(String)) : String + # - `-n` Prepend the line number and a colon (':') to each line in the output, placing the number after the filename (if present). + # - `-l` Output only the names of the files that contain at least one matching line. + # - `-i` Match using a case-insensitive comparison. + # - `-v` Invert the program -- collect all lines that fail to match. + # - `-x` Search only for lines where the search string matches the entire line. + + results = [] of String + files.each do |file| + File.open("assets/#{file}") do |f| + f.each_line.with_index(1) do |line, line_number| + line_to_check = line.chomp + pattern_to_check = pattern.dup + + if flags.includes?("-i") + line_to_check = line_to_check.downcase + pattern_to_check = pattern_to_check.downcase + end + + is_match = if flags.includes?("-x") + line_to_check == pattern_to_check + else + line_to_check.includes?(pattern_to_check) + end + + is_match = !is_match if flags.includes?("-v") + + if is_match + if flags.includes?("-l") + results << file unless results.includes?(file) + break + else + result_line = "" + result_line += "#{file}:" if files.size > 1 && !flags.includes?("-l") + result_line += "#{line_number}:" if flags.includes?("-n") + result_line += line.chomp + results << result_line + end + end + end + end + end + results.join("\n") + end +end diff --git a/exercises/practice/grep/.meta/test_template.ecr b/exercises/practice/grep/.meta/test_template.ecr new file mode 100644 index 00000000..400754b7 --- /dev/null +++ b/exercises/practice/grep/.meta/test_template.ecr @@ -0,0 +1,17 @@ +require "spec" +require "../src/*" + +describe "<%-= to_capitalized(@json["exercise"].to_s) %>" do +<%- @json["cases"].as_a.each do |cases| %> + <%- cases["cases"].as_a.each do |subcases| %> + <%= status()%> "<%-= subcases["description"] %>" do + expected = "<%= subcases["expected"].as_a.join("\n") %>" + pattern = "<%= subcases["input"]["pattern"] %>" + flags = <%= to_s_deep(subcases["input"]["flags"]) %> <%= to_s_deep(subcases["input"]["flags"]) == "[]" ? "of String" : "" %> + files = <%= to_s_deep(subcases["input"]["files"]) %> + + <%= to_capitalized(@json["exercise"].to_s) %>.search(pattern, flags, files).should eq(expected) + end + <% end %> +<% end %> +end diff --git a/exercises/practice/grep/.meta/tests.toml b/exercises/practice/grep/.meta/tests.toml new file mode 100644 index 00000000..04c51e71 --- /dev/null +++ b/exercises/practice/grep/.meta/tests.toml @@ -0,0 +1,85 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[9049fdfd-53a7-4480-a390-375203837d09] +description = "Test grepping a single file -> One file, one match, no flags" + +[76519cce-98e3-46cd-b287-aac31b1d77d6] +description = "Test grepping a single file -> One file, one match, print line numbers flag" + +[af0b6d3c-e0e8-475e-a112-c0fc10a1eb30] +description = "Test grepping a single file -> One file, one match, case-insensitive flag" + +[ff7af839-d1b8-4856-a53e-99283579b672] +description = "Test grepping a single file -> One file, one match, print file names flag" + +[8625238a-720c-4a16-81f2-924ec8e222cb] +description = "Test grepping a single file -> One file, one match, match entire lines flag" + +[2a6266b3-a60f-475c-a5f5-f5008a717d3e] +description = "Test grepping a single file -> One file, one match, multiple flags" + +[842222da-32e8-4646-89df-0d38220f77a1] +description = "Test grepping a single file -> One file, several matches, no flags" + +[4d84f45f-a1d8-4c2e-a00e-0b292233828c] +description = "Test grepping a single file -> One file, several matches, print line numbers flag" + +[0a483b66-315b-45f5-bc85-3ce353a22539] +description = "Test grepping a single file -> One file, several matches, match entire lines flag" + +[3d2ca86a-edd7-494c-8938-8eeed1c61cfa] +description = "Test grepping a single file -> One file, several matches, case-insensitive flag" + +[1f52001f-f224-4521-9456-11120cad4432] +description = "Test grepping a single file -> One file, several matches, inverted flag" + +[7a6ede7f-7dd5-4364-8bf8-0697c53a09fe] +description = "Test grepping a single file -> One file, no matches, various flags" + +[3d3dfc23-8f2a-4e34-abd6-7b7d140291dc] +description = "Test grepping a single file -> One file, one match, file flag takes precedence over line flag" + +[87b21b24-b788-4d6e-a68b-7afe9ca141fe] +description = "Test grepping a single file -> One file, several matches, inverted and match entire lines flags" + +[ba496a23-6149-41c6-a027-28064ed533e5] +description = "Test grepping multiples files at once -> Multiple files, one match, no flags" + +[4539bd36-6daa-4bc3-8e45-051f69f5aa95] +description = "Test grepping multiples files at once -> Multiple files, several matches, no flags" + +[9fb4cc67-78e2-4761-8e6b-a4b57aba1938] +description = "Test grepping multiples files at once -> Multiple files, several matches, print line numbers flag" + +[aeee1ef3-93c7-4cd5-af10-876f8c9ccc73] +description = "Test grepping multiples files at once -> Multiple files, one match, print file names flag" + +[d69f3606-7d15-4ddf-89ae-01df198e6b6c] +description = "Test grepping multiples files at once -> Multiple files, several matches, case-insensitive flag" + +[82ef739d-6701-4086-b911-007d1a3deb21] +description = "Test grepping multiples files at once -> Multiple files, several matches, inverted flag" + +[77b2eb07-2921-4ea0-8971-7636b44f5d29] +description = "Test grepping multiples files at once -> Multiple files, one match, match entire lines flag" + +[e53a2842-55bb-4078-9bb5-04ac38929989] +description = "Test grepping multiples files at once -> Multiple files, one match, multiple flags" + +[9c4f7f9a-a555-4e32-bb06-4b8f8869b2cb] +description = "Test grepping multiples files at once -> Multiple files, no matches, various flags" + +[ba5a540d-bffd-481b-bd0c-d9a30f225e01] +description = "Test grepping multiples files at once -> Multiple files, several matches, file flag takes precedence over line number flag" + +[ff406330-2f0b-4b17-9ee4-4b71c31dd6d2] +description = "Test grepping multiples files at once -> Multiple files, several matches, inverted and match entire lines flags" diff --git a/exercises/practice/grep/assets/iliad.txt b/exercises/practice/grep/assets/iliad.txt new file mode 100644 index 00000000..960ec6b9 --- /dev/null +++ b/exercises/practice/grep/assets/iliad.txt @@ -0,0 +1,9 @@ +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. \ No newline at end of file diff --git a/exercises/practice/grep/assets/midsummer-night.txt b/exercises/practice/grep/assets/midsummer-night.txt new file mode 100644 index 00000000..2c577050 --- /dev/null +++ b/exercises/practice/grep/assets/midsummer-night.txt @@ -0,0 +1,7 @@ +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. \ No newline at end of file diff --git a/exercises/practice/grep/assets/paradise-lost.txt b/exercises/practice/grep/assets/paradise-lost.txt new file mode 100644 index 00000000..2bdc17b0 --- /dev/null +++ b/exercises/practice/grep/assets/paradise-lost.txt @@ -0,0 +1,8 @@ +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 \ No newline at end of file diff --git a/exercises/practice/grep/spec/grep_spec.cr b/exercises/practice/grep/spec/grep_spec.cr new file mode 100644 index 00000000..d22f9001 --- /dev/null +++ b/exercises/practice/grep/spec/grep_spec.cr @@ -0,0 +1,285 @@ +require "spec" +require "../src/*" + +describe "Grep" do + it "One file, one match, no flags" do + expected = "Of Atreus, Agamemnon, King of men." + pattern = "Agamemnon" + flags = [] of String + files = ["iliad.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "One file, one match, print line numbers flag" do + expected = "2:Of that Forbidden Tree, whose mortal tast" + pattern = "Forbidden" + flags = ["-n"] + files = ["paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "One file, one match, case-insensitive flag" do + expected = "Of that Forbidden Tree, whose mortal tast" + pattern = "FORBIDDEN" + flags = ["-i"] + files = ["paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "One file, one match, print file names flag" do + expected = "paradise-lost.txt" + pattern = "Forbidden" + flags = ["-l"] + files = ["paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "One file, one match, match entire lines flag" do + expected = "With loss of Eden, till one greater Man" + pattern = "With loss of Eden, till one greater Man" + flags = ["-x"] + files = ["paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "One file, one match, multiple flags" do + expected = "9:Of Atreus, Agamemnon, King of men." + pattern = "OF ATREUS, Agamemnon, KIng of MEN." + flags = ["-n", "-i", "-x"] + files = ["iliad.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "One file, several matches, no flags" do + expected = "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," + pattern = "may" + flags = [] of String + files = ["midsummer-night.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "One file, several matches, print line numbers flag" do + expected = "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," + pattern = "may" + flags = ["-n"] + files = ["midsummer-night.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "One file, several matches, match entire lines flag" do + expected = "" + pattern = "may" + flags = ["-x"] + files = ["midsummer-night.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "One file, several matches, case-insensitive flag" do + expected = "Achilles sing, O Goddess! Peleus' son; +The noble Chief Achilles from the son" + pattern = "ACHILLES" + flags = ["-i"] + files = ["iliad.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "One file, several matches, inverted flag" do + expected = "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" + pattern = "Of" + flags = ["-v"] + files = ["paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "One file, no matches, various flags" do + expected = "" + pattern = "Gandalf" + flags = ["-n", "-l", "-x", "-i"] + files = ["iliad.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "One file, one match, file flag takes precedence over line flag" do + expected = "iliad.txt" + pattern = "ten" + flags = ["-n", "-l"] + files = ["iliad.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "One file, several matches, inverted and match entire lines flags" do + expected = "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." + pattern = "Illustrious into Ades premature," + flags = ["-x", "-v"] + files = ["iliad.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "Multiple files, one match, no flags" do + expected = "iliad.txt:Of Atreus, Agamemnon, King of men." + pattern = "Agamemnon" + flags = [] of String + files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "Multiple files, several matches, no flags" do + expected = "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," + pattern = "may" + flags = [] of String + files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "Multiple files, several matches, print line numbers flag" do + expected = "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" + pattern = "that" + flags = ["-n"] + files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "Multiple files, one match, print file names flag" do + expected = "iliad.txt +paradise-lost.txt" + pattern = "who" + flags = ["-l"] + files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "Multiple files, several matches, case-insensitive flag" do + expected = "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" + pattern = "TO" + flags = ["-i"] + files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "Multiple files, several matches, inverted flag" do + expected = "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." + pattern = "a" + flags = ["-v"] + files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "Multiple files, one match, match entire lines flag" do + expected = "midsummer-night.txt:But I beseech your grace that I may know" + pattern = "But I beseech your grace that I may know" + flags = ["-x"] + files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "Multiple files, one match, multiple flags" do + expected = "paradise-lost.txt:4:With loss of Eden, till one greater Man" + pattern = "WITH LOSS OF EDEN, TILL ONE GREATER MAN" + flags = ["-n", "-i", "-x"] + files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "Multiple files, no matches, various flags" do + expected = "" + pattern = "Frodo" + flags = ["-n", "-l", "-x", "-i"] + files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "Multiple files, several matches, file flag takes precedence over line number flag" do + expected = "iliad.txt +paradise-lost.txt" + pattern = "who" + flags = ["-n", "-l"] + files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end + + pending "Multiple files, several matches, inverted and match entire lines flags" do + expected = "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" + pattern = "Illustrious into Ades premature," + flags = ["-x", "-v"] + files = ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"] + + Grep.search(pattern, flags, files).should eq(expected) + end +end diff --git a/exercises/practice/grep/src/grep.cr b/exercises/practice/grep/src/grep.cr new file mode 100644 index 00000000..b5773b50 --- /dev/null +++ b/exercises/practice/grep/src/grep.cr @@ -0,0 +1,46 @@ +module Grep + def self.search(pattern : String, flags : Array(String), files : Array(String)) : String + # - `-n` Prepend the line number and a colon (':') to each line in the output, placing the number after the filename (if present). + # - `-l` Output only the names of the files that contain at least one matching line. + # - `-i` Match using a case-insensitive comparison. + # - `-v` Invert the program -- collect all lines that fail to match. + # - `-x` Search only for lines where the search string matches the entire line. + + results = [] of String + files.each do |file| + File.open("assets/#{file}") do |f| + f.each_line.with_index(1) do |line, line_number| + line_to_check = line.chomp + pattern_to_check = pattern.dup + + if flags.includes?("-i") + line_to_check = line_to_check.downcase + pattern_to_check = pattern_to_check.downcase + end + + is_match = if flags.includes?("-x") + line_to_check == pattern_to_check + else + line_to_check.includes?(pattern_to_check) + end + + is_match = !is_match if flags.includes?("-v") + + if is_match + if flags.includes?("-l") + results << file unless results.includes?(file) + break + else + result_line = "" + result_line += "#{file}:" if files.size > 1 && !flags.includes?("-l") + result_line += "#{line_number}:" if flags.includes?("-n") + result_line += line.chomp + results << result_line + end + end + end + end + end + results.join("\n") + end +end