Skip to content

Commit 7b718fc

Browse files
authored
Add grep (#51)
1 parent ad41bce commit 7b718fc

File tree

19 files changed

+848
-0
lines changed

19 files changed

+848
-0
lines changed

config.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,14 @@
368368
"practices": [],
369369
"prerequisites": [],
370370
"difficulty": 8
371+
},
372+
{
373+
"slug": "grep",
374+
"name": "Grep",
375+
"uuid": "f5f84a44-4e65-4598-811a-31766f68567c",
376+
"practices": [],
377+
"prerequisites": [],
378+
"difficulty": 8
371379
}
372380
]
373381
},
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Instructions append
2+
3+
## IO process
4+
5+
Unlike other exercises, `grep` requires interacting with the "external world": reading files, handling system errors and printing to `stdout`.
6+
7+
In Lean, interacting with the system is usually done in the [IO][io] monad, which allows for side effects to occur in a contained environment without affecting the pure state of most other functions.
8+
9+
## Reading arguments
10+
11+
Lean allows for a file to be called as a script, either directly as a standalone script via `lean --run` or with the use of `lake`.
12+
13+
A lean project has one point of entry, a function called `main`, that takes a `List String` with all arguments passed to it.
14+
In order to simulate running `Grep.lean` as an executable, arguments are passed in the same way in this exercise.
15+
16+
## Reading files
17+
18+
Lean offers [functions][files] to manipulate files in the `IO.FS` monad.
19+
For example, `IO.FS.readFile` may be used to read the contents of a file in a given path, as a `String`.
20+
21+
## Writing output
22+
23+
Instead of returning a value, this exercise requires results are written to standard output or standard error.
24+
There are [functions][console] in Lean for this purpose.
25+
26+
## Handling system errors
27+
28+
Unlike errors in `Except`, `IO` exceptions are actually runtime exceptions.
29+
You may use `try`/`catch` to handle them:
30+
31+
```
32+
try
33+
/-
34+
code that may possibly raise an exception
35+
-/
36+
catch msg =>
37+
/-
38+
code that is executed only if an exception was thrown
39+
-/
40+
```
41+
42+
[io]: https://lean-lang.org/doc/reference/latest/IO/#io
43+
[files]: https://lean-lang.org/doc/reference/latest/IO/Files___-File-Handles___-and-Streams/#The-Lean-Language-Reference--IO--Files___-File-Handles___-and-Streams
44+
[console]: https://lean-lang.org/doc/reference/latest/IO/Console-Output/#The-Lean-Language-Reference--IO--Console-Output
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Instructions
2+
3+
Search files for lines matching a search string and return all matching lines.
4+
5+
The Unix [`grep`][grep] command searches files for lines that match a regular expression.
6+
Your task is to implement a simplified `grep` command, which supports searching for fixed strings.
7+
8+
The `grep` command takes three arguments:
9+
10+
1. The string to search for.
11+
2. Zero or more flags for customizing the command's behavior.
12+
3. One or more files to search in.
13+
14+
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.
15+
When searching in multiple files, each matching line is prepended by the file name and a colon (':').
16+
17+
## Flags
18+
19+
The `grep` command supports the following flags:
20+
21+
- `-n` Prepend the line number and a colon (':') to each line in the output, placing the number after the filename (if present).
22+
- `-l` Output only the names of the files that contain at least one matching line.
23+
- `-i` Match using a case-insensitive comparison.
24+
- `-v` Invert the program -- collect all lines that fail to match.
25+
- `-x` Search only for lines where the search string matches the entire line.
26+
27+
[grep]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/grep.html
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import Std
2+
3+
namespace Grep
4+
5+
def aggregateResults (results : List String) (prepend : String := "") : String :=
6+
results.foldl (fun acc x => (prepend ++ x ++ "\n" ++ acc).trim) ""
7+
8+
def processFile
9+
(check : String -> Bool)
10+
(getResult : Nat -> String -> String -> String)
11+
(file : String) : IO (Except String (List String)) := do
12+
try
13+
let fileContent ← IO.FS.readFile file
14+
let mut results := []
15+
let lines := fileContent.splitOn "\n" |> (·.toArray)
16+
for idx in [:lines.size] do
17+
let line := lines[idx]!
18+
if check line then
19+
results := (getResult (idx + 1) line file) :: results
20+
return (.ok results)
21+
catch e =>
22+
return (.error e.toString)
23+
24+
def processGrep
25+
(pattern : String)
26+
(flags : List String)
27+
(files : List String) : IO Unit := do
28+
let flagsSet := Std.HashSet.ofList flags
29+
let adjustedPattern :=
30+
if flagsSet.contains "-i" then
31+
pattern.toLower
32+
else
33+
pattern
34+
let formatLine :=
35+
if flagsSet.contains "-i" then
36+
fun line => String.toLower line
37+
else
38+
fun line => line
39+
let getResult :=
40+
if flagsSet.contains "-n" then
41+
fun (idx : Nat) (line : String) _ => s!"{idx}:{line}"
42+
else
43+
fun _ line _ => line
44+
let compare :=
45+
if flagsSet.contains "-x" then
46+
fun (line : String) => s!"{formatLine line}" == adjustedPattern
47+
else
48+
fun (line : String) => formatLine line |> (·.toSlice.contains adjustedPattern)
49+
let check :=
50+
if flagsSet.contains "-v" then
51+
fun line => !(compare line)
52+
else
53+
compare
54+
match files with
55+
| [] => return
56+
| file :: [] =>
57+
let maybeResults <- processFile check getResult file
58+
match maybeResults with
59+
| .error _ =>
60+
IO.eprintln "File not found"
61+
| .ok results =>
62+
let validResults := results.filter (·!="")
63+
if validResults.isEmpty then
64+
return
65+
else if flagsSet.contains "-l" then
66+
IO.println file
67+
else
68+
IO.println (aggregateResults validResults)
69+
| _ =>
70+
for file in files do
71+
let maybeResults <- processFile check getResult file
72+
match maybeResults with
73+
| .error _ =>
74+
IO.eprintln "File not found"
75+
| .ok results =>
76+
let validResults := results.filter (·!="")
77+
if validResults.isEmpty then
78+
continue
79+
else if flagsSet.contains "-l" then
80+
IO.println file
81+
else
82+
IO.println (aggregateResults validResults s!"{file}:")
83+
84+
def grep (args : List String) : IO Unit := do
85+
match args with
86+
| [] => IO.eprintln "Called without arguments"
87+
| _ :: [] => IO.eprintln "Called without a file name"
88+
| pattern :: xs =>
89+
let flags := xs.takeWhile (fun flag => flag.startsWith "-")
90+
let files := xs.dropWhile (fun flag => flag.startsWith "-")
91+
processGrep pattern flags files
92+
93+
end Grep
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"authors": [
3+
"oxe-i"
4+
],
5+
"files": {
6+
"solution": [
7+
"Grep.lean"
8+
],
9+
"test": [
10+
"GrepTest.lean"
11+
],
12+
"example": [
13+
".meta/Example.lean"
14+
]
15+
},
16+
"blurb": "Search a file for lines matching a regular expression pattern. Return the line number and contents of each matching line.",
17+
"source": "Conversation with Nate Foster.",
18+
"source_url": "https://www.cs.cornell.edu/Courses/cs3110/2014sp/hw/0/ps0.pdf"
19+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[
2+
{
3+
"description": "Test error -> Called without arguments",
4+
"property": "grep",
5+
"input": {
6+
"pattern": "",
7+
"flags": [],
8+
"files": []
9+
},
10+
"expected": {
11+
"error": "Called without arguments"
12+
}
13+
},
14+
{
15+
"description": "Test error -> Called without a file",
16+
"property": "grep",
17+
"input": {
18+
"pattern": "who",
19+
"flags": [],
20+
"files": []
21+
},
22+
"expected": {
23+
"error": "Called without a file name"
24+
}
25+
},
26+
{
27+
"description": "Test error -> File not found",
28+
"property": "grep",
29+
"input": {
30+
"pattern": "Agamemnon",
31+
"flags": [],
32+
"files": ["odyssey.txt"]
33+
},
34+
"expected": {
35+
"error": "File not found"
36+
}
37+
}
38+
]
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# This is an auto-generated file.
2+
#
3+
# Regenerating this file via `configlet sync` will:
4+
# - Recreate every `description` key/value pair
5+
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
6+
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
7+
# - Preserve any other key/value pair
8+
#
9+
# As user-added comments (using the # character) will be removed when this file
10+
# is regenerated, comments can be added via a `comment` key.
11+
12+
[9049fdfd-53a7-4480-a390-375203837d09]
13+
description = "Test grepping a single file -> One file, one match, no flags"
14+
15+
[76519cce-98e3-46cd-b287-aac31b1d77d6]
16+
description = "Test grepping a single file -> One file, one match, print line numbers flag"
17+
18+
[af0b6d3c-e0e8-475e-a112-c0fc10a1eb30]
19+
description = "Test grepping a single file -> One file, one match, case-insensitive flag"
20+
21+
[ff7af839-d1b8-4856-a53e-99283579b672]
22+
description = "Test grepping a single file -> One file, one match, print file names flag"
23+
24+
[8625238a-720c-4a16-81f2-924ec8e222cb]
25+
description = "Test grepping a single file -> One file, one match, match entire lines flag"
26+
27+
[2a6266b3-a60f-475c-a5f5-f5008a717d3e]
28+
description = "Test grepping a single file -> One file, one match, multiple flags"
29+
30+
[842222da-32e8-4646-89df-0d38220f77a1]
31+
description = "Test grepping a single file -> One file, several matches, no flags"
32+
33+
[4d84f45f-a1d8-4c2e-a00e-0b292233828c]
34+
description = "Test grepping a single file -> One file, several matches, print line numbers flag"
35+
36+
[0a483b66-315b-45f5-bc85-3ce353a22539]
37+
description = "Test grepping a single file -> One file, several matches, match entire lines flag"
38+
39+
[3d2ca86a-edd7-494c-8938-8eeed1c61cfa]
40+
description = "Test grepping a single file -> One file, several matches, case-insensitive flag"
41+
42+
[1f52001f-f224-4521-9456-11120cad4432]
43+
description = "Test grepping a single file -> One file, several matches, inverted flag"
44+
45+
[7a6ede7f-7dd5-4364-8bf8-0697c53a09fe]
46+
description = "Test grepping a single file -> One file, no matches, various flags"
47+
48+
[3d3dfc23-8f2a-4e34-abd6-7b7d140291dc]
49+
description = "Test grepping a single file -> One file, one match, file flag takes precedence over line flag"
50+
51+
[87b21b24-b788-4d6e-a68b-7afe9ca141fe]
52+
description = "Test grepping a single file -> One file, several matches, inverted and match entire lines flags"
53+
54+
[ba496a23-6149-41c6-a027-28064ed533e5]
55+
description = "Test grepping multiples files at once -> Multiple files, one match, no flags"
56+
57+
[4539bd36-6daa-4bc3-8e45-051f69f5aa95]
58+
description = "Test grepping multiples files at once -> Multiple files, several matches, no flags"
59+
60+
[9fb4cc67-78e2-4761-8e6b-a4b57aba1938]
61+
description = "Test grepping multiples files at once -> Multiple files, several matches, print line numbers flag"
62+
63+
[aeee1ef3-93c7-4cd5-af10-876f8c9ccc73]
64+
description = "Test grepping multiples files at once -> Multiple files, one match, print file names flag"
65+
66+
[d69f3606-7d15-4ddf-89ae-01df198e6b6c]
67+
description = "Test grepping multiples files at once -> Multiple files, several matches, case-insensitive flag"
68+
69+
[82ef739d-6701-4086-b911-007d1a3deb21]
70+
description = "Test grepping multiples files at once -> Multiple files, several matches, inverted flag"
71+
72+
[77b2eb07-2921-4ea0-8971-7636b44f5d29]
73+
description = "Test grepping multiples files at once -> Multiple files, one match, match entire lines flag"
74+
75+
[e53a2842-55bb-4078-9bb5-04ac38929989]
76+
description = "Test grepping multiples files at once -> Multiple files, one match, multiple flags"
77+
78+
[9c4f7f9a-a555-4e32-bb06-4b8f8869b2cb]
79+
description = "Test grepping multiples files at once -> Multiple files, no matches, various flags"
80+
81+
[ba5a540d-bffd-481b-bd0c-d9a30f225e01]
82+
description = "Test grepping multiples files at once -> Multiple files, several matches, file flag takes precedence over line number flag"
83+
84+
[ff406330-2f0b-4b17-9ee4-4b71c31dd6d2]
85+
description = "Test grepping multiples files at once -> Multiple files, several matches, inverted and match entire lines flags"

exercises/practice/grep/Grep.lean

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Grep
2+
3+
def grep (args : List String) : IO Unit :=
4+
sorry
5+
6+
end Grep

0 commit comments

Comments
 (0)