rmdirhier な Go

今日のニーズがてらのお勉強、てのが、新しいものを学ぶのには一番手っ取り早い、と思う。

「rmdir」を再帰的に実行したい、というニーズ、ただし、「rmdir --parents」とは違うやつ。

「フォルダを削除」という行為において rmdir をあえて使いたい理由は、「空のフォルダを消したい」から。だって「問答無用で消えやがれ」には「rm -fr」でいいわけなんだからね、あえて空でないフォルダで不平を言う rmdir を使う理由がない。すなわち「「rmdir」を再帰的に実行したい」とは?

再帰的に、という概念で括るなら「rmdir --parents」は、まごうことなき再帰である。これは、「a/b/c」という構造に対して:

1 [me@host: ~]$ rmdir a/b/c  # leaf から順に
2 [me@host: ~]$ rmdir a/b  # leaf から順に
3 [me@host: ~]$ rmdir a  # めでてぇな

というバカバカしい操作をしなくて済むように、という機能。これはこれでありがたいはありがたいのだが。

これから言う「ワタシのニーズ」の方の「再帰的に」が必要になることは、普通はあんまりない。というか一生必要に思わない人も多いかもしれない。ゆえに、この「ワタシのニーズ」に見合うものが何かのインフラに組み込まれているのを、ワタシは見たことがない。だから欲しければ自分でなんとかするしかない。「ワタシのニーズ」はこれでわかるかしらん?:

1 [me@host: ~]$ find /tmp -type d -exec rmdir --parents '{}' ;

これ、やってみればわかるけれど、「複数回繰り返さないと全部の空フォルダを消せない」。リーフが消えたおかげで空フォルダになった、という結果に追従出来ないから、だよ。(厳密には、正確に列挙順をコントロール可能なら、「出来る」。列挙順をコントロール出来ないことが理由で出来ないのだ。必要なのは「深い方から順に rmdir」である。)

やりたいことは、要するに /tmp 以下のゴミ掃除を「まずは空フォルダを真っ先に消しちまえ」みたいなことよ。/tmp に一時ファイルを作るアプリケーションてさ、ファイルは後始末で消すのに、フォルダは消さずに残しちゃうやつが結構多いんだよね(消さないというか消せなくて結果的にてのも多い)。気付くと大量の空フォルダが出来てたりする。そして、なんでこんなことをしたいかといえば、「/tmp に置かれてしまったファイルに用事がある」場合よね、そういう場合にゴミだらけだと、目的の一時ファイルに辿り着くのがかなり面倒になる。

てことなのだが、そう、この「ワタシのニーズ」って、結局「/tmp 相手」くらいにしか必要にならないの。だって考えてみなさいな、「問答無用で空フォルダがゴミ」と断言できる場所って、一時ファイル関連にしかないじゃないか。普通の空フォルダは、普通は「後日ナニモノかが置かれるであろうよ」な場所なんだろ? そのために掘ったんじゃないの?

てことで、今日それが必要になったので。「勉強し始めた Go でやってみようかな」と:

rmdirhierwego.go
 1 package main
 2 
 3 import (
 4     "os"
 5     "log"
 6     "sort"
 7     "strings"
 8     "path/filepath"
 9 )
10 
11 func CollectDirs(root string, alldirs []string) []string {
12     ents, err := os.ReadDir(root)
13     if err != nil {
14         log.Fatal(err)
15     }
16     for _, ent := range ents {
17         path := filepath.ToSlash(filepath.Join(root, ent.Name()))
18         if ent.IsDir() {
19             alldirs = append(alldirs, path)
20             alldirs = CollectDirs(path, alldirs)
21         }
22     }
23     return alldirs
24 }
25 func RmdirHier(roots []string) {
26     var alldirs []string
27     for _, root := range roots {
28         alldirs = CollectDirs(root, alldirs)
29     }
30     sort.Slice(alldirs, func(i, j int) bool {
31         lhs := strings.Split(alldirs[i], "/")
32         rhs := strings.Split(alldirs[j], "/")
33         return len(lhs) > len(rhs)
34     })
35     for _, fn := range alldirs {
36         ents, _ := os.ReadDir(fn)
37         if len(ents) == 0 {
38             rmerr := os.Remove(fn)
39             if rmerr != nil {
40                 log.Fatal(fn, rmerr)
41             } else {
42                 log.Printf("removed: %s\n", fn)
43             }
44         }
45     }
46 }
47 func main() {
48     RmdirHier(os.Args[1:])
49 }

