Add Functionalities
You need to support these functionalities as shown in the project objectives section:
-c, --count
-i, --ignore-case
-n, --line-number
-r, --recursive
-v, --invert-match
main.go:
package main
import (
"flag"
"fmt"
"log"
"os"
"github.com/Literank/gorep/pkg/grep"
)
func main() {
// Set custom usage message
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [options] pattern file_path\n", os.Args[0])
fmt.Println("Options:")
flag.PrintDefaults()
}
// Optional flags
countFlag := flag.Bool("c", false, "count\nOnly a count of selected lines is written to standard output.")
ignoreCaseFlag := flag.Bool("i", false, "ignore-case\nPerform case insensitive matching. By default, it is case sensitive.")
lineNumberFlag := flag.Bool("n", false, "line-number\nEach output line is preceded by its relative line number in the file, starting at line 1. This option is ignored if -count is specified.")
recursiveFlag := flag.Bool("r", false, "recursive\nRecursively search subdirectories listed.")
invertMatchFlag := flag.Bool("v", false, "invert-match\nSelected lines are those not matching any of the specified patterns.")
flag.Parse()
// Retrieve positional arguments
// pattern - The pattern to search for
// file_path - The path to the file to search in
args := flag.Args()
if len(args) < 2 {
fmt.Println("Both pattern and file_path are required.")
flag.Usage()
os.Exit(0)
}
pattern, filePath := args[0], args[1]
options := &grep.Options{}
if *ignoreCaseFlag {
options.IgnoreCase = true
}
if *invertMatchFlag {
options.InvertMatch = true
}
var result grep.MatchResult
var err error
if *recursiveFlag {
result, err = grep.GrepRecursive(pattern, filePath, options)
if err != nil {
log.Fatal("Failed to do recursive grep, error:", err)
}
} else {
result, err = grep.Grep(pattern, filePath, options)
if err != nil {
log.Fatal("Failed to grep, error:", err)
}
}
if *countFlag {
fmt.Println(grep.GrepCount(result))
} else {
printResult(result, *lineNumberFlag)
}
}
func printResult(result grep.MatchResult, lineNumberOption bool) {
currentFile := ""
fileCount := len(result)
for filePath, items := range result {
for _, item := range items {
if fileCount > 1 && filePath != currentFile {
currentFile = filePath
fmt.Printf("\n%s:\n", filePath)
}
if lineNumberOption {
fmt.Printf("%d: %s\n", item.LineNumber, item.Line)
} else {
fmt.Println(item.Line)
}
}
}
}
flag
adds all the optional arguments into the app. Function printResult
parses the MatchResult
and prints the filePath
and lineNumber
if needed.
pkg/grep/search.go:
package grep
import (
"bufio"
"os"
"path/filepath"
"regexp"
"strings"
)
// MatchItem represents a match in grep searching
type MatchItem struct {
LineNumber int
Line string
}
// MatchResult represents all matches of all files of a grep search
type MatchResult = map[string][]*MatchItem
// Options struct represents the control options of a grep search
type Options struct {
CountOnly bool
IgnoreCase bool
InvertMatch bool
}
func Grep(pattern string, filePath string, options *Options) (MatchResult, error) {
lines, err := readFileLines(filePath)
if err != nil {
return nil, err
}
var matchingLines []*MatchItem
patternRegex, err := regexp.Compile(pattern)
if err != nil {
return nil, err
}
// normal grep
if options == nil {
matchingLines = filterLines(patternRegex, lines, true)
} else {
if options.IgnoreCase {
patternRegex, err = regexp.Compile("(?i)" + pattern)
if err != nil {
return nil, err
}
}
if options.InvertMatch {
matchingLines = filterLines(patternRegex, lines, false)
} else {
matchingLines = filterLines(patternRegex, lines, true)
}
}
return MatchResult{filePath: matchingLines}, nil
}
func GrepCount(result MatchResult) int {
count := 0
for _, v := range result {
count += len(v)
}
return count
}
func GrepRecursive(pattern string, directoryPath string, options *Options) (MatchResult, error) {
results := make(MatchResult)
filepath.Walk(directoryPath, func(filePath string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if !info.IsDir() {
result, grepErr := Grep(pattern, filePath, options)
if grepErr != nil {
return grepErr
}
results[filePath] = result[filePath]
}
return nil
})
return results, nil
}
func readFileLines(filePath string) ([]string, error) {
// Open the file
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
// Create a scanner to read the file line by line
scanner := bufio.NewScanner(file)
var lines []string
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, err
}
return lines, nil
}
func filterLines(pattern *regexp.Regexp, lines []string, flag bool) []*MatchItem {
var filteredLines []*MatchItem
for lineNumber, line := range lines {
if flag == pattern.MatchString(line) {
filteredLines = append(filteredLines, &MatchItem{lineNumber + 1, strings.TrimLeft(line, " \t")})
}
}
return filteredLines
}
Function Grep
adds logic for "case-insensitive match" and "invert match."
Function GrepRecursive
lists all files recursively, grep
s them, and puts the results into the MatchResult
.
Run the normal way:
go run main.go result main.go
Result lines:
var result grep.MatchResult
result, err = grep.GrepRecursive(pattern, filePath, options)
result, err = grep.Grep(pattern, filePath, options)
fmt.Println(grep.GrepCount(result))
printResult(result, *lineNumberFlag)
func printResult(result grep.MatchResult, lineNumberOption bool) {
fileCount := len(result)
for filePath, items := range result {
Do the count:
go run main.go -c result main.go
Result number is: 8.
Show the line numbers:
go run main.go -n result main.go
Result lines:
48: var result grep.MatchResult
52: result, err = grep.GrepRecursive(pattern, filePath, options)
57: result, err = grep.Grep(pattern, filePath, options)
64: fmt.Println(grep.GrepCount(result))
66: printResult(result, *lineNumberFlag)
70: func printResult(result grep.MatchResult, lineNumberOption bool) {
72: fileCount := len(result)
74: for filePath, items := range result {
Use regex pattern
go run main.go -n "\br[a-z]+t" main.go
Result lines:
23: lineNumberFlag := flag.Bool("n", false, "line-number\nEach output line is preceded by its relative line number in the file, starting at line 1. This option is ignored if -count is specified.")
48: var result grep.MatchResult
52: result, err = grep.GrepRecursive(pattern, filePath, options)
57: result, err = grep.Grep(pattern, filePath, options)
64: fmt.Println(grep.GrepCount(result))
66: printResult(result, *lineNumberFlag)
70: func printResult(result grep.MatchResult, lineNumberOption bool) {
72: fileCount := len(result)
74: for filePath, items := range result {
Do the invert match:
go run main.go -v -n result main.go
Result is something like this:
1: package main
2:
3: import (
4: "flag"
5: "fmt"
6: "log"
7: "os"
...
19:
20: // Optional flags
21: countFlag := flag.Bool("c", false, "count\nOnly a count of selected lines is written to standard output.")
22: ignoreCaseFlag := flag.Bool("i", false, "ignore-case\nPerform case insensitive matching. By default, it is case sensitive.")
23: lineNumberFlag := flag.Bool("n", false, "line-number\nEach output line is preceded by its relative line number in the file, starting at line 1. This option is ignored if -count is specified.")
24: recursiveFlag := flag.Bool("r", false, "recursive\nRecursively search subdirectories listed.")
25: invertMatchFlag := flag.Bool("v", false, "invert-match\nSelected lines are those not matching any of the specified patterns.")
...
80: if lineNumberOption {
81: fmt.Printf("%d: %s\n", item.LineNumber, item.Line)
82: } else {
83: fmt.Println(item.Line)
...
Do the case insensitive match:
go run main.go -i only main.go
Result is:
countFlag := flag.Bool("c", false, "count\nOnly a count of selected lines is written to standard output.")
Do the big recursive match:
go run main.go -r count .
Result lines should be like:
main.go:
countFlag := flag.Bool("c", false, "count\nOnly a count of selected lines is written to standard output.")
lineNumberFlag := flag.Bool("n", false, "line-number\nEach output line is preceded by its relative line number in the file, starting at line 1. This option is ignored if -count is specified.")
if *countFlag {
pkg/grep/search.go:
count := 0
count += len(v)
return count