Juliaでファイル名からパスを検索する関数を作る

##はじめに

どうも、こんにちは。
今回が初めての投稿です。

元々Rを使っていましたが、Juliaに手を出して以来完全にハマってしまっています。Rではデータ分析を主にしていました。ただ、データ分析の際にファイルの読み込もうと思っても、ファイルを移動させていた場合なんかはいちいち設定が面倒くさいんですよね。そこで、ファイル名からそのファイルのあるディレクトリのパスを返す関数があると便利なのでJuliaに移行する際にそれらを自作しました。

今回はその関数の紹介をさせていただきます。
##関数を定義するよ

やることは単純で、とりあえず全ディレクトリにあるファイル名を取得します。次に指定したワードをファイル名に含んだファイルが存在するディレクトリのパスを返すだけです。とはいっても全ディレクトリ探すのも無駄なので、スタートするディレクトリを設定できるようにします。

まとめると、
1. スタートするディレクトリを設定する
2. ディレクトリ内にあるファイル及びサブディレクトリを取得する
3. とりあえず全部どこかにしまっておく
4. その中にディレクトリがあれば、2に戻る。なかったら5へ進む
5. 全部しまってあるやつの中から、 指定したワードを含むものを抽出
6. 抽出したもののパスの部分だけを返す
以上です。

これが実際のコードです。

```julia:findDir.jl
function findDir(startDir::String, searchWord::String)

    corresp = String[]
    readPath = String[]
    cd(startDir)
    allPath = readdir()

    n = 1
    while true
        try
            allPath[n] = startDir * "/" * allPath[n]
        catch
            break
        end
        n += 1
    end

    n = 1
    while true
        try
            if isdir(allPath[n])
                push!(readPath, allPath[n])
            end
        catch
            break
        end
        n += 1
    end

    while true
        newPath = String[]
        for i in 1:length(readPath)
            temp = readdir(readPath[i])
            for j in 1:length(temp)
                push!(newPath, readPath[i]* "/"* temp[j])
            end
        end

        append!(allPath, newPath)
        readPath = String[]
        for i in 1:length(newPath)
            if isdir(newPath[i])
                push!(readPath, newPath[i])
            end
        end

        if length(newPath) == 0
            break
        end
    end

    n = 1
    while true
        try
            if contains(allPath[n], searchWord)
                push!(corresp, dirname(allPath[n]))
            end
        catch
            break
        end
        n += 1
    end

    unique(corresp)
end
```

まずは簡単に引数の説明をします。

searchWordは検索するワードです。関数の返り値はこのsearchWordをファイル名に含んだファイルが存在するディレクトリのパスとなります。例えば、tukareta.csvというファイルがデスクトップ上のtukaretenaiというフォルダにあった場合は、tukaretenaiのパスが返ってきます。

startDirは名前の通り探索を開始するディレクトリです。そのためstartDirで指定したディレクトリ以降に存在するファイルのみを探すことになります。

##実際に使ってみるよ
それでは早速findDir関数を使ってみます。

```julia:findDirTest.jl
using DataFrames

Desktop = "ご自分のデスクトップのパスをどうぞ"
datTest = DataFrame(a = Float64[1, 2, 3], b = Float64[4, 5, 6])

mkdir(Desktop* "/test")
writetable(Desktop* "/test/datTest.csv", datTest)

findDir(Desktop, "datTest")
```

まず、Desktopというオブジェクトについてですが、これは私自身が頻繁に使用するのであらかじめ指定しているだけです。要らなければ省いてしまっても問題ありませんが、その場合はコード中のDesktopを書き換えるのを忘れずにしましょう。

次にmkdirという関数を使用しています。これはディレクトリを作成する関数です。実際にこのコードを実行するとデスクトップ上にtestというフォルダが新たに作成されます。さらにwritetableでtestというフォルダに、先ほど作成したdatTestをcsvファイルとして保存します。

ここまでで、デスクトップ上にtestというディレクトリが作成してあり、そのディレクトリにはdatTest.csvというファイルがあるはずです。

最後にfindDir関数を使用します。第一引数にstartDirを第二引数に検索ワードを指定します。これで実行すればdatTestが存在するディレクトリのパス、つまりtestというディレクトリのパスが返ってきます。

できていれば、さらに追加でこんなことをしてみます。

```julia:findDirTest2.jl
mkdir(Desktop*"/test/test2")
writetable(Desktop* "/test/test2/datTest.csv", datTest)

findDir(Desktop, "datTest")
```

まず、先ほど作成したtestというディレクトリに新たにtest2というディレクトリを作成します。次にdatTestをtest2に保存します。するとデスクトップ上にtestというディレクトリがあり、その中にはtest2というディレクトリとdatTest.csvというファイルがあります。さらにtest2の中にはdatTest.csvというファイルがあります。

これで先ほどと同じようにfindDirをすると、testとtest2という2つのパスが返ってきます。このようにfindDirは複数のファイルが検索ワードを含んでいた場合に、それら全てのディレクトリのパスを返すようになっています。

##関数の中身の解説するよ
ここまででは、findDir関数が結局何をやっているのかについては簡単なフローチャート以外では触れませんでした。「正直使い方分かればどうでもいいっすわ」という方や「あのフローチャートだけ見れば大体分かるわ」という方はわざわざ読む必要はないと思いますが、気になる人もいると思うので一応解説はしておきます。

細かく分けて順を追って説明します。
コードの下にそのコードの解説が書いてあります。

```julia:findDir.jl
    corresp = String[]
    readPath = String[]
    cd(startDir)
    allPath = readdir()
```

