推荐将某个文件夹专门放置应用,然后在里面,放置这两个sh文件:
在该目录下打开终端,使用
sudo ./install.sh 安装,选择序号,然后一直回车
sudo ./uninstall.sh 卸载,选择序号卸载
对需要沙箱的应用,在/usr/share/applications快捷方式的应用路径后面添加 --no-sandbox,做了这步要注销登陆重新进入才生效
安装
install.sh:
sh
#!/usr/bin/env bash
set -euo pipefail
# === 工具函数 ===
clean_name() {
# 去掉 .AppImage 后缀,替换空格/点为下划线,连字符压缩为1个,转小写 # 中文注释
echo "$1" | sed -E 's/\.(AppImage|appimage)$//i' | tr '. ' '_' | tr -s '_' | tr '[:upper:]' '[:lower:]'
}
say() { echo -e "$*"; } # 统一中文输出 # 中文注释
# === 必须 root 运行 ===
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
say "请使用 sudo 运行本脚本"
exit 1
fi
# === 获取本脚本名称,避免把自己加进列表 ===
SCRIPT_NAME="$(basename "$0")"
# === 扫描当前目录的所有 AppImage 和 可执行文件 并让用户选择 ===
# 使用 -executable 查找可执行文件,排除目录和本脚本自身
mapfile -d '' FILES < <(find . -maxdepth 1 -type f \( -iname '*.AppImage' -o -executable \) ! -name "$SCRIPT_NAME" -print0 | sort -z)
if [ "${#FILES[@]}" -eq 0 ]; then
say "错误:当前目录未找到任何 AppImage 或可执行二进制文件"
exit 1
fi
say "检测到以下可执行程序:"
i=1
for f in "${FILES[@]}"; do
fname="${f#./}"
fsize="$(stat -c '%s' "$fname" 2>/dev/null || echo 0)"
# 人性化大小
if [ "$fsize" -ge 1073741824 ]; then sz="$(awk "BEGIN{printf \"%.2fG\", $fsize/1073741824}")"
elif [ "$fsize" -ge 1048576 ]; then sz="$(awk "BEGIN{printf \"%.2fM\", $fsize/1048576}")"
elif [ "$fsize" -ge 1024 ]; then sz="$(awk "BEGIN{printf \"%.2fK\", $fsize/1024}")"
else sz="${fsize}B"; fi
# 标记文件类型
if [[ "$fname" =~ \.AppImage$ ]] || [[ "$fname" =~ \.appimage$ ]]; then
type_tag="[AppImage]"
else
type_tag="[二进制]"
fi
say " $i) $type_tag ${fname} (${sz})"
i=$((i+1))
done
read -p "请输入要安装的序号 [1-${#FILES[@]}]: " PICK
if ! [[ "$PICK" =~ ^[0-9]+$ ]] || [ "$PICK" -lt 1 ] || [ "$PICK" -gt "${#FILES[@]}" ]; then
say "错误:输入的序号无效"
exit 1
fi
FILE_REL="${FILES[$((PICK-1))]}"
FILE_PATH="$(readlink -f "${FILE_REL#./}")" # 绝对路径 # 中文注释
FILE_BASE="$(basename "$FILE_PATH")" # 文件名(含扩展) # 中文注释
CLEAN_NAME="$(clean_name "$FILE_BASE")" # 清洗名 # 中文注释
WORKDIR="$(pwd -P)" # 当前目录 # 中文注释
chmod +x "$FILE_PATH" # 确保可执行 # 中文注释
# 判断是否为 AppImage
IS_APPIMAGE=false
if [[ "$FILE_BASE" =~ \.AppImage$ ]] || [[ "$FILE_BASE" =~ \.appimage$ ]]; then
IS_APPIMAGE=true
fi
# === 先创建 /opt 目标目录(图标与应用都存这里) ===
TARGET_DIR="/opt/$CLEAN_NAME"
mkdir -p "$TARGET_DIR" # 创建应用目录 # 中文注释
# === 提取或生成图标 ===
ICON_LOCAL_PATH="" # /opt/<clean_name>/ 内的图标绝对路径 # 中文注释
if [ "$IS_APPIMAGE" = true ]; then
say "正在挂载 AppImage(不解包)以提取图标……"
MNT="" # 挂载点路径 # 中文注释
FUSE_PID="" # FUSE 进程 PID(用于回收) # 中文注释
cleanup_mount() { # 收尾函数 # 中文注释
if [ -n "${MNT:-}" ] && mountpoint -q "$MNT"; then
umount "$MNT" || true
fi
[ -n "${MNT:-}" ] && rmdir "$MNT" 2>/dev/null || true
if [ -n "${FUSE_PID:-}" ]; then
kill -INT "$FUSE_PID" 2>/dev/null || true
wait "$FUSE_PID" 2>/dev/null || true
fi
}
trap cleanup_mount EXIT
# 1) 尝试内核只读挂载(loop+offset)
USE_KERNEL_MOUNT=false
if OFF="$("$FILE_PATH" --appimage-offset 2>/dev/null)"; then
if [[ "$OFF" =~ ^[0-9]+$ ]]; then
MNT="$(mktemp -d)"
if mount -o "ro,loop,offset=$OFF" -t squashfs "$FILE_PATH" "$MNT" 2>/dev/null; then
USE_KERNEL_MOUNT=true
say "已使用内核只读挂载方式。"
fi
fi
fi
# 2) 若失败,回退到 FUSE 挂载(--appimage-mount)
if [ "$USE_KERNEL_MOUNT" = false ]; then
TMP_OUT="$(mktemp)"
"$FILE_PATH" --appimage-mount >"$TMP_OUT" & FUSE_PID=$!
for _ in $(seq 1 50); do
if [ -s "$TMP_OUT" ]; then break; fi
sleep 0.1
done
if [ ! -s "$TMP_OUT" ]; then
say "错误:无法挂载 AppImage,提取图标失败。"
exit 1
fi
MNT="$(head -n1 "$TMP_OUT")"
rm -f "$TMP_OUT"
say "已使用 FUSE 临时挂载方式。"
fi
# 3) 搜索规则:先根目录(不递归)找图片 → 若无,再到 usr/share/ 下找第一张
find_first_image() {
local root="$1"
find "$root" -maxdepth 1 -type f \( -iname '*.svg' -o -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.ico' \) | head -n1
}
find_first_image_in_usr_share() {
local root="$1"
local base="$root/usr/share"
[ -d "$base" ] || return 0
find "$base" -type f \( -iname '*.svg' -o -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.ico' \) | head -n1
}
CAND="$(find_first_image "$MNT")"
if [ -z "${CAND:-}" ]; then
CAND="$(find_first_image_in_usr_share "$MNT")"
fi
if [ -n "${CAND:-}" ]; then
EXT="${CAND##*.}"
ICON_LOCAL_PATH="$TARGET_DIR/${CLEAN_NAME}.${EXT}"
cp -f "$CAND" "$ICON_LOCAL_PATH"
chmod 644 "$ICON_LOCAL_PATH" || true
say "已提取图标到:$ICON_LOCAL_PATH"
fi
else
say "处理普通二进制文件,尝试寻找同级目录下的同名图标……"
# 尝试在当前目录找同名的 .png 或 .svg
BASENAME_NOEXT="${FILE_BASE%.*}"
if [ -f "$WORKDIR/$BASENAME_NOEXT.png" ]; then
ICON_LOCAL_PATH="$TARGET_DIR/${CLEAN_NAME}.png"
cp -f "$WORKDIR/$BASENAME_NOEXT.png" "$ICON_LOCAL_PATH"
chmod 644 "$ICON_LOCAL_PATH" || true
say "找到同名图片,已复制为图标:$ICON_LOCAL_PATH"
elif [ -f "$WORKDIR/$BASENAME_NOEXT.svg" ]; then
ICON_LOCAL_PATH="$TARGET_DIR/${CLEAN_NAME}.svg"
cp -f "$WORKDIR/$BASENAME_NOEXT.svg" "$ICON_LOCAL_PATH"
chmod 644 "$ICON_LOCAL_PATH" || true
say "找到同名矢量图,已复制为图标:$ICON_LOCAL_PATH"
fi
fi
# 兜底:如果 AppImage 没提取出图片,或者二进制没找到同名图片,则生成默认 SVG
if [ -z "$ICON_LOCAL_PATH" ]; then
ICON_LOCAL_PATH="$TARGET_DIR/${CLEAN_NAME}.svg"
cat > "$ICON_LOCAL_PATH" <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect x="5" y="5" width="90" height="90" rx="20" ry="20" fill="#4A90E2"/>
<circle cx="50" cy="50" r="30" fill="#FFFFFF" opacity="0.2"/>
<path d="M35 35 L65 35 L65 65 L35 65 Z" fill="none" stroke="#FFFFFF" stroke-width="6"/>
<path d="M42 42 L58 42 L58 58 L42 58 Z" fill="#FFFFFF"/>
</svg>
EOF
chmod 644 "$ICON_LOCAL_PATH" || true
say "未找到匹配的图标,已生成默认图标:$ICON_LOCAL_PATH"
fi
# === 交互信息 ===
BASENAME="$(basename "$FILE_BASE" .AppImage)"
# 二进制文件如果带后缀,继续去掉
BASENAME="${BASENAME%.*}"
read -p "请输入应用显示名称(回车默认:$BASENAME): " APP_NAME
APP_NAME=${APP_NAME:-$BASENAME}
read -p "请输入应用描述(回车默认:Application $APP_NAME): " DESCRIPTION
DESCRIPTION=${DESCRIPTION:-"Application $APP_NAME"}
say "请选择应用分类:"
say "1) 工具 Utility"
say "2) 图形 Graphics"
say "3) 开发 Development"
say "4) 办公 Office"
say "5) 网络 Network"
read -p "请输入序号(回车默认:1): " CATEGORY_NUM
case ${CATEGORY_NUM:-1} in
2) CATEGORY="Graphics;2DGraphics;3DGraphics;";;
3) CATEGORY="Development;IDE;";;
4) CATEGORY="Office;";;
5) CATEGORY="Network;";;
*) CATEGORY="Utility;";;
esac
# === 可选:检查并安装 libfuse2(仅针对 AppImage) ===
if [ "$IS_APPIMAGE" = true ]; then
say "检查 libfuse2……"
if ! dpkg -s libfuse2 >/dev/null 2>&1; then
say "未检测到 libfuse2,正在安装(不执行 apt-get update)……"
if ! apt-get install -y libfuse2; then
say "安装 libfuse2 失败(可能需要你稍后手动执行 apt-get update 再安装)。"
fi
else
say "已安装 libfuse2。"
fi
fi
# === 将程序拷贝到 /opt/<clean_name>/ 并赋权 ===
say "正在安装到:$TARGET_DIR"
cp -f "$FILE_PATH" "$TARGET_DIR/"
chmod +x "$TARGET_DIR/$FILE_BASE"
# === 创建 .desktop:Exec 和 Icon 都指向 /opt/<clean_name>/ ===
DESKTOP_FILE="/usr/share/applications/${CLEAN_NAME}.desktop"
ICON_ABS="$ICON_LOCAL_PATH"
say "正在创建桌面启动器:$DESKTOP_FILE"
cat > "$DESKTOP_FILE" <<EOF
[Desktop Entry]
Version=1.0
Name=$APP_NAME
Comment=$DESCRIPTION
Exec=$TARGET_DIR/$FILE_BASE
Icon=$ICON_ABS
Terminal=false
Type=Application
Categories=$CATEGORY
StartupNotify=true
EOF
# === 刷新 desktop 数据库 ===
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database /usr/share/applications >/dev/null 2>&1 || true
fi
say "安装完成!"
say "—— 主程序:$TARGET_DIR/$FILE_BASE"
say "—— 图标 :$ICON_ABS"
say "—— 启动器:$DESKTOP_FILE"
say "如图标未立即出现,可尝试注销/重启图形会话。"
卸载
uninstall.sh:
sh
#!/bin/bash
# =========================
# AppImage 卸载脚本(中文版)
# 从 /usr/share/applications 中筛选 Exec 行含 ".AppImage" 的桌面项
# 并按所选项删除 /opt/<app_id>、对应 .desktop、以及 hicolor 主题下的图标文件
# =========================
# 必须用 sudo/root 运行
if [ "$EUID" -ne 0 ]; then
echo "请使用 sudo 运行本脚本"
exit 1
fi
echo "已安装的 AppImage 应用:"
echo "------------------------"
counter=1
declare -a desktop_files
# 列出所有包含 .AppImage 的 .desktop(更稳健:匹配 Exec 行)
while IFS= read -r file; do
if grep -qE '^Exec=.*\.AppImage' "$file"; then
# 优先展示 Name=,没有就用文件名
name=$(grep -E '^Name=' "$file" | head -n1 | cut -d= -f2-)
[ -z "$name" ] && name=$(basename "$file" .desktop)
echo "$counter) $name [$(basename "$file")]"
desktop_files+=("$file")
((counter++))
fi
done < <(find /usr/share/applications -type f -name "*.desktop" 2>/dev/null | sort)
if [ ${#desktop_files[@]} -eq 0 ]; then
echo "未发现通过 AppImage 方式安装的应用。"
exit 0
fi
# 选择要卸载的应用
read -p "请输入要卸载的序号 (1-$((counter-1))): " selection
# 校验输入
if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt $((counter-1)) ]; then
echo "无效的选择"
exit 1
fi
# 解析选择项
selected_file="${desktop_files[$((selection-1))]}"
app_id=$(basename "$selected_file" .desktop) # 安装脚本中用的 CLEAN_NAME 即为此 app_id
app_name=$(grep -E '^Name=' "$selected_file" | head -n1 | cut -d= -f2-)
[ -z "$app_name" ] && app_name="$app_id"
echo "正在卸载:$app_name ($app_id)"
# 【关键点-删除程序主体】移除 /opt/<app_id> 目录(AppImage 主体位于此处)
if [ -d "/opt/$app_id" ]; then
echo "移除程序目录:/opt/$app_id"
rm -rf "/opt/$app_id"
fi
# 【关键点-删除桌面启动器】移除 .desktop 文件
echo "移除桌面启动器:$selected_file"
rm "$selected_file"
# Remove icon
if [ -f "/usr/share/icons/hicolor/scalable/apps/$app_name.svg" ]; then
echo "Removing icon..."
rm "/usr/share/icons/hicolor/scalable/apps/$app_name.svg"
fi
# Update system caches
echo "更新系统缓存"
update-desktop-database /usr/share/applications
gtk-update-icon-cache -f /usr/share/icons/hicolor
echo "卸载完成!您可能需要注销并重新登录才能在启动器中看到更改。"
带文件夹应用
sh
#!/bin/bash
# ============================================================
# 通用便携式应用安装脚本 (交互选择版)
# 功能:扫描当前目录子文件夹 → 用户选择 → 安装到 /opt/
# 关键文件(可执行/图标/desktop)均列出供用户确认
# ============================================================
set -e
# 颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# 脚本所在目录
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
# ============================================================
# 通用选择函数
# 参数: $1=提示文字 $2=默认序号(留空表示无默认) $3..=候选项
# 返回: 用户选中的项 (通过 echo)
# ============================================================
select_from_list() {
local prompt="$1"
local default_idx="$2"
shift 2
local items=("$@")
if [ ${#items[@]} -eq 0 ]; then
echo ""
return 1
fi
echo -e "${CYAN}$prompt${NC}" >&2
for i in "${!items[@]}"; do
local marker=" "
if [ -n "$default_idx" ] && [ "$((i+1))" = "$default_idx" ]; then
marker="${GREEN}★${NC} "
fi
printf " ${marker}${YELLOW}%2d)${NC} %s\n" "$((i+1))" "${items[$i]}" >&2
done
local hint="(1-${#items[@]})"
[ -n "$default_idx" ] && hint="$hint,回车使用默认 ★$default_idx"
local choice
while true; do
read -p "请输入序号 $hint: " choice </dev/tty
# 空输入 → 用默认
if [ -z "$choice" ] && [ -n "$default_idx" ]; then
choice="$default_idx"
fi
# 校验
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#items[@]} ]; then
echo "${items[$((choice-1))]}"
return 0
fi
echo -e "${RED}无效输入,请重新输入${NC}" >&2
done
}
# ============================================================
# 主流程
# ============================================================
echo -e "${BLUE}======================================${NC}"
echo -e "${BLUE} 便携式应用安装脚本${NC}"
echo -e "${BLUE}======================================${NC}"
echo -e "扫描目录:${YELLOW}$SCRIPT_DIR${NC}"
echo ""
# ---------- 1. 选择应用文件夹 ----------
mapfile -t DIRS < <(find . -maxdepth 1 -mindepth 1 -type d -printf '%f\n' | sort)
if [ ${#DIRS[@]} -eq 0 ]; then
echo -e "${RED}错误:当前目录下没有任何子文件夹!${NC}"
exit 1
fi
SELECTED_DIR=$(select_from_list "请选择要安装的应用文件夹:" "" "${DIRS[@]}")
SOURCE_PATH="$SCRIPT_DIR/$SELECTED_DIR"
echo -e "${GREEN}✓ 已选择:$SELECTED_DIR${NC}"
echo ""
# ---------- 2. 输入应用名称 ----------
DEFAULT_NAME=$(echo "$SELECTED_DIR" | cut -d'-' -f1 | tr '[:upper:]' '[:lower:]')
read -p "$(echo -e ${CYAN}请输入安装后的应用名称${NC} [默认: ${YELLOW}$DEFAULT_NAME${NC}]: )" APP_NAME </dev/tty
APP_NAME=${APP_NAME:-$DEFAULT_NAME}
TARGET_PATH="/opt/$APP_NAME"
echo -e "${GREEN}✓ 将安装到:$TARGET_PATH${NC}"
echo ""
# ---------- 3. 选择主可执行文件 ----------
# 列出所有可执行的二进制(排除 .sh/.svg/.png/.txt/.so 等)
mapfile -t EXEC_CANDIDATES < <(
find "$SOURCE_PATH" -maxdepth 2 -type f -executable \
! -name "*.svg" ! -name "*.png" ! -name "*.txt" \
! -name "*.so*" ! -name "*.desktop" \
-printf '%P\n' | sort
)
if [ ${#EXEC_CANDIDATES[@]} -eq 0 ]; then
echo -e "${RED}错误:未找到任何可执行文件${NC}"
exit 1
fi
# 找默认序号:精确同名优先
DEFAULT_EXEC_IDX=""
for i in "${!EXEC_CANDIDATES[@]}"; do
if [ "${EXEC_CANDIDATES[$i]}" = "$APP_NAME" ]; then
DEFAULT_EXEC_IDX="$((i+1))"
break
fi
done
# 没精确匹配 → 找包含应用名的
if [ -z "$DEFAULT_EXEC_IDX" ]; then
for i in "${!EXEC_CANDIDATES[@]}"; do
if [[ "${EXEC_CANDIDATES[$i]}" == *"$APP_NAME"* ]] && [[ "${EXEC_CANDIDATES[$i]}" != *.sh ]]; then
DEFAULT_EXEC_IDX="$((i+1))"
break
fi
done
fi
# 都没有 → 第一个
[ -z "$DEFAULT_EXEC_IDX" ] && DEFAULT_EXEC_IDX="1"
EXEC_REL=$(select_from_list "请选择主可执行文件:" "$DEFAULT_EXEC_IDX" "${EXEC_CANDIDATES[@]}")
EXEC_FILE="$TARGET_PATH/$EXEC_REL"
echo -e "${GREEN}✓ 可执行文件:$EXEC_REL${NC}"
echo ""
# ---------- 4. 选择图标文件 ----------
mapfile -t ICON_CANDIDATES < <(
find "$SOURCE_PATH" -maxdepth 3 -type f \
\( -name "*.svg" -o -name "*.png" -o -name "*.xpm" -o -name "*.ico" \) \
-printf '%P\n' | sort
)
ICON_FILE=""
if [ ${#ICON_CANDIDATES[@]} -eq 0 ]; then
echo -e "${YELLOW}⚠ 未找到图标文件,将使用默认图标${NC}"
else
# 默认序号:优先 .svg 且包含应用名(且非 symbolic)
DEFAULT_ICON_IDX=""
for i in "${!ICON_CANDIDATES[@]}"; do
item="${ICON_CANDIDATES[$i]}"
if [[ "$item" == *"$APP_NAME"*.svg ]] && [[ "$item" != *symbolic* ]]; then
DEFAULT_ICON_IDX="$((i+1))"
break
fi
done
# 退而求其次:含应用名的任何图标
if [ -z "$DEFAULT_ICON_IDX" ]; then
for i in "${!ICON_CANDIDATES[@]}"; do
if [[ "${ICON_CANDIDATES[$i]}" == *"$APP_NAME"* ]]; then
DEFAULT_ICON_IDX="$((i+1))"
break
fi
done
fi
[ -z "$DEFAULT_ICON_IDX" ] && DEFAULT_ICON_IDX="1"
# 添加"跳过"选项
ICON_CANDIDATES+=("[跳过 - 不设置图标]")
ICON_REL=$(select_from_list "请选择应用图标:" "$DEFAULT_ICON_IDX" "${ICON_CANDIDATES[@]}")
if [[ "$ICON_REL" != "[跳过"* ]]; then
ICON_FILE="$TARGET_PATH/$ICON_REL"
echo -e "${GREEN}✓ 图标文件:$ICON_REL${NC}"
else
echo -e "${YELLOW}✓ 跳过图标设置${NC}"
fi
fi
echo ""
# ---------- 5. 选择 .desktop 文件 ----------
mapfile -t DESKTOP_CANDIDATES < <(
find "$SOURCE_PATH" -maxdepth 3 -type f -name "*.desktop" -printf '%P\n' | sort
)
DESKTOP_SRC=""
if [ ${#DESKTOP_CANDIDATES[@]} -eq 0 ]; then
echo -e "${YELLOW}⚠ 未找到 .desktop 文件,将自动生成${NC}"
elif [ ${#DESKTOP_CANDIDATES[@]} -eq 1 ]; then
DESKTOP_SRC="$SOURCE_PATH/${DESKTOP_CANDIDATES[0]}"
echo -e "${GREEN}✓ Desktop 文件(自动选中唯一一个):${DESKTOP_CANDIDATES[0]}${NC}"
else
DESKTOP_CANDIDATES+=("[跳过 - 自动生成新的]")
DESKTOP_REL=$(select_from_list "请选择 .desktop 模板文件:" "1" "${DESKTOP_CANDIDATES[@]}")
if [[ "$DESKTOP_REL" != "[跳过"* ]]; then
DESKTOP_SRC="$SOURCE_PATH/$DESKTOP_REL"
echo -e "${GREEN}✓ Desktop 文件:$DESKTOP_REL${NC}"
fi
fi
echo ""
# ---------- 6. 安装确认 ----------
echo -e "${BLUE}======== 安装预览 ========${NC}"
echo -e "源目录: ${YELLOW}$SOURCE_PATH${NC}"
echo -e "目标位置: ${YELLOW}$TARGET_PATH${NC}"
echo -e "可执行文件:${YELLOW}$EXEC_FILE${NC}"
echo -e "图标: ${YELLOW}${ICON_FILE:-(无)}${NC}"
echo -e "Desktop: ${YELLOW}${DESKTOP_SRC:-(自动生成)}${NC}"
echo -e "命令链接: ${YELLOW}/usr/local/bin/$APP_NAME${NC}"
echo -e "${BLUE}=========================${NC}"
read -p "确认安装?(Y/n): " CONFIRM </dev/tty
if [[ "$CONFIRM" =~ ^[Nn]$ ]]; then
echo -e "${RED}已取消${NC}"
exit 0
fi
# ---------- 7. 检查 sudo ----------
if [ "$EUID" -ne 0 ]; then
echo -e "${YELLOW}需要管理员权限:${NC}"
sudo -v || { echo -e "${RED}权限获取失败${NC}"; exit 1; }
fi
# ---------- 8. 处理已存在的目标 ----------
if [ -d "$TARGET_PATH" ]; then
echo -e "${YELLOW}⚠ $TARGET_PATH 已存在${NC}"
read -p "是否覆盖?(y/N): " OVERWRITE </dev/tty
if [[ "$OVERWRITE" =~ ^[Yy]$ ]]; then
sudo rm -rf "$TARGET_PATH"
else
echo -e "${RED}已取消${NC}"
exit 1
fi
fi
# ---------- 9. 移动并设权限 ----------
echo -e "${BLUE}[1/4] 移动到 $TARGET_PATH ...${NC}"
sudo mv "$SOURCE_PATH" "$TARGET_PATH"
sudo chmod -R 755 "$TARGET_PATH"
# 【关键修复】文件已移动到 /opt/,所以 DESKTOP_SRC 路径要更新
# 之前 DESKTOP_SRC 指向 $SOURCE_PATH(下载目录),mv 后那里已经没文件了
if [ -n "$DESKTOP_SRC" ]; then
DESKTOP_REL_PATH="${DESKTOP_SRC#$SOURCE_PATH/}"
DESKTOP_SRC="$TARGET_PATH/$DESKTOP_REL_PATH"
fi
# ---------- 10. 创建命令行软链接 ----------
echo -e "${BLUE}[2/4] 创建命令行链接 /usr/local/bin/$APP_NAME ...${NC}"
sudo ln -sf "$EXEC_FILE" "/usr/local/bin/$APP_NAME"
# ---------- 11. 处理 .desktop 文件 ----------
echo -e "${BLUE}[3/4] 注册到应用菜单 ...${NC}"
DESKTOP_TARGET="/usr/share/applications/$APP_NAME.desktop"
if [ -n "$DESKTOP_SRC" ] && [ -f "$DESKTOP_SRC" ]; then
echo -e " 从模板复制:$DESKTOP_SRC"
sudo cp "$DESKTOP_SRC" "$DESKTOP_TARGET"
# 替换或追加 Exec=
if sudo grep -q "^Exec=" "$DESKTOP_TARGET"; then
sudo sed -i "s|^Exec=.*|Exec=$EXEC_FILE %f|" "$DESKTOP_TARGET"
else
echo "Exec=$EXEC_FILE %f" | sudo tee -a "$DESKTOP_TARGET" > /dev/null
fi
# 替换或追加 Icon=
if [ -n "$ICON_FILE" ]; then
if sudo grep -q "^Icon=" "$DESKTOP_TARGET"; then
sudo sed -i "s|^Icon=.*|Icon=$ICON_FILE|" "$DESKTOP_TARGET"
else
echo "Icon=$ICON_FILE" | sudo tee -a "$DESKTOP_TARGET" > /dev/null
fi
fi
else
# 自动生成
echo -e " ${YELLOW}未指定 desktop 模板,自动生成${NC}"
sudo tee "$DESKTOP_TARGET" > /dev/null <<EOF
[Desktop Entry]
Version=1.0
Type=Application
Name=${APP_NAME^}
GenericName=$APP_NAME
Comment=$APP_NAME application
Exec=$EXEC_FILE %f
Icon=${ICON_FILE:-application-x-executable}
Terminal=false
Categories=Utility;
StartupNotify=true
EOF
fi
sudo chmod 644 "$DESKTOP_TARGET"
# 验证 .desktop 文件
if command -v desktop-file-validate >/dev/null 2>&1; then
if ! desktop-file-validate "$DESKTOP_TARGET" 2>/dev/null; then
echo -e " ${YELLOW}⚠ desktop 文件验证有警告(通常不影响使用)${NC}"
fi
fi
# 确认文件确实存在
if [ -f "$DESKTOP_TARGET" ]; then
echo -e " ${GREEN}✓ 已创建:$DESKTOP_TARGET${NC}"
else
echo -e " ${RED}✗ 失败:$DESKTOP_TARGET 未生成${NC}"
fi
# ---------- 12. 刷新菜单缓存 ----------
echo -e "${BLUE}[4/4] 刷新应用菜单缓存 ...${NC}"
sudo update-desktop-database /usr/share/applications/ 2>/dev/null || true
# ---------- 完成 ----------
echo ""
echo -e "${GREEN}======================================${NC}"
echo -e "${GREEN} ✓ 安装完成!${NC}"
echo -e "${GREEN}======================================${NC}"
echo -e "应用位置:${YELLOW}$TARGET_PATH${NC}"
echo -e "终端命令:${YELLOW}$APP_NAME${NC}"
echo -e "应用菜单:在 Mint 菜单中搜索 ${YELLOW}${APP_NAME^}${NC}"
echo ""
echo -e "${BLUE}卸载方法:${NC}"
echo -e " sudo rm -rf $TARGET_PATH"
echo -e " sudo rm /usr/local/bin/$APP_NAME"
echo -e " sudo rm $DESKTOP_TARGET"