[Go] grepコマンドを一部実装してみる

# はじめに

前回は、`ls`コマンドの一部実装に取り組んでみました。今回は、`grep`コマンドの一部実装に取り組んでみます。
目標としては、
1. 検索文字列が出現する行を出力し、その検索文字列は区別できるように色づけてあげること
2. `-n`オプションで出現位置の行数も出力するようにすること 

の2つを設定したいと思います。

前回の記事はこちらから

https://qiita.com/taga3_dev/items/1dd46c3593d1a414dc32


# 実装
今回のリポジトリですmm

https://github.com/ayanami77/go-my-commands/blob/main/my-grep/main.go

まず、1つ目の「 `my-grep <target> sample.txt`で検索文字列が出現する行を出力すること」を実装してみます。ポイントは、`Scanner`を用いて、一行ずつテキストを読み込み、対象の文字列が含まれているか走査している点です。また、対象文字列の色付けには`fatih/color`というライブラリを拝借させていただきました。

```main.go
package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"

	"github.com/fatih/color"
)

type Line struct {
	Content  string
}

func main() {
	targetString := os.Args[1]
	targetFile := os.Args[2]

	file, err := os.Open(targetFile)
	if err != nil {
		panic("Error opening file")
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)

	matchedLines := []Line{}

	for scanner.Scan() {
		if strings.Contains(scanner.Text(), targetString) {
			matchedLines = append(matchedLines, Line{Content: scanner.Text()})
		}
	}

	listLines(matchedLines, targetString)
}

func listLines(lines []Line, target string) {
	for _, line := range lines {
		fmt.Println(reconstructLine(line.Content, target))
	}
}

func reconstructLine(line string, target string) string {
	green := color.New(color.FgGreen).SprintFunc()
	return strings.ReplaceAll(line, target, green(target))
}
```

検証用のテキストファイルの内容を示しておきます。テキストはジャレド・ダイアモンドの「銃・病原菌・鉄」からの引用です。

```sample.txt
"History followed different courses for
different peoples because of differences among peoples' environments,
not because of biological differences among peoples themselves"
― Jared Diamond, Guns, Germs and Steel: The Fates of Human Societies
```

では、実行してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2816396/1f23e364-8698-7d63-9d0b-42d8831845e2.png)

`peoples`という文字列の出現した行がきちんと出力されています！

では、目標の2つ目の`-n`オプションも実装してみます。内容としては、`flag`ライブラリを利用して、コマンドライン引数を受け取るようにし、あとは単純にループで行数のインクリメントを行っています。

```diff_go: main.go
package main

import (
	"bufio"
	"flag"
	"fmt"
	"os"
	"strings"

	"github.com/fatih/color"
)

type Line struct {
	Content  string
+   Position int
}

func main() {
+	isLineNumberCountingEnabled := flag.Bool("n", false, "Count lines")
+	flag.Parse()

+	targetString := flag.Arg(0)
+	targetFile := flag.Arg(1)
-   targetString := os.Args[1]
-	targetFile := os.Args[2]	

	file, err := os.Open(targetFile)
	if err != nil {
		panic("Error opening file")
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)

	matchedLines := []Line{}
+	currentLineNumber := 1

	for scanner.Scan() {
		if strings.Contains(scanner.Text(), targetString) {
-  			matchedLines = append(matchedLines, Line{Content: scanner.Text()})
+			matchedLines = append(matchedLines, Line{Content: scanner.Text(), Position: currentLineNumber})
		}
		currentLineNumber++
	}

+	if *isLineNumberCountingEnabled {
+		listLinesWithLineNumber(matchedLines, targetString)
+		return
+	}

	listLines(matchedLines, targetString)
}

func listLines(lines []Line, target string) {
	for _, line := range lines {
		fmt.Println(reconstructLine(line.Content, target))
	}
}

+func listLinesWithLineNumber(lines []Line, target string) {
+	for _, line := range lines {
+		fmt.Printf("%d: %s\n", line.Position, reconstructLine(line.Content, target))
+	}
+}

func reconstructLine(line string, target string) string {
	green := color.New(color.FgGreen).SprintFunc()
	return strings.ReplaceAll(line, target, green(target))
}
```

では、こちらも実行してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2816396/3547b095-4e61-9b17-d180-9c424d8f812b.png)

無事、`among`という単語が行数と共に出力されていることが分かります！

# おわりに
今回は、`grep`コマンドの一部実装をしてみました。次回はlinuxの標準コマンドではありませんが、`tree`コマンドの実装にチャレンジしてみます！