いろいろなフォルダに散らばってるWordファイルを全部まるごと収集したい時ってありませんか。もちろん、OSの検索ツールなどを利用して検索できるのですが、まとめて特定のフォルダにコピーするというのは、なかなか骨の折れる作業です。そこで、今回は、Go言語を利用して、特定ファイルを収集してコピーするツールを作ってみます。

  • 今回は特定のファイルを収集するツールを作ってみます

    今回は特定のファイルを収集するツールを作ってみます

今回作成するツール - 任意ファイルを検索してコピーするツール

今回作成するのは、特定のパターンに合致するファイルを検索して、任意フォルダにコピーするプログラムです。このツールを作るのに必要なのは、ファイルをコピーする機能と、ファイルを検索する機能です。この二つの機能は、Go言語の標準ライブラリを使って実現できます。

そこで、ツールを作るのに際して、必要となるこの二つの関数の使い方を確認し、その後で、コマンドラインから使えるファイル収集ツールを完成されてみましょう。

ファイルをコピーする方法

最初にファイルをコピーする方法を確認しておきましょう。意外なことに、原稿執筆時点でGo言語には、ファイルのコピーを行うコマンドは存在しません。そのため、ファイルをコピーしたい時には、次のようにしてファイルの内容を読み込んで、ファイルへ書き込むという手順を自分で記述するしかありません。

以下のプログラムは、「aa.txt」から「bb.txt」へファイルコピーを行うものです。

package main
import (
    "io/ioutil"
)
func main() {
    infile := "aa.txt"
    outfile := "bb.txt"
    // ファイルを一度に読み込む --- (*1)
    b, err := ioutil.ReadFile(infile)
    if err != nil {
        panic(err)
    }
    // ファイルにデータを書き込む ---- (*2)
    err = ioutil.WriteFile(outfile, b, 0644)
    if err != nil {
        panic(err)
    }
}

プログラムを実行するには、上記プログラムを「copyfile.go」という名前で保存します。そして、コマンドラインから「go run copyfile.go」と入力すると、プログラムを実行できます。

上記のプログラムでは、(*1)の部分でファイルの内容全部をメモリに読み込みます。そして、(*2)の部分でファイルへ書き出します。ioutilのReadFileとWriteFileを利用することで、手軽にファイルの読み書きが可能です。ただし、このプログラムは、最も単純なファイルコピーの実装方法であり、巨大なサイズのファイルをコピーすることはできません。ファイルのコピーは、様々な手法が考えられるので、興味のある方は調べて見ると良いでしょう。

また、Go言語の特徴ですが、多くの関数では処理の実行が失敗した場合、error型のデータが返されます。そのため、ReadFileやWriteFileで処理が失敗したかどうかを確認するには、戻り値がnilかどうかを確認します。もし、nilであれば正しく実行できたことになり、nilでなければerror型のデータを確認し処理が失敗した理由を確かめることになります。

ファイルの一覧を取得する方法

次に、ファイルの一覧を取得する方法を見てみましょう。最初にカレントディレクトリにあるファイル一覧を取得してみます。

package main
import (
    "fmt"
    "log"
    "io/ioutil"
)
func main() {
    // カレントディレクトリのファイル一覧を得る
    files, err := ioutil.ReadDir(".")
    if err != nil {
        log.Fatal(err)
    }
    // 取得した一覧を表示
    for _, file := range files {
        fmt.Println(file.Name())
    }
}

上記のプログラムを「files.go」という名前で保存します。そして、コマンドラインから「go run files.go」を実行してみましょう。すると、カレントディレクトリにあるファイルの一覧を取得して表示します。

  • カレントディレクトリのファイル一覧が表示される

    カレントディレクトリのファイル一覧が表示される

指定ディレクトリにあるファイル一覧を取得するのが、ioutil.ReadDir関数です。引数にディレクトリを与えると、string型のスライスを返します。ちなみに、ファイルの一覧に関しては、前回詳しく解説しています。

特定のファイルをまるごと収集するツール

