【Go】簡易 fuzzylauncher

以前の記事： [【PowerShell】 peco を使った簡易ランチャ](https://qiita.com/AWtnb/items/d2842d86c5482832daa5)

最初はうまく動作してはしゃいでていたものの、なんとなくモッサリした動作が気になってきた今日この頃。勉強も兼ねて go で書き直してみました。

最終的にこのような感じになります。
![202006262125035.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501098/55b6b3de-b2e1-309c-2f11-4b352eca9308.png)


### 制作過程
まずは `Test-Path` に相当するファイルの有無を確認する関数。

```golang
func isVaridPath(filename string) bool {
	_, err := os.Stat(filename)
	return err == nil
}
```

次は `Get-Content` の代わりのテキストファイル読み取り関数。
先に `make` しておくとベターとの記事を見かけたので組み込んでますが容量はどの値を指定すればいいのでしょうか……（勉強不足）。とりあえずキリの良い100で。

```golang
func readFile(filePath string) []string {
	f, err := os.Open(filePath)
	if err != nil {
		panic(err)
	}
	defer f.Close()

	lines := make([]string, 0, 100)
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		lines = append(lines, scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		panic(err)
	}

	return lines
}
```

苦労したのは `Get-Childitem -depth` を go で実装する処理。
最初は `filepath.Walk` を使っていたのですが、 [godirwalk](https://github.com/karrick/godirwalk) というライブラリが探索の速さを謳っていたので我流で組み込んでいます。
こちらも容量の値はよく理解しないまま1000を指定。理由はテキストファイルから読み込むよりも確実にスライスが長くなるからです。そしてポインタにはまだ理解が及んでいないので写経状態。

```golang
func dirWalkDepth(dirname string, depth int) []string {
	dirs := make([]string, 0, 1000)
	err := godirwalk.Walk(dirname, &godirwalk.Options{
		FollowSymbolicLinks: false,
		Callback: func(osPathname string, de *godirwalk.Dirent) error {
			rel, _ := filepath.Rel(dirname, osPathname)
			if strings.Count(rel, `\`) >= depth {
				return filepath.SkipDir
			}
			if de.IsDir() {
				if strings.HasPrefix(de.Name(), ".") {
					return filepath.SkipDir
				}
				dirs = append(dirs, osPathname)
			}
			return nil
		},
		ErrorCallback: func(osPathname string, err error) godirwalk.ErrorAction {
			fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
			return godirwalk.SkipNode
		},
	})
	if err != nil {
		panic(err)
	}
	return dirs
}
```

最後はメインのフィルタリング処理。[ライブラリとしての fuzzy-finder](https://github.com/ktr0731/go-fuzzyfinder) が提供されていました。大変ありがたいことです。

実装はドキュメントのサンプルをほぼコピペ。
`fuzzyfinder.Find` で呼んだ関数の戻り値が実際に表示されるようなので、スライスの要素に `|` が含まれていればその後ろ、そうでなければ `filepath.Base` でパス末端を取得します。 Escape や Ctrl-C でキャンセルされた場合は `err` が戻ります。

```golang
	idx, err := fuzzyfinder.Find(rootSlice, func(i int) string {
		sliceItem := rootSlice[i]
		if strings.Contains(sliceItem, "|") {
			return strings.Split(sliceItem, "|")[1]
		}
		return filepath.Base(sliceItem)
	})
	if err != nil {
		return
	}
```


### コード全体

```fuzzylauncher.go
package main

import (
	"bufio"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strings"

	"github.com/karrick/godirwalk"
	"github.com/ktr0731/go-fuzzyfinder"
)

const (
	dataPath = `C:\Personal\launch.txt`
)

func main() {
	if !isVaridPath(dataPath) {
		fmt.Println("cannot find data file...")
		return
	}
	rootSlice := []string{}
	for _, p := range readFile(dataPath) {
		if isVaridPath(strings.Split(p, "|")[0]) {
			rootSlice = append(rootSlice, p)
		}
	}
	sort.Slice(rootSlice, func(i, j int) bool {
		return filepath.Base(rootSlice[i]) < filepath.Base(rootSlice[j])
	})
	idx, err := fuzzyfinder.Find(rootSlice, func(i int) string {
		sliceItem := rootSlice[i]
		if strings.Contains(sliceItem, "|") {
			return strings.Split(sliceItem, "|")[1]
		}
		return filepath.Base(sliceItem)
	})
	if err != nil {
		return
	}
	src1 := strings.Split(rootSlice[idx], "|")[0]
	if f, _ := os.Stat(src1); !f.IsDir() {
		exec.Command("cmd", "/c", "start", "", src1).Start()
		return
	}
	subDir := dirWalkDepth(src1, 4)
	src2 := subDir[0]
	if len(subDir) > 1 {
		idx, err := fuzzyfinder.Find(subDir, func(i int) string {
			relPath, _ := filepath.Rel(src1, subDir[i])
			return relPath
		})
		if err != nil {
			return
		}
		src2 = subDir[idx]
	}
	exec.Command("explorer.exe", src2).Start()
}

func isVaridPath(filename string) bool {
	_, err := os.Stat(filename)
	return err == nil
}

func readFile(filePath string) []string {
	f, err := os.Open(filePath)
	if err != nil {
		panic(err)
	}
	defer f.Close()

	lines := make([]string, 0, 100)
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		lines = append(lines, scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		panic(err)
	}

	return lines
}

func dirWalkDepth(dirname string, depth int) []string {
	dirs := make([]string, 0, 1000)
	err := godirwalk.Walk(dirname, &godirwalk.Options{
		FollowSymbolicLinks: false,
		Callback: func(osPathname string, de *godirwalk.Dirent) error {
			rel, _ := filepath.Rel(dirname, osPathname)
			if strings.Count(rel, `\`) >= depth {
				return filepath.SkipDir
			}
			if de.IsDir() {
				if strings.HasPrefix(de.Name(), ".") {
					return filepath.SkipDir
				}
				dirs = append(dirs, osPathname)
			}
			return nil
		},
		ErrorCallback: func(osPathname string, err error) godirwalk.ErrorAction {
			fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
			return godirwalk.SkipNode
		},
	})
	if err != nil {
		panic(err)
	}
	return dirs
}
```



### 感想
コピー＆ペーストでできたフランケンシュタインの怪物のようなコードですが動いているので満足です。
はじめての go でした。コンパイルが通りさえすれば大抵は問題なく動作するというのは安心感がありますね。