最初の4行を説明します。

まずは最終的に検索ワードと一致するものを保存するためのオブジェクトを作成します。String[]なので文字列型になっています。readPathは読み込んだディレクトリやファイルのパスを一時的に保存しておくためのオブジェクトです。
次のcd(startDir)ですが、これは引数で与えたstartDirをワーキングディレクトリに設定しています。最後のreaddir()が現在のワーキングディレクトリ上にあるディレクトリやファイル名を取得する関数です。

これで、startDir上にあるディレクトリとファイル名はallPathというオブジェクトに入れました。

```julia:findDir.jl
    n = 1
    while true
        try
            allPath[n] = startDir * "/" * allPath[n]
        catch
            break
        end
        n += 1
    end
```
次のwhile文です。
ここでは、さきほどreaddirで取得したディレクトリやファイル名にディレクトリくっつけているだけです。readdirではファイル名やディレクトリ名だけが読まれます。つまり、readdirで読み込んだもののディレクトリまではないのです。ということでこのようなことをしています。ちなみに文字列の結合は見ていただければ分かるように、文字列同士の掛け算でできます。

ここで使用しているtryとcatchですが、簡単に言うと「とりあえずtryにあるコードを実行して、エラーが起きたらcatchにあるコードを実行するよ」というものです。ここではallPathの長さをNとした時, N + 1番目の要素にアクセスしようとするとエラーが返ってくるので、allPathにある全ての要素にtry内のコードを実行したら、whileから抜けるようになっています。

ちなみにfor文で書くとこうなります。

```julia:forVer.jl
    for i in 1:length(allPath)
        allPath[i] = startDir * "/" * allPath[i]
    end
```

こっちの方が短いですね。
なんでわざわざtryとcatchを使ったのか自分でも覚えていません...

```julia:findDir.jl
    n = 1
    while true
        try
            if isdir(allPath[n])
                push!(readPath, allPath[n])
            end
        catch
            break
        end
        n += 1
    end
```
2番目のwhile文です。
ここではallPathに含まれる要素の内, ディレクトリだけをreadPathというオブジェクトにpush!しています。isdirの返り値はtrueかfalseで、引数がディレクトリならtrueそうでなければfalseを返します。

```julia:findDir.jl
    while true
        newPath = String[]
        for i in 1:length(readPath)
            temp = readdir(readPath[i])
            for j in 1:length(temp)
                push!(newPath, readPath[i]* "/"* temp[j])
            end
        end

        append!(allPath, newPath)
        readPath = String[]
        for i in 1:length(newPath)
            if isdir(newPath[i])
                push!(readPath, newPath[i])
            end
        end

        if length(newPath) == 0
            break
        end
    end
```

3番目のwhile文です。ここが割と重要で、やっていることはstartDir以降のサブディレクトリを全て探索するということです。

まず、先ほどallPathからディレクトリだけをreadPathにpush!しました。今度はreadPathの各要素についてreaddirします。つまり、デスクトップ上にある全てのディレクトリからファイル名とディレクトリ名を取得してtempというオブジェクトに入れます。

そして、先ほどもやりましたがreaddirの結果はファイル名とディレクトリ名だけなので、そこまでのパスを結合します。そして出来上がったものをnewPathにpush!します。

次にallPathにnewPathをappend!しています。簡単に言うと、allPathというオブジェクトにnewPathに含まれる全ての要素を追加しています。

次にreadPathを初期化します。この後は2番目のwhile文と同じで、newPathの要素のうちディレクトリだけをreadPathにpush!しています。

そして最後のif文はnewPathが0になったらwhileからbreakするようにしています。つまり、全てのサブディレクトリの探索が終わったところで、3番目のwhileが終了します。

```julia:findDir.jl
    n = 1
    while true
        try
            if contains(allPath[n], searchWord)
                push!(corresp, dirname(allPath[n]))
            end
        catch
            break
        end
        n += 1
    end

    unique(corresp)
```
これで最後です。ここではallPathにstartDir以降のサブディレクトリとそこにあるファイルのパスが保存してあるので、そこからsearchWordと一致する文字列を含んだ要素のディレクトリを取得します。

ここで使用しているcontainsは第一引数と第二引数に文字列を指定します。そして第一引数で渡した文字列が第二引数で渡した文字列が含まれているかをtrueかfalseで返します。これで、allPathの各要素がsearchWordを含んでいれば、correspにそのファイルがあるディレクトリをpush!しています。

最後に、uniqueで重複をなくしています。

##〆だよ
今回はファイル名から、そのファイルの存在するディレクトリのパスを返す関数を作ってみました。これでディレクトリの設定とかはかなり楽になるのではないでしょうか?個人的にはかなり重宝しています。

##追記(2018/08/24)
@tenfu2teaさんよりディレクトリ探索にwalkdirという関数があるということを教えてただいたため、それを使って書き直してみました。アドバイスありがとうございます。
まだwalkdirの挙動を把握しきれていないため詳しい解説はできないのでコードだけ載せておきます。一応、同じような結果が返ってくることは確認済みです。

```julia:findDir.jl
#findDirのwalkdirバージョン
function findDir(searchWord::String; rootDir = Desktop)
    pathToFiles = String[]
    corresp = String[]
    for (root, dirs, files) in walkdir(rootDir)
        for file in files
            push!(pathToFiles, joinpath(root, file))
        end
    end

    for i in 1:length(pathToFiles)
        if contains(pathToFiles[i], searchWord)
            push!(corresp, dirname(pathToFiles[i]))
        end
    end

    unique(corresp)
end
```

以上がwalkdirを使って書き直したものです。