smb_unmount
Linux 卸载 Samba 共享目录
一键脚本
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/smb_unmount.sh)
bash <(curl -sL ../sh/smb_unmount.sh)
效果预览
补充说明
该脚本用于在 Linux 系统上交互式卸载已挂载的 SMB/CIFS 共享目录,自动扫描当前挂载点、处理占用进程、清理 fstab 条目和相关凭据文件。
功能特点
- root 权限自动检测(root_use 函数),非 root 用户提示后返回
- 自动扫描当前已挂载的 CIFS 共享(支持 findmnt 和 mount 双方案)
- 列表展示所有挂载点及对应的远程源地址
- 交互式选择要卸载的共享(输入序号,0 退出)
- 卸载前二次确认,避免误操作
- 检测挂载点是否被进程占用(lsof)
- 占用时显示当前使用进程列表(最多 5 行)
- 提供强制卸载选项(fuser -km 终止占用进程)
- 卸载成功后自动清理 /etc/fstab 条目(先备份原文件)
- 解析 fstab 中的 credentials 参数,可选删除凭据文件
- 卸载完成后可选删除空挂载点目录
- 普通卸载失败时自动尝试 lazy 强制卸载(umount -l)
- 完全失败时提供手动修复命令参考(fuser/umount -f)
- 支持输出序号验证([0-9] 模式匹配),输入无效时提示重新选择
输出说明
脚本输出包含以下字段:
| 字段 | 说明 |
|---|---|
| 根用户检查 | 检测 root 权限,非 root 用户提示后返回 |
| 挂载点扫描 | 显示当前已挂载的所有 CIFS 共享列表 |
| 共享列表 | 逐行显示序号、本地挂载点、远程源地址 |
| 卸载选择 | 用户输入序号选择要卸载的共享 |
| 进程检测 | 检查挂载点是否被进程占用(lsof) |
| 强制卸载 | 用户确认后执行 fuser -km 终止占用进程 |
| 卸载结果 | 显示卸载成功/失败/lazy 卸载状态 |
| fstab 清理 | 显示 /etc/fstab 备份和条目移除结果 |
| 凭据文件清理 | 可选删除对应的凭据文件 |
| 空目录清理 | 可选删除已卸载的空挂载点目录 |
注意事项
- 脚本需要 root 权限来执行 umount、修改 /etc/fstab 和删除凭据文件
- 卸载前建议确保挂载点无进程占用,否则 lsof 会检测到并提示
- 强制卸载(fuser -km)会终止所有使用该挂载点的进程,请谨慎确认
- /etc/fstab 自动备份为 /etc/fstab.bak.unmount.时间戳,不会覆盖
- 凭据文件删除需用户二次确认,默认不删除
- 空目录删除需用户二次确认,默认不删除
- 普通 umount 失败后自动尝试 umount -l(lazy 卸载),立即断开但清理后台进程
- 脚本依赖 findmnt(util-linux 包)扫描挂载点,不存在时回退到 mount 命令
- 序号输入校验:仅接受 1-N 的数字,0 为退出,其他字符提示重复输入
脚本源码
#!/bin/bash
set -uo pipefail
list_color_init() {
export gl_hui=$'\033[38;5;59m'
export gl_hong=$'\033[38;5;9m'
export gl_lv=$'\033[38;5;10m'
export gl_huang=$'\033[38;5;11m'
export gl_lan=$'\033[38;5;32m'
export gl_bai=$'\033[38;5;15m'
export gl_zi=$'\033[38;5;13m'
export gl_bufan=$'\033[38;5;14m'
export gl_cheng=$'\033[38;5;208m'
export reset=$'\033[0m'
}
list_color_init
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 -r -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:-上一级选单}"
local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
local dots=(
"${gl_hong}."
"${gl_huang}."
"${gl_lv}."
"${gl_bufan}."
"${gl_zi}."
"${gl_cheng}."
)
local dot_buffer=""
local frame_len=${#frames[@]}
local dot_idx=0
local total_dots=6
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}即将返回 ${gl_huang}${menu_name} ${dot_buffer}"
sleep_fractional 0.06
done
echo -e "\r\033[K${gl_lv}✓${gl_bai} 成功返回${gl_huang}${menu_name}${gl_bai} \n"
clear
}
exit_animation() {
local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
local dots=(
"${gl_hong}."
"${gl_huang}."
"${gl_lv}."
"${gl_bufan}."
"${gl_zi}."
"${gl_cheng}."
)
local dot_buffer=""
local frame_len=${#frames[@]}
local dot_idx=0
local total_dots=6
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_script() {
local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
local dots=(
"${gl_hong}."
"${gl_huang}."
"${gl_lv}."
"${gl_bufan}."
"${gl_zi}."
"${gl_cheng}."
)
local dot_buffer=""
local frame_len=${#frames[@]}
local dot_idx=0
local total_dots=6
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
}
root_use() {
clear
if [ "$EUID" -ne 0 ]; then
echo -e "\n${gl_zi}>>> ROOT登录检查 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_huang}提示: ${gl_bai}该功能需要root用户才能运行!"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
return 1
fi
return 0
}
handle_invalid_input() {
echo -e "${gl_huang}[提示]${gl_bai} 输入无效,请输入正确的序号"
}
unmount_samba_shares() {
local mounted_shares target_mount choice
root_use
clear
echo -e "${gl_zi}>>> 卸载Samba/CIFS共享${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
get_mounted_cifs() {
if command -v findmnt &>/dev/null; then
mapfile -t mounted_shares < <(findmnt -t cifs -o TARGET --noheadings 2>/dev/null | grep -v '^$' | sort)
else
mapfile -t mounted_shares < <(mount -t cifs 2>/dev/null | awk '{print $3}' | sort)
fi
}
cleanup_fstab() {
local mount_pattern="$1"
mount_pattern=$(echo "$mount_pattern" | sed 's/[\/&]/\\&/g')
cp /etc/fstab /etc/fstab.bak.unmount.$(date +%Y%m%d_%H%M%S) 2>/dev/null
if grep -q "^[^#].*$mount_pattern.*cifs" /etc/fstab; then
local fstab_line
fstab_line=$(grep "^[^#].*$mount_pattern.*cifs" /etc/fstab)
if echo "$fstab_line" | grep -q "credentials="; then
local cred_path
cred_path=$(echo "$fstab_line" | grep -o "credentials=[^, ]*" | cut -d= -f2)
if [[ -f "$cred_path" ]]; then
read -r -e -p "$(echo -e "${gl_bai}是否删除凭据文件 ${gl_huang}$cred_path${gl_bai}? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" del_cred
if [[ $del_cred =~ ^[Yy]$ ]]; then
rm -f "$cred_path" && log_ok "已删除凭据文件"
fi
fi
fi
sed -i "\|^[^#].*$mount_pattern.*cifs|d" /etc/fstab
log_ok "已从 /etc/fstab 移除挂载项"
fi
}
log_info "正在扫描已挂载的 Samba/CIFS 共享 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
get_mounted_cifs
if [ ${#mounted_shares[@]} -eq 0 ]; then
log_ok "当前没有挂载任何 Samba/CIFS 共享"
return 0
fi
echo -e "${gl_huang}发现以下挂载点:${gl_bai}"
for idx in "${!mounted_shares[@]}"; do
local share_info=""
if command -v findmnt &>/dev/null; then
share_info=$(findmnt -n -o SOURCE --target "${mounted_shares[$idx]}" 2>/dev/null)
else
share_info=$(mount | grep "on ${mounted_shares[$idx]} " | head -1 | awk '{print $1}')
fi
echo -e " ${gl_bufan}$((idx + 1)).${gl_bai} ${mounted_shares[$idx]} ${gl_huang}($share_info)${gl_bai}"
done
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
while true; do
read -r -e -p "$(echo -e "${gl_bai}请选择要卸载的序号 (${gl_hong}0${gl_bai}退出) : ")" choice
case $choice in
0) exit_script ;;
*[0-9]*)
if ((choice > 0 && choice <= ${#mounted_shares[@]})); then
target_mount="${mounted_shares[$((choice - 1))]}"
read -r -e -p "$(echo -e "${gl_bai}确定要卸载 ${gl_huang}$target_mount${gl_bai} 吗? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" confirm
if [[ $confirm =~ ^[Yy]$ ]]; then
break
else
log_info "操作取消"
continue
fi
else
handle_invalid_input
fi
;;
*)
handle_invalid_input
;;
esac
done
if lsof "$target_mount" &>/dev/null; then
log_warn "挂载点 $target_mount 正在被使用"
echo -e "${gl_bai}使用进程:"
lsof "$target_mount" | head -5
read -r -e -p "$(echo -e "${gl_bai}是否强制卸载? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" force_unmount
if [[ $force_unmount =~ ^[Yy]$ ]]; then
log_info "正在强制卸载 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
if fuser -km "$target_mount" 2>/dev/null; then
sleep_fractional 2
fi
else
log_info "操作取消"
return 0
fi
fi
log_info "正在卸载: $target_mount"
if umount "$target_mount" 2>/dev/null; then
log_ok "卸载成功"
cleanup_fstab "$target_mount"
if [ -d "$target_mount" ] && [ -z "$(ls -A "$target_mount" 2>/dev/null)" ]; then
read -r -e -p "$(echo -e "${gl_bai}是否删除空目录 ${gl_huang}$target_mount${gl_bai}? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" del_dir
if [[ $del_dir =~ ^[Yy]$ ]]; then
rmdir "$target_mount" 2>/dev/null && log_ok "已删除空目录"
fi
fi
else
log_warn "普通卸载失败,尝试强制卸载 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
if umount -l "$target_mount" 2>/dev/null; then
log_ok "强制卸载完成"
cleanup_fstab "$target_mount"
else
log_error "卸载失败,挂载点可能仍在被使用"
echo -e "${gl_bai}尝试以下命令手动解决:"
echo -e "${gl_huang} fuser -m -v \"$target_mount\"${gl_bai} # 查看使用进程"
echo -e "${gl_huang} umount -f \"$target_mount\"${gl_bai} # 强制卸载"
return 1
fi
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
}
unmount_samba_shares
创建本地脚本
new_script="new_test.sh"
cat > "$new_script" << 'EOF'
#!/bin/bash
# 粘贴脚本源码
EOF
# 保留本地脚本,去掉 rm -f "$new_script"
chmod +x "$new_script" && ./"$new_script" && rm -f "$new_script"