ここまでの部分で、Goの関数の使い方を確認できました。それでは、特定のファイルをまるごと収集するツールを作ってみましょう。以下のプログラムを「atumeru.go」という名前で保存しましょう。

package main
import (
    "os"
    "fmt"
    "log"
    "io/ioutil"
    "path/filepath"
    "regexp"
)

var pattern *regexp.Regexp // 検索パターン
var files = []string{} // 収集したファイル

func main() {
    // コマンドライン引数を得る --- (*1)
    if (len(os.Args) <= 2) {
        fmt.Println("[USAGE] atumeru (pattern) (target)")
        return
    }
    patternStr := os.Args[1]
    target := os.Args[2]
    curDir, _ := os.Getwd()
    // ファイル一覧を得る --- (*2)
    pattern = regexp.MustCompile(patternStr)
    searchFiles(target)
    // コピーを行う --- (*3)
    for _, path := range files {
      fmt.Println("Copy:" + path)
      bname := filepath.Base(path)
      savepath := filepath.Join(curDir, bname)
      copyFile(path, savepath)
    }
}
// 指定のディレクトリのファイル一覧を検索 --- (*4)
func searchFiles(target string) {
    fs, err := ioutil.ReadDir(target)
    if err != nil {
        log.Fatal(err)
    }
    for _, file := range fs {
        fpath := filepath.Join(target, file.Name())
        if file.IsDir() {
            // ディレクトリなら再帰的に検索
            searchFiles(fpath)
            continue
        }
        // パターンにマッチする? --- (*5)
        if pattern.MatchString(file.Name()) {
            files = append(files, fpath)
        }
    }
}
// ファイルをコピーする
func copyFile(infile, outfile string) bool {
    // ファイルを一度に読み込む
    b, err := ioutil.ReadFile(infile)
    if err != nil {
        return false
    }
    // ファイルにデータを書き込む
    err = ioutil.WriteFile(outfile, b, 0644)
    if err != nil {
        return false
    }
    return true
}

このプログラムは、ビルドして利用します。コマンドラインで「go build atumeru.go」を実行します。すると、実行ファイルが生成されます。

以下はカレントディレクトリ以下にある、Wordファイルを検索して、見つけたWordファイルをカレントディレクトリにコピーします。

(Windowsの場合)
.\atumeru "\.docx$" .




(macOSの場合)
./atumeru "\.docx$" .
  • プログラムを実行したところ

    プログラムを実行したところ

このツールの使い方ですが、コマンドラインから「./atumeru (正規表現パターン) (検索ディレクトリ)」の書式で利用します。

プログラムを見てみましょう。(*1)の部分では、コマンドライン引数を取得します。os.Argsには、コマンドライン引数が入っています。(*2)の部分では、正規表現パターンをコンパイルし、ファイルの一覧を取得します。そして、(*3)の部分でファイルの一覧を元にしてカレントディレクトリにそのファイルをコピーします。(*4)の部分ではファイルを任意パターンにマッチするファイルを検索します。ファイルの一覧を得て、ディレクトリであれば、そのディレクトリの中も検索するように再帰的にsearchFilesを呼び出します。(*5)の部分では、正規表現パターンにマッチするか調べて、マッチしていれば、検索結果に追加します。

まとめ

以上、今回は、コマンドラインから利用する、任意パターンのファイル収集ツールを作成してみました。Go言語はこうしたちょっとしたツールを作るのにも向いています。

ところで、今回のツールはまだまだ改良の余地が残っています。まず、収集したファイルに同名ファイルがある場合、上書きしてしまうので、ファイルが既にある場合は、ファイル名に数字をつけて上書きを回避する機能を追加するのも良いでしょう。また、ディレクトリの階層が深い場面では、検索に時間がかかってしまうので、検索する階層を指定できるようにオプションを追加するのも良いでしょう。

ツールを改良したり、自分のアイデアを付け加えたりしていくことで、ツールに対する愛情が増していきます。プログラミングの実力アップにもつながりますので、挑戦してみると良いでしょう。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ、2010年 OSS貢献者章受賞。技術書も多く執筆している。