(→rmdirhierwego.0.0.1.zip)
配列(alldirs)の取り回し方が、Go プログラミングマナーの標準なのかがよくわかんない。なんとなく参照とかポインタで受け取ったものを直接書き換えるとかも出来るんだと思うし、そちらがより郷(Go)なら従うべきだとは思うが、そこらはまだ良くわからん。

配列まわり以外でやっててちょっと思ったのは、「さすが Unix 開発の首謀者たちだ」てこと。可能ならインターフェイスを小さくしようとする傾向が強いんだろうなぁと。そもそも Unix に rmdir が実在してるにも関わらず、Go の「os」が rmdir を提供しないんだよね、rmdir したければ「os.Remove」。plan9 でやろうとしてたことに通ずるというか。


まだあまりワタシのサイトで Go について多くは書いてないので、はじめのうちはこういうことも少し書いておこうかなという話を。

ワタシのサイトで初めて Go について触れたときにも書いたけれど、Go はとても興味深くて面白い言語であることは間違いない。けれども「総合評価」についてはこれは、「多くの人にいつでもどこでもオススメ」というにはかなり厳しいものであることも、これは事実。

オススメしづらい元凶は、やはり「Windows」のせい。Go で EXE アプリケーションを作る、というだけならそれほど支障はないのだが、ライブラリ、つまり「*.lib」や「*.dll」を作ったりそれを使ったりするのは、現時点では絶望的。そして、将来的にこの未来が明るくなるのかどうかについても、首謀者の年齢を考えるとどうなんだろうか、と思ったりもするし。ちゃんと若者が熱狂してくれて、跡を継ぐ若者がたくさんいるようなら問題ないけれど、こればっかはそうなってみないとわからんもんねぇ。

もう一つは、その「Go の使いやすさ・面白さ」が非常にわかりにくいこと。ある程度多数の言語を経験した者が Go に感じる特徴って、おそらく「とても使いやすくなった C」が一番近いんじゃないかと思うんだ。ハデな個性がほとんど無くて、「ここがすげーんだぜっ」と熱狂しづらいんじゃないかと。…個々人が熱狂しづらいというよりは、「評論家」気質の、たとえばコンピュータ関連のライターがこぞって褒め称えるようなものではない、てことかなと思う。…まぁ要は「そこがいい」のだけどね。かなり短く表現すると「小さくて気が利くヤツ」とか「無骨」とか。高倉健を評価するのと似た感じかもしんない。

「面白いよ」とは言っておくけれど、どのように面白くて使いやすいのかはこれは、実際に自分で体験してみないとわからんかなと思う。「オブジェクト指向とはなんだったのか」みたいなことを考え直すキッカケにもなると思うしね。遊んでみれば得るものはあると思うよ。


2022-02-27追記:
「結局「/tmp 相手」くらいにしか必要にならない」と言った通りでほんとに常に /tmp 相手に使っているわけなんだけれど、「工場出荷状態で定期的に /tmp クリーンアップしてくれるお便利ジョブがスケジュールされている」なんて親切なベンダーはいない Windows において「長期間野ざらしのままにしといた /tmp」って、ほんとに「大量のゴミ」が溜まってるわけね、だからこの rmdirhier もかなり時間がかかる処理になる。

んで、「最初に対象フォルダをかき集めて、再帰削除処理に適した順序に並べ替えてから削除」だと、うん、「もどかしい」んだわ、起動してかなり長い時間だんまりになるからね。ので、最初の収集の段階から「まずはダメ元で消してみる」という振る舞いにしたほうが気分は良いだろうなと。てわけで、書き換えたヤツ…は Gist に置いた:

最初に書いた時よりは少しは熟達してるが、まぁまだ初心者だわ。けど「なんとなく参照とかポインタで受け取ったものを直接書き換えるとかも出来る」は、ポインタを介するやり方に変えた。ポインタが C/C++ のものと同じ概念なら、きっと効率は少し良くなったんだろうと思う。…というかそろそろちゃんとした学習もせんといかんなぁ…、「だろう」とか「たぶん」とか気持ち悪いよね。