在 Linux 上删目录,听起来是
rm -rf xxx十个字符就能解决的问题,但只要加上“指定日期”四个字,事情就开始变得微妙。我最早做运维时接过一个工单:备份盘告警,需要删掉 2024 年 12 月之前的目录。我想都没想就敲了一条find /backup -type d -mtime +30 -exec rm -rf {} \;,结果第二天就被同事找上门——有几个目录是“看着旧、里面文件刚被引用”的数据,因为目录 mtime 只是被直接子项的增删改带动,并不能完全代表所有文件的修改时间。那次之后我才意识到,Linux 删除指定日期目录,真正的难点从来不是rm本身,而是“日期”这两个字到底怎么理解。
这篇博文不打算讲那些一搜一大把的命令手册,就结合我在生产服务器上做日志清理、备份转储、打包产物整理时真正用过的方案,把“按日期删除目录(包括目录下文件)”这件事拆透。我会覆盖按目录名匹配、按修改时间匹配、按精确时间区间匹配三种思路,也会把踩过的坑和最终的成品脚本分享出来。
1. 先把“指定日期”翻译成 Linux 能理解的语言
1.1 三种日期含义,对应完全不同的命令
所谓的“指定日期”,在 Linux 文件系统里至少有三种理解方式,很多踩坑事故都是从这里开始的。
第一种是目录名里带日期。比如 /data/backup/backup_2025-01-15、/opt/logs/20250115,这种情况最简单,日期是命名规则的一部分,直接按字符串匹配就行。
第二种是目录的修改时间(mtime)。它表示目录本身最后一次被修改的时间。注意,目录的 mtime 变化规则和文件不太一样,后面我们会专门说。
第三种是目录的状态变更时间(ctime)。ctime 会在权限、属主、硬链接数变化时更新,用 chmod、chown 都会影响它,所以用来判断“哪天创建的目录”并不是最可靠的指标。
还有 atime(访问时间),它在读目录内容时可能更新,在生产环境清理场景中我基本不考虑它,因为访问行为太频繁,noatime 挂载还可能导致它不准。
动手之前,先用一条命令看清楚这些时间到底是什么:
bash复制stat /data/backup/backup_2025-01-15
输出里会分别列出 Access、Modify、Change 三行时间。也可以用更紧凑的方式批量看目录名和 mtime:
bash复制find /data/backup -maxdepth 1 -type d -printf '%TY-%Tm-%Td %f\n' | sort
这条命令会列出 data/backup 下所有一级目录,并将每个目录的修改时间输出为 YYYY-MM-DD 目录名。跑完之后,我建议你先问自己一句话:我准备删的这批目录,判定依据到底是名字里的日期,还是内核记录的 mtime?想清楚这个问题,后面几乎不会再出错。
提示:如果目录名里已经有明确的日期,比如
backup_2025-01-15,优先按名字匹配。名字是人为约定的规则,最稳定;mtime 受复制、解压、同步工具影响,往往和你“以为的日期”对不上。
1.2 应用场景决定日期判定方式
根据我处理过的几类实际需求,“指定日期”通常对应下面几种场景:
| 场景 | 目录命名习惯 | 推荐判定方式 |
|---|---|---|
| 日志按天归档 | /var/log/app/2025/01/15 |
目录名日期 |
| 数据库备份目录 | /backup/mysql_2025-01-15 |
目录名日期 |
| 构建产物目录 | /workspace/build_1684224000 |
mtime(时间戳) |
| 用户上传目录 | /data/uploads/ |
mtime + 目录名混合 |
| 临时解压目录 | /tmp/tmp.* |
mtime |
大多数正式系统,目录名里都会带日期,因为人眼一看就知道是什么时候的东西。但如果你面对的是像 /tmp/tmp.aB3xY9 这种随机名字,那就只能依赖时间戳。
还有一类混合场景:目录名里带的是时间戳(epoch),比如 build_1704067200。这种可以先用 date -d @1704067200 换算成可读日期,再决定用名称匹配还是用 mtime 匹配。我更推荐在脚本里直接按 mtime 处理,因为时间戳表达的就是某个时刻,和文件系统记录的时间是一致的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 目录名里带日期:用 find 精准匹配,别绕弯子
2.1 最基础的一条完整命令
假设你有一个目录 /data/backup,里面按 backup_YYYY-MM-DD 的规则生成备份目录,现在要删掉 backup_2025-01-15,包括它里面的所有文件。
bash复制find /data/backup -maxdepth 1 -type d -name 'backup_2025-01-15' -exec rm -rf {} \;
拆开解释:
-maxdepth 1限制只在/data/backup这一层查找,不会递归到子目录里去找同名目录。这一点非常重要,防止误删更深层的目录。-type d只匹配目录,不会匹配同名的普通文件。-name 'backup_2025-01-15'按名称精确匹配。-exec rm -rf {} \;对每个找到的目录执行删除。
注意:find 后面如果不用 -maxdepth,它会递归查找所有符合 -name 条件的目录。如果正好有一层子目录也叫这个名,也会一并删掉。大多数场景下,清理历史目录时,我们只希望操作顶层目录,所以 -maxdepth 1 是默认带上的安全阀。
{} 是 find 的占位符,代表当前匹配到的路径。\; 表示每匹配一个目录就执行一次命令,你也可以写成 -exec ... {} +,它会尽量把多个路径攒成一批再执行。但在 rm -rf 的场景下,单批和批量没有本质区别,我更推荐默认用 \;,因为一旦某个路径有问题,错误日志更清晰,容易定位。
2.2 用通配符删“某一类日期”的目录
如果需求不是某一个具体日期,而是“所有 2025 年 1 月的历史备份”,命令只需要改一下 -name:
bash复制find /data/backup -maxdepth 1 -type d -name 'backup_2025-01-*' -exec rm -rf {} \;
通配符 * 是 shell 和 find 都支持的。只要目录名规律一致,这条命令能覆盖大部分按日期命名的场景。
如果是 20250115 这种紧凑格式,也支持:
bash复制find /data/backup -maxdepth 1 -type d -name 'backup_202501*' -exec rm -rf {} \;
更进一步,如果目录名是 2025-01-15 开头,后面还有别的信息,比如 2025-01-15_full、2025-01-15_inc,用 -name '2025-01-15*' 就都能命中。
2.3 需要删除日期区间时,用正则
严格来说,-name 的通配符只能表达“前缀、后缀”这种模糊匹配,做不到“从 2025-01-01 到 2025-06-30”这种区间判断。要实现区间,可以用 -regex。
GNU find 默认的正则类型是 emacs,和我们熟悉的 grep -E 有差异。我建议先显式指定 -regextype posix-extended,再写正则:
bash复制find /data/backup -maxdepth 1 -type d -regextype posix-extended \
-regex '.*/backup_2025-0[1-6]-[0-9]{2}' \
-exec rm -rf {} \;
这里 .*/ 是因为 -regex 匹配的是完整路径,不是文件名。0[1-6] 匹配 01 到 06 月,[0-9]{2} 匹配 01 到 31 日,整体效果就是删除 2025 年上半年所有 backup_YYYY-MM-DD 目录。
正则方案有个前提:目录名必须严格满足你定义的规则。如果有一个目录叫 backup_2025-01-15_extra,上面的正则会漏掉它,因为正则已经匹配到 } 就结束了,后面多了 _extra 就不算完全匹配。这时可以把正则改成 '.*/backup_2025-0[1-6]-[0-9]{2}.*',让后面可以跟任意内容。
提示:无论用什么方式匹配,先跑一遍带
-exec rm -rf {} \;,这是删目录最基本的职业素养。
3. 目录名不带日期:用 mtime 时间戳判断,但注意边界
3.1 mtime、ctime、atime 到底怎么选
当目录名里没有日期信息时,只能靠文件系统的时间戳。但时间戳有三个:mtime、ctime、atime。我的选择顺序是:
- 优先用 mtime。它表示目录内容的修改时间,普通文件的创建、删除、重命名、内容写入都会影响对应目录的 mtime。
- ctime 表示 inode 状态变更时间,
chmod、chown、mv都会更新它。用来判断“一天前的目录”误差不大,但严格来说它不等于创建时间。 - atime 不做清理依据,访问行为太频繁,精度和稳定性都不行。
要查一个目录的 mtime,命令是:
bash复制stat -c '%y %n' /data/logs/abc123
%y 输出人类可读的修改时间,%n 输出文件名。
3.2 删除“某一天”的目录:用 -newermt 构造时间区间
常见的需求是“把 2025 年 1 月 15 日创建的目录找出来删掉”。注意这里说的是“创建的日期”,但 Linux 并没有直接的“创建时间”字段,所以实际操作中我们通常用“该目录 mtime 落在某个区间内”来近似。
一条命令搞定:
bash复制find /data/logs -maxdepth 1 -type d \
-newermt '2025-01-15 00:00:00' \
! -newermt '2025-01-16 00:00:00' \
-exec rm -rf {} \;
-newermt 是 GNU find 的扩展参数,后面的字符串会被解析为日期时间。! -newermt '2025-01-16' 表示“不新于 2025-01-16 00:00:00”,也就是 mtime 在 2025-01-15 00:00:00 到 2025-01-15 23:59:59 范围内。
这个思路比 -mtime 更直觉,因为它直接对应日期区间。生产上我用它清理过一批“恰好某天生成的临时编译目录”。
3.3 删除“N 天以前”的目录:-mtime 的边界问题
另一个高频需求是“删除 30 天前的目录”。命令是:
bash复制find /data/logs -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;
但 -mtime 的边界很迷惑人,值得单独说。find 对时间的计算是用“当前时刻减去文件 mtime,再除以 86400 秒后取整”。-mtime +30 表示“距今超过 30 天”,-mtime 30 表示“距今 30 到 31 天之间”(不是“刚好 30 天”),-mtime -30 表示“距今 30 天以内”。
因为除法取整的边界问题,你可能只差几十分钟,目录就会从一个区间跳到另一个区间。如果你希望“今天不管多少点,只要是 30 天前的就删”,加上 -daystart 会更符合直觉:
bash复制find /data/logs -maxdepth 1 -type d -daystart -mtime +30 -exec rm -rf {} \;
-daystart 让 find 从今天零点开始计算时间差,避免凌晨执行定时任务时因为“今天”的判断边界导致误删或漏删。
3.4 目录 mtime 的隐蔽坑:子目录里的文件变化不会改父目录
这是我踩过最狠的一个坑,必须单独讲。
很多人会直觉地认为:目录里有个文件改动了,那目录的 mtime 也会变。实际不是。目录的 mtime 只会在“直接子项”发生变化时更新,包括直接在它下面创建、删除、重命名文件或子目录。子目录里面的文件如果有改动,父目录的 mtime 完全不受影响。
举个例子:
bash复制mkdir -p /data/a/b
touch /data/a/b/file.txt
/data/a 和 /data/a/b 的 mtime 都会被 touch 操作影响吗?不会,或者说不一定。执行 touch /data/a/b/file.txt 时,受影响的是 /data/a/b 这个目录的 mtime(因为 file.txt 是它的直接子项),但 /data/a 的 mtime 不会因为 b/file.txt 的内容变化而变化。只有在 /data/a 下面创建、删除或重命名 b 这个子项时,/data/a 的 mtime 才会更新。
这意味着,按 mtime 删旧目录时,你删的可能是一个“目录本身很久没变,但里面的子目录文件刚刚更新”的目录。在我遇到的那个事故里,同事在 backup_old/2024/ 下的 current/ 子目录里频繁写入文件,但顶层目录 backup_old/2024 的 mtime 一直停在上次创建 current/ 的时间,导致它被当成旧目录列入了清理名单。
所以,如果目录层级深、内容会不断更新,按目录 mtime 清理有风险。更稳妥的方式是根据目录名里的日期来删,或者至少在脚本里额外检查一层“最深层子文件的新旧程度”,但那样复杂度就上去了。我的建议是:生产环境尽量不要对深层次目录直接 -mtime +N,如果一定要用,先在日志里打印出将要删除的路径,人工过一眼。
4. 删目录的真实事故复盘:那些年我用错的 find 参数
4.1 误删事故:我把“刚更新过”的目录当成旧目录删了
我在开头提到的备份盘告警事故,完整复盘是这样的:备份目录命名规则是 backup_YYYY-MM-DD,但运维团队后来引入了一个 rsync 同步任务,会在某些历史备份目录的 latest/ 子目录里写入增量数据,顶层目录的 mtime 因为这个写入操作变成了前一天。我当时执行的是:
bash复制find /backup -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;
结果,一个标记日期在 30 天前、但里面 latest/ 子目录刚被写入增量数据的备份目录被整个删除。那次之后,我把生产环境的清理策略改成:
- 目录名里有日期的,先按目录名匹配。
- 没有日期的,按 mtime,但要额外确认目录下面没有“最近 N 天主动更新的标记文件”。
- 任何批量删除,第一次执行必须只打印,不真正删除。
4.2 参数顺序真的会影响结果
find 的表达式是“短路求值”的,参数顺序会改变行为。最典型的是:
bash复制find /data -type d -exec rm -rf {} \; -print
这条命令不会打印任何东西。因为 -exec rm -rf {} \; 执行成功后,整个表达式才继续求值,但目录已经被删了,find 自然不会再对该路径执行 -print。如果你把 -print 放在 -exec 前面:
bash复制find /data -type d -print -exec rm -rf {} \;
这样会在删除前把路径打印出来。先打印再执行,是删除命令的黄金法则。
还有一个常见误用是把 -delete 当成 rm -rf。find -delete 对空目录和文件有效,但对非空目录会直接报错:find: cannot delete '/data/xxx': Directory not empty。所以,如果你要删的是“目录及其下所有文件”,别用 -delete,老老实实用 -exec rm -rf {} \;。
4.3 特殊目录名:以 - 开头、带空格、带换行符
目录名如果是以 - 开头,rm -rf 会把它当成选项解析,比如 rm -rf -hack 会报错。虽然正常业务不会这么命名,但攻击者或者误操作可能生成这种目录。解决方法是 rm 后面加 --:
bash复制find /data -maxdepth 1 -type d -name '-*' -exec rm -rf -- {} \;
目录名带空格时,find -exec 的处理还算安全,因为 {} 是作为一个独立的参数传给命令的。但如果你把 find 结果通过管道交给 xargs rm -rf,就很容易出问题:
bash复制find /data -maxdepth 1 -type d | xargs rm -rf # 不要这样写,目录名带空格会拆成多个参数
正确写法是 -print0 配合 xargs -0:
bash复制find /data -maxdepth 1 -type d -print0 | xargs -0 rm -rf
-print0 用空字符分隔路径,xargs -0 按空字符解析路径,这样无论文件名里有什么特殊符号都不会被错误拆开。
4.4 三种“演练”方式,按需选择
真正删除之前,我至少会做一次演练。从上到下,安全级别从高到低:
bash复制# 方式一:只打印路径,不执行删除
find /data/backup -maxdepth 1 -type d -name 'backup_2025-01-*' -print
# 方式二:把结果交给 rm,但每次删除前交互确认
find /data/backup -maxdepth 1 -type d -name 'backup_2025-01-*' -ok rm -rf {} \;
# 方式三:先输出路径,再执行删除
find /data/backup -maxdepth 1 -type d -name 'backup_2025-01-*' -print -exec rm -rf {} \;
方式一的结果最清晰,但会多敲一条命令。方式二适合目录数量少、你能逐个回车确认的场景。方式三适合你已经比较有把握、但仍然想在日志里留痕的场景。我自己的习惯是:数量少,用方式一;数量多,写脚本并记录日志。
5. 把“按日期删目录”做成一个安全的生产脚本
5.1 脚本设计的核心思路
在手动命令跑通之后,我更建议把清理任务固化成脚本,再接定时任务。脚本的价值不是替代 find,而是把保护逻辑、日志记录、异常退出都处理掉,避免半夜三点 cron 执行时,因为一个意外路径把重要数据删了。
脚本设计我考虑了几个硬性要求:
- 支持几种删除模式:按目录名日期匹配、按 mtime 天数匹配、按精确日期区间匹配。
- 删除之前先打印将要删除的路径,并写入日志。
- 目标路径必须是绝对路径,且排除根目录
/、家目录$HOME等关键路径。 - 命令执行失败时能及时退出,并输出错误日志。
- 遵循
set -Eeuo pipefail,让未定义变量、管道错误都能暴露出来。
5.2 完整脚本:cleanup_dirs_by_date.sh
下面的脚本是我在生产环境简化后的版本,你可以直接复制修改。它会接收一个目标目录和几个可选参数,具体用法:
bash复制# 按目录名删除 2025-01-15 这个目录
./cleanup_dirs_by_date.sh /data/backup --date 2025-01-15
# 按目录名删除 2025-01 所有目录
./cleanup_dirs_by_date.sh /data/backup --month 2025-01
# 按 mtime 删除 30 天前的目录
./cleanup_dirs_by_date.sh /data/logs --days 30
# 只打印,不删除
./cleanup_dirs_by_date.sh /data/logs --days 30 --dry-run
脚本内容:
bash复制#!/usr/bin/env bash
set -Eeuo pipefail
# 简单的参数解析
BASE_DIR=""
ACTION=""
DATE_STR=""
MONTH_STR=""
DAYS=""
DRY_RUN=false
LOG_FILE="/var/log/cleanup_dirs.log"
usage() {
echo "Usage: $0 <base_dir> [--date YYYY-MM-DD | --month YYYY-MM | --days N] [--dry-run]"
exit 1
}
# 检查参数个数
if [ $# -lt 2 ]; then
usage
fi
BASE_DIR="$1"
shift
while [ $# -gt 0 ]; do
case "$1" in
--date)
DATE_STR="$2"
shift 2
;;
--month)
MONTH_STR="$2"
shift 2
;;
--days)
DAYS="$2"
shift 2
;;
--dry-run)
DRY_RUN=true
shift
;;
*)
usage
;;
esac
done
# 目标目录必须存在
if [ ! -d "$BASE_DIR" ]; then
echo "ERROR: Base directory $BASE_DIR does not exist."
exit 1
fi
# 保护:绝对路径,且不能是 / 或 $HOME
REAL_PATH="$(realpath "$BASE_DIR")"
if [ "$REAL_PATH" = "/" ] || [ "$REAL_PATH" = "$HOME" ]; then
echo "ERROR: Refusing to operate on $REAL_PATH"
exit 1
fi
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOG_FILE"
}
# 构造匹配条件
FIND_COND=()
if [ -n "$DATE_STR" ]; then
FIND_COND=(-maxdepth 1 -type d -name "*${DATE_STR}*")
elif [ -n "$MONTH_STR" ]; then
FIND_COND=(-maxdepth 1 -type d -name "*${MONTH_STR}*")
elif [ -n "$DAYS" ]; then
FIND_COND=(-maxdepth 1 -type d -daystart -mtime "+${DAYS}")
else
echo "ERROR: Missing --date or --month or --days."
usage
fi
log "INFO: base_dir=$REAL_PATH action_date=${DATE_STR:-} action_month=${MONTH_STR:-} days=${DAYS:-} dry_run=${DRY_RUN}"
if [ "$DRY_RUN" = true ]; then
find "$REAL_PATH" "${FIND_COND[@]}" -print
echo "DRY RUN, not deleted."
exit 0
fi
# 正式删除前,统计数量
COUNT=$(find "$REAL_PATH" "${FIND_COND[@]}" -print | wc -l)
echo "Preparing to delete $COUNT directories from $REAL_PATH"
# 先打印,再删除
find "$REAL_PATH" "${FIND_COND[@]}" -print -exec rm -rf {} \;
log "INFO: deleted $COUNT directories from $REAL_PATH"
几点说明:
realpath "$BASE_DIR"会解析成绝对路径,避免相对路径和软链接造成的误判。- 保护逻辑只排除了
/和$HOME,如果你的业务路径还有其他关键目录,比如/data、/opt,可以继续加判断。 FIND_COND数组化是为了兼容--date、--month、--days三种模式,在 bash 里用数组保存 find 参数最稳。- 真正执行删除前先统计数量并打印,已经删除的路径也会进入系统日志,方便事后审计。
5.3 挂到 cron 定时执行
脚本放到 /usr/local/bin/ 并加执行权限:
bash复制chmod +x /usr/local/bin/cleanup_dirs_by_date.sh
然后执行 crontab -e,添加一行:
cron复制0 3 * * * /usr/local/bin/cleanup_dirs_by_date.sh /data/logs --days 30 --dry-run >> /var/log/cleanup_console.log 2>&1
先用 --dry-run 跑几天,确认日志里列出的目录都正确,再把 --dry-run 去掉。我在生产环境一般会先 dry-run 观察一个完整的清理周期,确认没有误报再切换正式删除。
注意:cron 环境变量和交互 shell 不同,脚本里最好使用绝对路径,并且不要依赖
~这种展开。如果 find 命令都写着绝对路径,脚本在任何环境下执行结果一致。
6. 几个补充技巧:任意日期区间、跨目录清理、rm 以外的选择
6.1 用基准文件划定任意日期区间
find -newermt 很方便,但有些老版本 find 不支持。这时候可以创建一个基准文件,用 -newer 来判断。
比如要删除“2025 年 1 月 15 日当天”的目录,可以创建两个基准文件:
bash复制touch -t 202501150000 /tmp/ref_start
touch -t 202501160000 /tmp/ref_end
find /data -maxdepth 1 -type d \
-newer /tmp/ref_start \
! -newer /tmp/ref_end \
-exec rm -rf {} \;
-newer 是 POSIX find 的标准参数,兼容性比 -newermt 好。这个思路在嵌入式环境或者最小化安装的系统上很实用,因为很多精简系统里的 find 未必支持 -newermt。
6.2 跨目录批量清理
如果清理范围不局限于一个根目录,而是多个路径,比如 /data/backup、/opt/archive、/srv/storage,可以在一个脚本里循环:
bash复制for dir in /data/backup /opt/archive /srv/storage; do
find "$dir" -maxdepth 1 -type d -name '2025-01-*' -exec rm -rf {} \;
done
循环的优点是每个目录可以单独设置不同的保留策略。比如 /data/backup 保留 7 天,/opt/archive 保留 30 天,这种没有统一规律的场景,脚本比一条 find 更灵活。
6.3 考虑用移动代替删除
有些场景下,直接 rm -rf 其实是下策。如果磁盘空间紧张是因为目录太大,想迅速腾空间,删掉最快;但如果只是“暂时不用”,我更建议先移动到 /data/trash/ 目录:
bash复制find /data/backup -maxdepth 1 -type d -name '2025-01-*' -exec mv {} /data/trash/ \;
这样做的优点是:万一误判,还能从回收目录恢复。缺点是需要额外的磁盘空间来容纳这些数据。磁盘空间本来就紧张时,这个方案不可行。我的经验是:磁盘使用率超过 80%,直接删;如果还有余量,优先移动观察一两天再确定。
最后说两句实在的
在 Linux 下删除指定日期目录,命令本身五分钟就能学会,但真正让它可靠的是你对自己的系统了解多少:目录命名是否规范?时间戳是否可靠?有没有隐藏的写入任务?这些坑我已经在文章里全部踩过,也给了对应的解决方法。你现在要做的是找一台测试机,把 -print 跑一遍,看看输出是不是你真正想删的那些目录。只要这一步做对了,后面接 cron、接脚本,都不会再出大事。
