golangの日記

Go言語を中心にプログラミングについてのブログ

Go言語(golang)で簡単なファイル検索プログラム

golang.png


初学者向け。golangで簡単なファイル検索プログラムの作り方


プログラミングを始めると何かしらメモしたりするので、
それをコマンドラインで検索してファイルを開くためのプログラムです。





package main

import (
    "bufio"
    "errors"
    "fmt"
    "os"
    "os/exec"
    "path/filepath"
    "regexp"
    "strconv"
    "strings"
)

const (
    // 検索対象のパス。検索したいフォルダのパスを指定する
    Root = `C:\Users\[user name]\Desktop`
)

var (
    // マッチしたファイルを追加するためのスライス
    files []string

    // 検索ワードを正規表現化して入れておく変数
    re    *regexp.Regexp

    // 実行するコマンド。notepadでファイルを開く
    command = []string{"cmd.exe", "/C", "notepad.exe"}
)

func init() {
    // os.Args[0] は、実行ファイルなのでそれ以降を検索文字としてAND検索にする
    words := strings.Join(os.Args[1:], ".*")

    // 正規表現としてコンパイルする
    var err error
    re, err = regexp.Compile(words)
    if err != nil {
        fmt.Fprintf(os.Stderr, "error: %s\n", err)
        os.Exit(1)
    }
}

// filepath.Walk に渡す関数
func visit(path string, info os.FileInfo, err error) error {
    if err != nil {
        return err
    }

    // フォルダ以外を正規表現でマッチングする
    if !info.IsDir() && re.MatchString(info.Name()) {
        // マッチしたファイルのみ files に追加する
        files = append(files, path)
    }

    return nil
}

var (
    // エラーではなく、そのまま終了させるための無視して良いエラー
    ignorableErr = errors.New("ignorable error")
)

// 改行と復帰(CRLF)を削除する
func trimfunc(r rune) bool {
    switch r {
    case '\n', '\r', ' ':
        return true
    default:
        return false
    }
}

func prompt() (int, error) {
    // 検索にマッチしたファイル名を番号付きで表示する
    for i, v := range files {
        fmt.Printf("%d: %s\n", i, filepath.Base(v))
    }

    // ファイル番号の入力を求める
    r := bufio.NewReader(os.Stdin)
    fmt.Printf("[INPUT FILE NUMBER]: ")

    line, err := r.ReadString('\n')
    if err != nil {
        return 0, err
    }

    // CRLFを削除して数字のみ取り出す
    input := strings.TrimFunc(line, trimfunc)

    // 数字を型変換する
    index, err := strconv.Atoi(input)
    if err != nil {
        return 0, ignorableErr
    }

    // スライス(files)の範囲内かどうか確認する
    if index < 0 || index >= len(files) {
        return 0, fmt.Errorf("out of range %d", index)
    }

    return index, nil
}

func search() (int, error) {
    // フォルダをたどる
    err := filepath.Walk(Root, visit)
    if err != nil {
        return 1, err
    }

    // マッチしたファイルがなければ終了する
    if len(files) == 0 {
        return 1, fmt.Errorf("file not matched")
    }

    // 入力を受け取る
    index, err := prompt()
    if err != nil {
        // 無視してよいエラーの場合は return でそのまま終了する
        if err == ignorableErr {
            return 0, nil
        }
        return 1, err
    }

    // コマンドを実行してファイルを notepad で開く
    c := exec.Command(command[0], append(command[1:], files[index])...)
    // Start は、コマンドの終了を待たないので、このプログラムをそのまま終わらせる
    if err := c.Start(); err != nil {
        return 1, err
    }

    return 0, nil
}

func main() {
    exitcode, err := search()
    if err != nil {
        fmt.Fprintf(os.Stderr, "error: %s\n", err)
    }
    os.Exit(exitcode)
}


コンパイル(findfiles.exe の findfiles は好きな名前に変更できる)

PS> go build -o findfiles.exe main.go
PS> ls
    findfiles.exe
    main.go


実行(ファイル名の左にある数字を入力してENTERキーを押下すれば notepad で開くことができる) 以下は、拡張子が、 .txt のファイルを検索してます。

PS> ./findfiles.exe \.txt
    0: foo.txt
    1: bar.txt
    2: baz.txt
    [INPUT FILE NUMBER]:

後は、パスの通ったフォルダに、実行ファイル(.exe) を置いてどこからでも実行できるようにします。


改良ポイント

  • JSONやYAMLで書かれた設定ファイルを読み込めるようにする
    • 対象とするルートフォルダのパスを変更できるようにする。
    • ルートフォルダ以下で検索対象から除外するフォルダを指定する。
    • 編集するエディタを変更できるようにする
  • flagパッケージを使ってコマンドラインオプションを設定する
    • 通常はmoreコマンドなどでpowershell上で表示し、 --edit オプションが指定されたときのみエディターで開くなど
    • AND検索とOR検索の切り替えや除外するワードを指定できるようにする
  • フォルダを再帰的に辿る(filepath.Walk)部分を高速化する
  • ファイルの絞り込みや選択に fzfpeco を使う

など