补充说明
该脚本用于批量清理指定目录(默认当前目录)下的所有空目录。使用 find -depth -type d -empty 以深度优先顺序扫描,交互式确认后逐目录 rmdir 删除,成功/失败数量一目了然。
安全设计:仅使用 rmdir(只允许删除空目录)而不使用 rm -rf,即使误操作也不会删掉任何文件内容;深度优先遍历保证先删内层空目录再删外层,一次扫描即可清干净整棵嵌套空目录树。
适合定期清理 NAS 上的空套娃文件夹、转码/迁移工具残留的空目录,或与文件迁移/转码脚本配合做收尾。
一键脚本
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/lx_rm_empty.sh)
bash <(curl -sL ../sh/lx_rm_empty.sh)
使用示例(脚本名以实际部署路径为准,以下为本地脚本名):
chmod +x lx_rm_empty.sh
# 当前目录交互式
./lx_rm_empty.sh
# 指定目录交互式
./lx_rm_empty.sh /vol2/1000/mydisk/Video/Video-Album
# 指定目录免确认删除
./lx_rm_empty.sh /vol2/1000/mydisk/Video/Video-Album --y
交互模式扫描到空目录后会先列出清单,确认(输入 y / yes)才开始删除;n / no / 直接回车即取消。
运行环境
- 系统:Linux / macOS(Windows 可借助 WSL / Git-Bash)
- 依赖:
find/rmdir/read/pwd(均属 coreutils / findutils 标准组件,通常预装) - 无需任何第三方软件,不涉及文件内容读写
功能特点
- 深度优先扫描空目录(
find -depth -type d -empty),一次清理整棵空目录树 - 交互确认 +
--y免确认双模式,适合脚本化 / cron 定时调用 - 仅用
rmdir删除空目录,绝不触碰任何文件,误操作风险低 - 删除前完整列出待删清单,删除结果逐条打印并汇总(成功 N / 总计 M)
- 目录不存在、无空目录等边界情况均有明确提示
- 首个参数省略时默认清理当前目录
- 彩色日志分级:信息 / 成功 / 警告 / 错误 一目了然
- 内置动画与空输入/无效输入交互提示,菜单体验一致
参数说明
| 参数 | 含义 | 默认值 | 示例 |
|---|---|---|---|
$1 |
目标目录 | 当前目录 | /vol2/1000/mydisk/Video/Video-Album |
$2 |
--y 免确认直接删除 |
需交互确认 | --y |
交互模式下输入
y/yes确认删除;n/no/ 空回车取消;其他输入走无效提示后重新选择。
删除逻辑详解
扫描空目录:
find "目标目录" -depth -type d -empty 2>/dev/null
| 片段 | 作用 |
|---|---|
-depth |
深度优先,先输出深层目录后输出上层目录 |
-type d |
仅匹配目录 |
-empty |
匹配空文件 / 空目录(这里只取空目录) |
2>/dev/null |
忽略权限不足等报错,跳过无法读取的目录 |
删除空目录(逐目录执行,允许失败不中断):
rmdir -- "空目录路径"
rmdir只删除空的目录;因并发产生的新文件、权限不足等导致删除失败时,脚本打印删除失败并继续处理其余目录,最后汇总成功数。
配套手工命令(--y 等价的纯 find 实现):
find "目标目录" -depth -type d -empty -delete
注意事项
- 只删空目录、不删文件:因
rmdir语义,任何非空目录都会被跳过并记为删除失败 - 深度优先 + 顺序执行可级联清空嵌套空目录;若外层目录被内层残留塞满非空内容,外层会保留
--y模式无确认直接删除,批量执行前建议先交互运行一次确认目录范围- 隐藏目录
.foo等同样会被扫描与删除,属预期行为 - 对权限不足的目录
find与rmdir均已做静默容错,不影响其他目录处理 - 链接指向:
-empty不匹配符号链接,符号链接目录不会被删除 - 目标目录不存在时打印
目录不存在并返回 1
脚本源码
#!/bin/bash
set -uo pipefail
gl_hui='\033[38;5;59m'
gl_hong='\033[38;5;9m'
gl_lv='\033[38;5;10m'
gl_huang='\033[38;5;11m'
gl_lan='\033[38;5;32m'
gl_bai='\033[38;5;15m'
gl_zi='\033[38;5;13m'
gl_bufan='\033[38;5;14m'
log_info() { echo -e "${gl_lan}[信息]${gl_bai} $*"; }
log_ok() { echo -e "${gl_lv}[成功]${gl_bai} $*"; }
log_warn() { echo -e "${gl_huang}[警告]${gl_bai} $*"; }
log_error() { echo -e "${gl_hong}[错误]${gl_bai} $*" >&2; }
break_end() {
echo -e "${gl_lv}操作完成${gl_bai}"
echo -e "${gl_bai}按任意键继续 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
read -r -n 1 -s -p ""
echo ""
clear
}
sleep_fractional() {
local seconds=$1
if sleep "$seconds" 2>/dev/null; then return 0; fi
if command -v perl >/dev/null 2>&1; then perl -e "select(undef, undef, undef, $seconds)"; return 0; fi
if command -v python3 >/dev/null 2>&1; then python3 -c "import time; time.sleep($seconds)"; return 0; fi
if command -v python >/dev/null 2>&1; then python -c "import time; time.sleep($seconds)"; return 0; fi
local int_seconds=$(echo "$seconds" | awk '{print int($1+0.999)}')
sleep "$int_seconds"
}
cancel_return() {
local menu_name="${1:-退出脚本}"
echo -ne "${gl_lv}即将返回 ${gl_huang}${menu_name} ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.5
echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.6
echo ""
clear
}
exit_script() {
local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
local dots=(
"${gl_hong}."
"${gl_huang}."
"${gl_lv}."
"${gl_bufan}."
"${gl_zi}."
)
local dot_buffer=""
local frame_len=${#frames[@]}
local dot_idx=0
local total_dots=${#dots[@]}
for ((i=0; i<20; i++)); do
if (( i > 0 && i % 3 == 0 && dot_idx < total_dots )); then
dot_buffer+=${dots[$dot_idx]}
((dot_idx++))
fi
echo -ne "\r\033[K${gl_bufan}${frames[i % frame_len]}${gl_bai} 正在退出 ${dot_buffer}"
sleep_fractional 0.06
done
echo -e "\r\033[K${gl_lv}✓${gl_bai} 成功退出\n"
clear
exit 0
}
handle_y_n() {
echo -ne "\r${gl_hong}无效的选择,请输入 ${gl_bai}(${gl_lv}y${gl_bai}或${gl_hong}N${gl_bai}) ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.3
echo -ne "\r${gl_huang}无效的选择,请输入 ${gl_bai}(${gl_lv}y${gl_bai}或${gl_hong}N${gl_bai}) ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.3
echo -ne "\r${gl_lv}无效的选择,请输入 ${gl_bai}(${gl_lv}y${gl_bai}或${gl_hong}N${gl_bai}) ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.6
echo ""
return 2
}
exit_animation() {
echo -ne "\r${gl_lv}即将退出 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.5
echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.6
echo ""
clear
}
cancel_empty() {
local menu_name="${1:-上一级选单}"
echo -e "${gl_hong}空输入,返回 ${gl_huang}${menu_name} ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.5
echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.6
echo ""
clear
}
handle_invalid_input() {
echo -ne "\r\033[K${gl_huang}无效的输入,请重新输入! ${gl_zi} 1 ${gl_huang} 秒后返回"
sleep_fractional 1
echo -ne "\r\033[K${gl_lv}无效的输入,请重新输入! ${gl_zi}0${gl_lv} 秒后返回"
sleep_fractional 0.5
echo -ne "\r\033[K"
return 2
}
del_empty_dir() {
local target_dir
local auto_confirm="0"
local -a empty_dirs=()
if [[ $# -ge 1 ]]; then
target_dir="$1"
else
target_dir="$(pwd)"
fi
if [[ $# -ge 2 && "$2" == "--y" ]]; then
auto_confirm="1"
fi
if [[ ! -d "${target_dir}" ]]; then
log_error "目录不存在:${target_dir}"
return 1
fi
log_info "扫描目录:${target_dir}"
log_info "正在查找所有空目录(深度优先) ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
while IFS= read -r line; do
empty_dirs+=("$line")
done < <(find "${target_dir}" -depth -type d -empty 2>/dev/null)
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local count=${#empty_dirs[@]}
if [[ ${count} -eq 0 ]]; then
log_ok "未发现任何空目录"
break_end
return 0
fi
log_warn "一共找到 ${gl_huang}${count}${gl_bai} 个空目录:"
for d in "${empty_dirs[@]}"; do
echo -e " ${gl_zi}${d}${gl_bai}"
done
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local answer
if [[ "${auto_confirm}" -eq 1 ]]; then
answer="y"
log_warn "免交互模式,直接开始删除..."
else
read -r -e -p "$(echo -e "${gl_bai}确认删除以上全部空目录?(${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" answer
answer="${answer,,}"
case "${answer}" in
y|yes) ;;
n|no|"")
log_info "已取消删除操作"
break_end
return 0
;;
*)
handle_y_n
return 2
;;
esac
fi
local del_cnt=0
log_info "开始删除空目录:"
for d in "${empty_dirs[@]}"; do
if rmdir -- "${d}" 2>/dev/null; then
log_ok "已删除:${d}"
((del_cnt++))
else
log_error "删除失败:${d}"
fi
done
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
log_ok "本次成功删除 ${gl_lv}${del_cnt}${gl_bai} / ${gl_huang}${count}${gl_bai} 个空目录"
break_end
}
del_empty_dir "$@"
创建本地脚本
new_script="lx_rm_empty.sh"
cat > "$new_script" << 'EOF'
#!/bin/bash
# 粘贴上方脚本源码
EOF
# 保留本地脚本,去掉 rm -f "$new_script"
chmod +x "$new_script" && ./"$new_script" && rm -f "$new_script"
相关命令
- lx_rm_empty 👈 当前所在位置
- batch_mv_subdir
- ffmpeg_video_hvc1_all 转码后清理残留空目录
- batch_replace_menu