特定日付のファイル検索は、GNU findの-newerがstrict comparisonである点を明示して設計します。求める区間は[start, end)です。開始境界の1 tick前と終了境界の1 tick前にreference fileを作り、実filesystem上のfixtureでstartちょうどを含みendちょうどを除外できた場合だけ本検索を実行します。
半開区間と出力先を固定する
mtimeを対象とし、+0900を含む絶対境界を使います。作成日時やatimeとは混同しません。NUL-safe resultが既に存在すれば上書きせず停止し、4ブロックは同じBashセッションで実行します。
set -Eeuo pipefail
root=/srv/lab/reports
result=/srv/lab/reports-20260710.list0
start='2026-07-10 00:00:00 +0900'
end='2026-07-11 00:00:00 +0900'
parent=$(dirname -- "$root")
test -d "$root" && test ! -L "$root"
test ! -e "$result" || { printf 'STOP: result file exists\n' >&2; exit 20; }
command -v python3 >/dev/null
printf 'filesystem_device=%s start=%s end=%s\n' "$(stat -c %d -- "$root")" "$(date -d "$start" --iso-8601=ns)" "$(date -d "$end" --iso-8601=ns)"
filesystem精度をfixtureで実証する
-newerはreferenceよりstrict newerです。start−1nsより新しく、end−1nsより新しくない条件を使い、同じdevice上でat-start・inside・at-endを作ります。expectedはat-startとinsideだけです。丸め精度がこの条件を満たさなければcmpで停止します。
atomic_noreplace() {
python3 - "$1" "$2" <<'PY'
import ctypes, os, sys
src, dst = map(os.fsencode, sys.argv[1:])
libc = ctypes.CDLL(None, use_errno=True)
try:
renameat2 = libc.renameat2
except AttributeError:
raise SystemExit("renameat2 is unavailable; no non-atomic fallback is allowed")
renameat2.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
if renameat2(-100, src, -100, dst, 1) != 0: # RENAME_NOREPLACE
number = ctypes.get_errno()
raise OSError(number, os.strerror(number), os.fsdecode(dst))
PY
}
run_id=$(date -u +%Y%m%dT%H%M%SZ)-$$-$RANDOM
work=$(mktemp -d -p "$parent" ".ittrip-122.$run_id.XXXXXXXX")
printf '%s\n' "$run_id" > "$work/.owner"
test "$(stat -c %d -- "$work")" = "$(stat -c %d -- "$root")"
start_ref="$work/start-minus-one-tick.ref"
end_ref="$work/end-minus-one-tick.ref"
touch --date="$start - 1 nanosecond" "$start_ref"
touch --date="$end - 1 nanosecond" "$end_ref"
mkdir -- "$work/fixtures"
touch --date="$start" "$work/fixtures/at-start"
touch --date="$start + 12 hours" "$work/fixtures/inside"
touch --date="$end" "$work/fixtures/at-end"
find "$work/fixtures" -xdev -type f -newer "$start_ref" ! -newer "$end_ref" -printf '%f\0' | sort -z > "$work/fixture.actual0"
printf 'at-start\0inside\0' > "$work/fixture.expected0"
cmp --silent "$work/fixture.expected0" "$work/fixture.actual0"
find "$root" -xdev -type f -newer "$start_ref" ! -newer "$end_ref" -print0 | sort -z > "$work/result.list0"
NUL-safe resultをno-clobberで採用する
実検索は-print0とsort -zを維持します。結果をRENAME_NOREPLACEで固定名へ採用し、PythonはbytesをNULで分割して表示するだけです。改行や空白を含むpathも1項目として保持されます。
atomic_noreplace "$work/result.list0" "$result"
test ! -e "$work/result.list0" && test -f "$result"
python3 - "$result" <<'PY'
import os, sys
data = open(sys.argv[1], 'rb').read()
if data and not data.endswith(b'\0'):
raise SystemExit('not NUL terminated')
items = data[:-1].split(b'\0') if data else []
for item in items[:30]:
print(os.fsdecode(item))
print('count=' + str(len(items)))
PY
result_sha=$(sha256sum -- "$result"); result_sha=${result_sha%% *}; result_sha=${result_sha#\\}
receipt="$parent/.ittrip-122-$run_id.receipt"
( umask 077; set -o noclobber; printf 'result=%s\nsha256=%s\nstart=%s\nend=%s\nrun_id=%s\n' "$result" "$result_sha" "$start" "$end" "$run_id" > "$receipt" )
fixture証拠とrun-owned retention
referenceの実mtime、expected/actual fixture、result hash、start/endをreceiptへ結びます。retentionはmktemp directoryのowner tokenが同じrun_idである場合だけ行い、検索rootや既存resultを変更せず同一filesystemの保管名へno-clobberで移します。破棄は別承認です。
# fixtureは実際のfilesystem精度で startを含み、endを除外した証拠である。
printf 'start_ref=%s\n' "$(stat -c '%y' -- "$start_ref")"
printf 'end_ref=%s\n' "$(stat -c '%y' -- "$end_ref")"
cmp --silent "$work/fixture.expected0" "$work/fixture.actual0"
grep -Fx "result=$result" "$receipt" >/dev/null
grep -Fx "sha256=$result_sha" "$receipt" >/dev/null
test "$(cat -- "$work/.owner")" = "$run_id"
retained_work="$work.completed-$run_id"
test ! -e "$retained_work"
atomic_noreplace "$work" "$retained_work"
printf 'run-owned evidence retained=%s; disposal requires a separate approved retention procedure\n' "$retained_work"
printf 'accepted interval=[%s, %s), NUL-safe result=%s\n' "$start" "$end" "$result"
受入条件
fixtureではstartちょうどと区間内だけが採用され、start直前とendちょうどは除外されます。本結果は末尾NULを持ち、path件数はNUL record数で数えます。findのpermission errorが出た場合は完全結果と扱わず、stderrと非0終了を保存します。
境界を言い換えない
単に-newermt start ! -newermt endと書くと(start, end]になり、要件と逆になります。-mtime 0も現在時刻からの丸めであり暦日検索ではありません。timezoneとfilesystem timestamp精度をfixtureで確認することが必須です。
実運用で半開区間を再現するチェックリスト
検索日時は実行端末の曖昧な既定値に任せず、依頼票・reference file・receiptで同じタイムゾーンとオフセットを使います。夏時間がある地域では暦日の長さが常に24時間とは限らないため、開始と終了をそれぞれ絶対日時へ解決してからfixtureを作ります。別ホストで結果を再現する場合は、date --iso-8601=nsで正規化した境界、stat -c %yで読み戻したreferenceの実mtime、filesystem device番号を一組の証拠として保存します。
filesystemがnanosecond指定を粗い精度へ丸めることは、説明だけで許容せずfixture比較で判定します。at-start、区間内、at-endの3ファイルから得たNUL区切りのactualがexpectedとbyte一致しない環境では、本番検索へ進みません。NFSなど別ホストの時刻や遅延が関わるrootでは、検索端末の時計だけを根拠にせず、対象側でsnapshotまたは変更停止点を確定し、その時点と境界の関係を運用記録へ残します。
権限不足、切断、読取り不能なdirectoryが1件でもあれば、見つかったpathだけを完全結果として採用してはいけません。標準エラーと終了statusをrun_idへ結び、statusが0でfixture・result hash・NUL終端の検査がすべて通った場合だけresultを受け入れます。件数確認は改行数ではなくNUL record数で行い、表示用の先頭30件は監査用resultそのものと分離します。これにより、改行を含むpathや空白だけが異なるpathも欠落させず照合できます。
検索中にfileが作成・更新・削除されるrootでは、得られる一覧は単一時点のsnapshotではありません。証拠保全や移行判定に使うなら、filesystem snapshot、アプリケーション停止、または更新を止めた複製rootを先に用意します。receiptにはroot、開始・終了境界、実行開始時刻、終了status、result SHA-256、fixture expected/actualのhash、使用したfindとdateの版を記録し、再実行時は固定名を上書きせず新しいrun_idで比較します。
公式情報・参考資料
特定日付ファイル検索の構文と制約は、本文末の一次資料と対象環境のlocal helpで照合します。特定日付ファイル検索の記事確認日は2026年7月17日で、版が異なる場合はoption、default、終了statusの差を先に確認してください。

コメント