findのmtimeオプションとその仲間たち ([cma](time|min)オプション

最近会社で聞かれて答えたことをまとめてみました。

Linuxで、一定期間以上過去のファイルをgzipするcron処理があって、その日数を変更したい、という話がありました。以下のようなコマンドを使っていました。

```
find ～～～ -mtime +2 -exec gzip {} \;
```

mtimeの+2のところを変えることになりました。"+2"の意味を聞かれて答えたこと、間違って答えてしまったこと、答えていない周辺部分をまとめてみました。

# 動作確認環境

以下の中で、実際に動かして確認している部分は"Lubuntu 18.04.4"を使用しています。

# findの基本機能のおさらい

findの基本機能は、指定したディレクトリ以下を走査してファイル一覧を標準出力に出力するコマンドです。

もともとの話に出ていたコマンドでは、"-exec"オプションを";"付きで使用しています。これは、"ファイル一覧を出力する"代わりに、"個々のファイルを引数にして、-execに後続するコマンドを実行する"という意味です。"{}”の位置に個々のファイルがセットされた状態でコマンド実行されます。なお、個人的には"-exec"を使う場面では、代わりに"xargs"を使うことが多いです。

以降では、ファイルに付随する日時を取り扱うオプションに焦点をあてて説明していきます。その他の機能の詳細は[JMのfind](https://linuxjm.osdn.jp/html/GNU_findutils/man1/find.1.html)で確認するのがよいでしょう。

# 日時ベースでの絞り込み

mtimeオプションは、出力するファイルを、ファイルの更新日時条件で絞り込むためのものです。

このように、ファイルに付随する日時を絞り込み条件にするオプションには、以下があります。

* ?time ( ctime / mtime / atime )
* ?min ( cmin / mmin / amin )

また、それに関連するオプションには以下があります。

* daystart

以降では、これらを細かく説明していきます。

なお、日時条件でファイルを絞り込むオプションには、"指定したファイルよりも新しい"という条件を指定するためのオプションもあります。しかし、この記事では説明を省略します。

## ?time と ?min

これらのオプションは、出力するファイルをファイルの"最終ステータス変更日時"、"最終更新日時"、"最終アクセス日時"の条件で絞り込むためのものです。

オプションの一文字目が日時の種類を表します。

* c : 最終ステータス変更日時
* m : 最終更新日時
* a : 最終アクセス日時

オプションの二文字目以降が日時の単位を表します。

* time : 日数単位
* min : 分数単位

そして、数値もしくは符号付き数値をオプションに後続させます。符号の有無は、以下のような意味になります。

* 符号なし : ぴったりその数値
* プラス : 指定された数値より大きい
* マイナス : 指定された数値ほど小さい

例えば"-mtime +2"は、おおざっぱに言えば"最終更新日時が2日以上過去のもの"となります。

では、これらの要素をより詳しく掘り下げていくこととしましょう。

### 数値指定(?time)

この節では、最終更新日時"mtime"を使って説明します。

きっかけとなった話では"一定以上過去"のファイルが対象でした。前述のように"-mtime +2"といった表記で指定することになります。さて、このような指定をすると、どのようなファイルが対象となるのでしょうか？少し実験してみます。

タイムスタンプを6時間ごとに変えたファイルを数日分作成して、どのファイルがfindの対象となるかを確認してみます。

まずはテスト環境の準備です。

```
linuser$ rm -f *.txt
linuser$ for day in $( seq -w $( date +%d --date '5 days ago' ) $( date +%d ) ); do for hour in $( seq -w 0 6 23 ); do touch -m -t 202003${day}${hour}00 $day-$hour.txt; done; done
linuser$ date; ls -l
2020年  3月 14日 土曜日 10:23:45 JST
合計 0
-rw-rw-r-- 1 linuser linuser 0  3月  9 00:00 09-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月  9 06:00 09-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月  9 12:00 09-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月  9 18:00 09-18.txt
-rw-rw-r-- 1 linuser linuser 0  3月 10 00:00 10-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月 10 06:00 10-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月 10 12:00 10-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月 10 18:00 10-18.txt
-rw-rw-r-- 1 linuser linuser 0  3月 11 00:00 11-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月 11 06:00 11-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月 11 12:00 11-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月 11 18:00 11-18.txt
-rw-rw-r-- 1 linuser linuser 0  3月 12 00:00 12-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月 12 06:00 12-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月 12 12:00 12-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月 12 18:00 12-18.txt
-rw-rw-r-- 1 linuser linuser 0  3月 13 00:00 13-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月 13 06:00 13-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月 13 12:00 13-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月 13 18:00 13-18.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14 00:00 14-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14 06:00 14-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 14-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 14-18.txt
linuser$
```

末尾の2行の時刻が表示されていないのは、ファイルの更新日時が未来を指しているからです。
さて、findを実行してみます。

```
linuser$ find . -type f -mtime +2 | xargs -r ls -l
-rw-rw-r-- 1 linuser linuser 0  3月  9 00:00 ./09-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月  9 06:00 ./09-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月  9 12:00 ./09-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月  9 18:00 ./09-18.txt
-rw-rw-r-- 1 linuser linuser 0  3月 10 00:00 ./10-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月 10 06:00 ./10-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月 10 12:00 ./10-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月 10 18:00 ./10-18.txt
-rw-rw-r-- 1 linuser linuser 0  3月 11 00:00 ./11-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月 11 06:00 ./11-06.txt
linuser$ find . -type f -mtime 2 | xargs -r ls -l
-rw-rw-r-- 1 linuser linuser 0  3月 11 12:00 ./11-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月 11 18:00 ./11-18.txt
-rw-rw-r-- 1 linuser linuser 0  3月 12 00:00 ./12-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月 12 06:00 ./12-06.txt
linuser$ find . -type f -mtime -2 | xargs -r ls -l
-rw-rw-r-- 1 linuser linuser 0  3月 12 12:00 ./12-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月 12 18:00 ./12-18.txt
-rw-rw-r-- 1 linuser linuser 0  3月 13 00:00 ./13-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月 13 06:00 ./13-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月 13 12:00 ./13-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月 13 18:00 ./13-18.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14 00:00 ./14-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14 06:00 ./14-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 ./14-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 ./14-18.txt
linuser$
```

このような結果となりました。

"-mtime 2"を指定すると、"48時間前～72時間前"が出力されています。つまり、"過去にさかのぼる日数が2日、但し24時間未満切り捨て"のような意味合いとなります。

以下のような図で表すと、より分かりやすくなるかもしれません。

![findのmtimeの範囲_daystartなし.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/381550/f99d7c9a-6fc8-503c-ae3e-55354d52c830.png)

計算単位が日数なのに、基点が0時ではないという部分が分かりにくいところです。

実は、基点を0時にするオプションがあります。それが"-daystart"です。このオプションをつけてもう一度試してみましょう。"-mtime"よりも前に"-daystart"を指定する必要があります。

```
linuser$ find . -type f -daystart -mtime +2 | xargs -r ls -l
-rw-rw-r-- 1 linuser linuser 0  3月  9 00:00 ./09-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月  9 06:00 ./09-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月  9 12:00 ./09-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月  9 18:00 ./09-18.txt
-rw-rw-r-- 1 linuser linuser 0  3月 10 00:00 ./10-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月 10 06:00 ./10-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月 10 12:00 ./10-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月 10 18:00 ./10-18.txt
-rw-rw-r-- 1 linuser linuser 0  3月 11 00:00 ./11-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月 11 06:00 ./11-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月 11 12:00 ./11-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月 11 18:00 ./11-18.txt
linuser$ find . -type f -daystart -mtime 2 | xargs -r ls -l
-rw-rw-r-- 1 linuser linuser 0  3月 12 06:00 ./12-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月 12 12:00 ./12-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月 12 18:00 ./12-18.txt
-rw-rw-r-- 1 linuser linuser 0  3月 13 00:00 ./13-00.txt
linuser$ find . -type f -daystart -mtime -2 | xargs -r ls -l
-rw-rw-r-- 1 linuser linuser 0  3月 13 00:00 ./13-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月 13 06:00 ./13-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月 13 12:00 ./13-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月 13 18:00 ./13-18.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14 00:00 ./14-00.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14 06:00 ./14-06.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 ./14-12.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 ./14-18.txt
linuser$
```

わかりやすい結果になったのではないでしょうか。暦上の日数がそのまま、数値指定となっています。こちらも同じく図で表してみます。

![findのmtimeの範囲_daystartあり.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/381550/fe019636-fbb2-f7f9-4311-a6fd6af92e5d.png)

さて、分かりやすくなったものの、こちらにも分かりにくい動作部分があります。どこか分かるでしょうか？

"13日00時"は、"-mtime 2"の結果にも"-mtime -2"の結果にも含まれています。一方、"12日00時"は、どこにも含まれていません。

直感的には、境界線を含めてすべての日時が"-mtime +2"か"-mtime 2"か"-mtime -2"のいずれか一つのみに含まれると思ってしまうのではないでしょうか。そして、マニュアルにも24時間で割って端数切捨てとありますので、境界線はどちらか一方に寄るのが妥当な気がします。

しかし実際はそうではないようです。

### 数値指定(?min)

"mmin"は、日単位ではなく分単位だということが違うだけで、"mtime"と同じです。

こちらは"-daystart"付きのみで確認してみます。現在日時で行うと、すぐにファイルの状況が変わってしまうので。

```
linuser$ rm *.txt
linuser$ for day in $( date +%d ); do for min in $( seq -w 53 59 ); do touch -m -t 202003${day}23${min} $day-23${min}.txt; done; done
linuser$ ls -l
合計 0
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 14-2353.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 14-2354.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 14-2355.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 14-2356.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 14-2357.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 14-2358.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 14-2359.txt
linuser$ find . -type f -daystart -mmin +5 | xargs -r ls -l
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 ./14-2353.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 ./14-2354.txt
linuser$ find . -type f -daystart -mmin 5 | xargs -r ls -l
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 ./14-2356.txt
linuser$ find . -type f -daystart -mmin -5 | xargs -r ls -l
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 ./14-2356.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 ./14-2357.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 ./14-2358.txt
-rw-rw-r-- 1 linuser linuser 0  3月 14  2020 ./14-2359.txt
linuser$
```

こちらも"-mtime"と同様の結果になりました。56分は"-mmin 5"と"-mmin -5"の両方に含まれる、55分はどこにも含まれないという結果になりました。

## 日時の種類

"?time"や"?min"に使用できる日時の種類には、以下の3つがあると書きました。

* c : 最終ステータス変更日時
* m : 最終更新日時
* a : 最終アクセス日時

会社では"c"は"ファイル作成日時"と答えてしまいました。これは誤りです。上に書いた"JMのfind"の表記では"最終ステータス変更日時"となっています。

大まかには、"最終アクセス日時"はファイルの読み込み、"最終更新日時"はファイルの書き込み、"最終ステータス変更日時"はファイルの書き込みやi-nodeデータの変更で変わります。i-nodeデータの変更は、ファイル名、所有者やグループやパーミッション、リンク数などの変更が該当します。

では、確認してみることにしましょう。00時にファイル作成、01時にファイル読み込み、02時にファイル名変更、03時にファイル書き込みを行い、それぞれの時点での状況を確認してみます。

```
linuser# rm -f a; date 03140000; touch a; stat a; date 03140100; cat a > /dev/null; stat a; date 03140200; mv a b; mv b a; stat a; date 03140300; echo OK >> a; stat a
2020年  3月 14日 土曜日 00:00:00 JST
  File: a
  Size: 0               Blocks: 0          IO Block: 4096   通常の空ファイル
Device: 801h/2049d      Inode: 18054       Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2020-03-14 00:00:00.007999999 +0900
Modify: 2020-03-14 00:00:00.007999999 +0900
Change: 2020-03-14 00:00:00.007999999 +0900
 Birth: -
2020年  3月 14日 土曜日 01:00:00 JST
  File: a
  Size: 0               Blocks: 0          IO Block: 4096   通常の空ファイル
Device: 801h/2049d      Inode: 18054       Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2020-03-14 01:00:00.003999999 +0900
Modify: 2020-03-14 00:00:00.007999999 +0900
Change: 2020-03-14 00:00:00.007999999 +0900
 Birth: -
2020年  3月 14日 土曜日 02:00:00 JST
  File: a
  Size: 0               Blocks: 0          IO Block: 4096   通常の空ファイル
Device: 801h/2049d      Inode: 18054       Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2020-03-14 01:00:00.003999999 +0900
Modify: 2020-03-14 00:00:00.007999999 +0900
Change: 2020-03-14 02:00:00.007999999 +0900
 Birth: -
2020年  3月 14日 土曜日 03:00:00 JST
  File: a
  Size: 3               Blocks: 8          IO Block: 4096   通常ファイル
Device: 801h/2049d      Inode: 18054       Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2020-03-14 01:00:00.003999999 +0900
Modify: 2020-03-14 03:00:00.003999999 +0900
Change: 2020-03-14 03:00:00.003999999 +0900
 Birth: -
linuser#
```

statコマンドの出力結果のうち、"Access/Modify/Change"が"atime/mtime/ctime"です。"a/m/c"は、どうやら"Access/Modify/Change"の頭文字っぽいですね。

予想していたように、以下のように状態が変わっていることが分かります。

* 00時：ファイル作成した日時が、3つの日時すべてにセットされている。
* 01時：ファイルからの読み込みのタイミングで、Accessが01時にセットされている。
* 02時：ファイル名変更のタイミングで、Changeが02時にセットされている。
* 03時：ファイルへの書き込みのタイミングで、ModifyとChangeが03時にセットされている。

さらについでに言うと、lsやstatコマンドを実行するだけでは"atime"は変わらないことも分かりました。最終アクセス日時が指す"アクセス"は、"ファイル読み込み"のみです。

# 最後に

まとめることで、ctimeについての理解を正すことができました。また、?timeや?minの判定方法、対象となる日時の範囲も、間違って覚えていたことが分かりました。

まとめることが理解を深めることにつながることを、実感しました。

なお、atimeはファイル読み込みで変わります。"open()だけでは変わらず、read()しないと変わらない"というところまで説明したいと思っていました。しかし、お手軽にシェルスクリプトだけで確認する方法が思い浮かばず、断念しました。