ちょっと説明しづらい…。
「提示する実例」としては単純明快なものだが、ここに至る動機はちょっとややこしい。
ちょっとここ最近の作業の都合で、再び「rmdirhier な Go」に御用、だったのよ。そう、「/tmp の下に作られる特定のファイルを直接覗き込む必要があったが、長期間放置プレイだったので大量のゴミで溢れていて、ゆえに /tmp の掃除が必要だった」ということね。ゆえに、ちょっと Go にも少し熟達し始めてきた記念も込めて rmdirhier の拡張をやってた。
つまり元の動機は「find /tmp -type f -exec rm -fv ‘{}’ \; 相当」ではなかったわけ。あくまでも「まずは空フォルダ掃除」から始まった。のだけどさ、そもそも…「長期間放置プレイだったので大量のゴミで溢れていて」の状態は「空フォルダだけ消せばいい」なんてことにはならんわね、その「大量のファイル」も消したいわけよ。
で、そのために「find /tmp -type f -exec rm -fv ‘{}’ \;」していたわけなんだけれど、けどねこれ…、ふと気付いたんだけれどもさ、ワタシはこうなのよ:
- find を使うおよそ約 95.32155583473% くらいは -exec rm だ
- find を使うおよそ約 95.32155583473% の -exec rm の相手の 99.9551211% は /tmp 相手だ
- Windows のプロセス生成はコストがかかる(経験的に Unix の10倍)ため、find の -exec は(Windows で特に)非常に遅い
3つ目のなのだが、そういうわけなので、「find が C で作られている」ことが理由で「python よりも高速」とはならない。ちゃんと測ってはいないが、たぶん同じことをする python スクリプトを書いたほうが高速だと思う。そして、1.、2. の理由により、「専用の道具を作っちゃった方がよくね?」と思った、つぅ話ね。して、「せっかくだから Go の勉強がてら」て話よ。
そうなんだけど、find 並に柔軟性を持たせようとまで考えるとかなり厄介なのは自明としても、それ以前の問題があったんだよねこれが。そしてそのことをメモっときたい、というのが、今回のネタを書き留めようと思った動機。それ以前の問題、とは、「How to find file creation date in Windows using Go」のことね。少し前から薄々感じてはいたんだけれど、ファイル属性に関して「更新日付」はプラットフォーム非依存で取得出来るのに、「作成日付」が「プラットフォーム非依存の方法で」取れんのね。なんなんだろうかこの仕様…。ゆえに、find みたいに「ctime が~より古い/mtime が~より古い」みたいなコンディション指定をさせたいなら「作成日付」問題をクリアしなければならない。
てなことで、ちょっとやりかけたんだけれど、なんか面倒くさくなっちまってよ、ひとまずこれでいいかと:
1 package main
2
3 import (
4 "os"
5 //"io/fs"
6 "fmt"
7 "time"
8 "path/filepath"
9 //"syscall"
10 )
11 var (
12 now int64
13 )
14
15 func init() {
16 now = time.Now().Unix()
17 }
18
19 /*
20 func w32fatimes(finf fs.FileInfo) (int64, int64) {
21 d := finf.Sys().(*syscall.Win32FileAttributeData)
22 return d.CreationTime.Nanoseconds() / 1e+9, finf.ModTime().Unix()
23 }
24 */
25
26 func rmfiles(root string, age int64) {
27 ents, err := os.ReadDir(root)
28 if err != nil {
29 fmt.Print(err)
30 return
31 }
32 for _, ent := range ents {
33 path := filepath.ToSlash(filepath.Join(root, ent.Name()))
34 if ent.IsDir() {
35 rmfiles(path, age)
36 } else {
37 finf, _ := ent.Info()
38 //ct, mt := w32fatimes(finf)
39 mt := finf.ModTime()
40 if now - mt.Unix() > age {
41 //fmt.Println(path, ", ", mt)
42 rmerr := os.Remove(path)
43 if rmerr != nil {
44 fmt.Print(path, "(", mt, ")", ": ", rmerr, "\n")
45 } else {
46 fmt.Print("removed `", path, "(", mt, ")", "'\n")
47 }
48 }
49 }
50 }
51 }
52
53 func Rmfiles(roots []string, age int64) {
54 for _, root := range roots {
55 rmfiles(root, age)
56 }
57 }
58
59 func main() {
60 Rmfiles([]string{os.Getenv("TEMP")}, 60*60*24*3)
61 }
コメントアウトしてる部分が「作成日付」をどうにかしようとした痕跡。実際意図通り取れるんだけれど、なんか値がヘンなのよね、作成日付よりも更新日付のほうが古いファイルがあんの。なんだこれ。今まで気付かなかったぞ、何か変わった作り方をするとそういうことになるんかいね? てことに気付いたこともあって、「だったら更新日付だけでいいか」と。
そして「60*60*24*3」は3日という意味であり、当然これをコマンドライン引数とか設定ファイルとか環境変数とかから取るようにすれば「ちゃんとした便利道具」になるわけなんだけれど、これもさ、「1hour」とか「3days」とか「3weeks」みたいな指定を許したくなるじゃないか。そういうことを考え始めちゃったら、なんか途端に馬鹿らしくなってきて…。というか「うん、今日はメモだけでいいや」となった、てのが顛末。
なお、Go はコンパイル型の言語なので、コンパイルすることで exe にして使う、というのが通常の使い方なのだけれど、たとえばこれを「何ヶ月かに一回」という程度の頻度でしか使わないということなら、「ちゃんとした汎用道具」として作り込まずに「60*60*24*3部分を編集して go run」という手はある。go run の魔法は単に一時フォルダに一時的に exe を作ってそれを実行する、というだけ。ので今みたいに「60*60*24*3みたいな直値部分を編集して実行する」という「スクリプト言語を使うのと似たノリで」使うことが出来る。数ヶ月に一回、なら、こんなんでもいいよなぁと。
「ちゃんとしたもの」をちゃんと作れば、それこそスケジューラに登録して使うとかすればいいんだけどね、まぁそこまでしようとはひとまず思ってない。
2022-03-01追記:
やらない興味がない思ってない、馬鹿らしくなって、みたいに思ったあと、「やっぱ悔しくて」というのはもはやワタシの伝統芸能。当座最小限の「ちゃんと」版:
設定ファイル駆動ね。「>1days」みたいな指定を許容するようにしてある。ただし今のところ「最小限」なので、「2days」は出来ても「1days3hours」みたいなのまでは対応してない。いらんと思うけどね、気力があったらそのうちやるかもね。
だた悔しくなっただけなら別に追記はいらんのだけどもよ、実際はさ、「Go のお勉強としては結構価値ある」だったよ、ワレにとっては。json のアンマーシャライズの部分とかね、ちまちまとワタシ的にはお初経験がちょいちょいあったわけよ。まぁこうやって完成品だけ見せちゃうとそういうのってなかなか伝わらんのだけどね、オレの作業の中でだけ発生した事件だからね、それらって。だけれども。一連の Go の話では結構何度も言ってるけどさ、Go のドキュメントって、かなりヒドいシロモノなわけ。一応ライブデモがあるのはいいんだけど、にしたってサンプルの絶対数はあまりに欠けてるし、そもそも非常に読みにくいの。だから、どんなちんまいネタだろうと、こうやって実例を紹介し続けるのはとても価値があることだと思っている、てことなー。