» Make grep CLI App in Go » 2. Development » 2.3 Add Functionalities

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, greps 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