antiX boot

0
#!/usr/bin/env bash
# ============================================================
#  antix-isoboot-grub.sh
#    — antiX専用 ISOループブート GRUBエントリー管理(統合版)
#
#  isoboot-grub.sh(エントリー追加/削除)と
#  antix-loopboot-patch.sh(configfile方式のFatal Error対策パッチ)
#  を1本化。ISO本来のメニュー(configfile方式)を選んだ場合でも、
#  登録時に自動でiso_path方式 or テキストパッチ方式を判定・適用する
#  ため、事後にパッチスクリプトを別途走らせる必要はない。
#
#  対応対象: antiX Linux(および同系統。runitベースのlive-init)
#            casper(Ubuntu系) / live-boot(汎用Debian live)は非対応。
#
#  ── BIOS(legacy)環境向けの修正点 ──
#
#  [1] insmod biosdisk の追加
#      BIOSモードでは、GRUBのインストール状態によっては biosdisk
#      モジュールが core.img に含まれておらず、search/loopback が
#      対象パーティションを見つけられないことがある。
#      → 生成するエントリーに、BIOS時のみ insmod biosdisk を追加する。
#
#  [2] linux16 / initrd16 の使用(direct kernel boot時、BIOS時のみ)
#      linux/initrd(32bit boot protocol)ではなく、GRUBの実モード
#      ローダーである linux16/initrd16 を使う方が、antiXが対象とする
#      古めのハードウェアでは互換性が高く、ハング/画面停止を回避
#      できるケースがある。
#
#  [3] 「ISO本来のメニュー(configfile方式)」に関する重要な注意
#      実CD/実USBをBIOS機で起動した際に出る F2言語選択メニューは
#      isolinux(BIOSブートローダー)が表示しているものであり、
#      GRUBのgrub.cfg/loopback.cfgとは別物。
#      GRUBのloopback+configfileでは常にISO内のGRUB設定
#      (EFI用として作られたものと同一)を読み込むため、
#      ホストがBIOSであってもUEFI機と同じメニューになる。
#      isolinuxのF2メニューをGRUB経由で再現することは原理的に不可。
#      → 本スクリプトでは「direct kernel boot」方式を既定の推奨とし、
#        F2の代替として起動パラメータに lang=ja_JP を常時
#        付与する(日本語固定。実CD起動時の日本語選択に相当)。
#        「ISO本来のメニュー」を使う場合はこの日本語固定は適用され
#        ない(ISO側メニューにまかせる)ことに注意。
#
#  バックアップは /root/grub-backups/ に保存する。
# ============================================================

set -euo pipefail

CUSTOM_FILE="${CUSTOM_FILE:-/etc/grub.d/40_custom}"
BACKUP_DIR="/root/grub-backups"
PATCHED_CFG_DIR_NAME="isoboot-patched-cfg"
#  antiXの lang= はロケール完全形式(xy_XY.UTF-8)ではなく xy_XY 形式を
#  期待する(公式ドキュメント: mxlinux.org/wiki/system/boot-parameters)。
#  .UTF-8 を付けると値が認識されず英語のまま起動する。
FIXED_LANG="lang=ja_JP"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'

info()    { echo -e "${CYAN}[INFO]${RESET}  $*"; }
success() { echo -e "${GREEN}[OK]${RESET}    $*"; }
warn()    { echo -e "${YELLOW}[WARN]${RESET}  $*"; }
error()   { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
die()     { error "$*"; exit 1; }
hr()      { echo -e "${CYAN}$(printf '=%.0s' {1..64})${RESET}"; }
hr_thin() { echo -e "${CYAN}$(printf -- '-%.0s' {1..64})${RESET}"; }

confirm() {
    local ans
    while true; do
        echo -en "${YELLOW}[確認]${RESET} $1 [y/N]: "
        read -r ans
        case "$ans" in
            [yY]*) return 0 ;;
            [nN]*|"") return 1 ;;
            *) echo "  y または n を入力してください。" ;;
        esac
    done
}

check_root() {
    [[ $EUID -eq 0 ]] || die "sudo で実行してください。\n  例: sudo bash $0"
}

check_deps() {
    local missing=()
    for cmd in lsblk blkid mount umount mountpoint find sed awk update-grub; do
        command -v "$cmd" &>/dev/null || missing+=("$cmd")
    done
    [[ ${#missing[@]} -eq 0 ]] || die "コマンドが見つかりません: ${missing[*]}"
}

backup_file() {
    local src="$1" base
    base=$(basename "$src")
    mkdir -p "$BACKUP_DIR"
    local dst="${BACKUP_DIR}/${base}.bak.$(date +%Y%m%d_%H%M%S)"
    cp "$src" "$dst"
    echo "$dst"
}

# ============================================================
#  ファームウェアモード検出 (uefi / bios)
# ============================================================
detect_firmware() {
    if [[ -d /sys/firmware/efi ]]; then
        echo "uefi"
    else
        echo "bios"
    fi
}

detect_partition_table() {
    local dev="$1"
    local disk
    disk=$(lsblk -no PKNAME "$dev" 2>/dev/null | head -1)
    [[ -z "$disk" ]] && echo "unknown" && return
    local pttype
    pttype=$(blkid -p -o value -s PTTYPE "/dev/${disk}" 2>/dev/null || true)
    case "$pttype" in
        gpt)   echo "gpt" ;;
        dos)   echo "msdos" ;;
        *)     echo "unknown" ;;
    esac
}

fstype_to_grub_insmod() {
    local fstype="$1" part_type="$2" ins_fs ins_part
    case "$fstype" in
        ext2|ext3|ext4)        ins_fs="ext2" ;;
        btrfs)                 ins_fs="btrfs" ;;
        xfs)                   ins_fs="xfs" ;;
        vfat|fat16|fat32|fat)  ins_fs="fat" ;;
        ntfs|ntfs-3g)          ins_fs="ntfs" ;;
        exfat)                 ins_fs="exfat" ;;
        *)                     ins_fs="ext2" ;;
    esac
    case "$part_type" in
        gpt)    ins_part="part_gpt" ;;
        msdos)  ins_part="part_msdos" ;;
        *)      ins_part="part_gpt part_msdos" ;;
    esac
    echo "${ins_fs}:${ins_part}"
}

# insmodブロックを生成。BIOS時は biosdisk を追加挿入する。
build_insmod_block() {
    local ins_part="$1" ins_fs="$2" fw="$3"
    local lines="" mod
    for mod in $ins_part; do lines+="    insmod ${mod}"$'\n'; done
    if [[ "$fw" == "bios" ]]; then
        lines+="    insmod biosdisk"$'\n'
    fi
    lines+="    insmod loopback"$'\n'
    lines+="    insmod iso9660"$'\n'
    lines+="    insmod ${ins_fs}"$'\n'
    lines+="    insmod regexp"
    echo "$lines"
}

find_grub_cfg() {
    local f
    for f in /boot/grub/grub.cfg /boot/grub2/grub.cfg \
             /boot/efi/EFI/antix/grub.cfg /boot/efi/EFI/*/grub.cfg; do
        [[ -f "$f" ]] && echo "$f" && return
    done
    echo ""
}

_SQ="'"
_DQ='"'
extract_entry_name() {
    local line="$1"
    local pat="(menuentry|submenu)[[:space:]]+[${_SQ}${_DQ}]([^${_SQ}${_DQ}]*)[${_SQ}${_DQ}]"
    if [[ "$line" =~ $pat ]]; then
        echo "${BASH_REMATCH[2]}"
    else
        echo ""
    fi
}

ENTRY_NAMES=()
ENTRY_CLASSES=()
ENTRY_GRUB_IDS=()
ENTRY_SOURCES=()

parse_grub_cfg() {
    local cfg_file="$1"

    declare -gA CUSTOM_ENTRY_MAP=()
    local in_cs=0
    while IFS= read -r line; do
        if [[ "$line" =~ ^###\ BEGIN\ (.+)\ ###$ ]]; then
            [[ "${BASH_REMATCH[1]}" == "$CUSTOM_FILE" ]] && in_cs=1 || in_cs=0
            continue
        fi
        [[ "$line" =~ ^###\ END\ (.+)\ ###$ ]] && { in_cs=0; continue; }
        if [[ $in_cs -eq 1 ]]; then
            local cn; cn=$(extract_entry_name "$line")
            [[ -n "$cn" ]] && CUSTOM_ENTRY_MAP["$cn"]=1
        fi
    done < "$cfg_file"

    ENTRY_NAMES=()
    ENTRY_CLASSES=()
    ENTRY_GRUB_IDS=()
    ENTRY_SOURCES=()

    local top_index=-1 in_submenu=0 sub_index=-1 sub_top_index=-1

    while IFS= read -r line; do

        if [[ $in_submenu -eq 0 ]] && [[ "$line" =~ ^submenu[[:space:]] ]]; then
            ((top_index++)) || true
            sub_top_index=$top_index; in_submenu=1; sub_index=-1
            local sname; sname=$(extract_entry_name "$line")
            local src="auto"; [[ -n "${CUSTOM_ENTRY_MAP[$sname]+_}" ]] && src="custom"
            ENTRY_NAMES+=("$sname"); ENTRY_CLASSES+=("submenu_header")
            ENTRY_GRUB_IDS+=("$top_index"); ENTRY_SOURCES+=("$src")
            continue
        fi

        if [[ $in_submenu -eq 1 ]] && [[ "$line" =~ ^'}'[[:space:]]*$ ]]; then
            in_submenu=0; sub_top_index=-1; continue
        fi

        if [[ $in_submenu -eq 1 ]] && [[ "$line" =~ ^[[:space:]]+menuentry[[:space:]] ]]; then
            ((sub_index++)) || true
            local ename; ename=$(extract_entry_name "$line")
            local src="auto"; [[ -n "${CUSTOM_ENTRY_MAP[$ename]+_}" ]] && src="custom"
            ENTRY_NAMES+=("$ename"); ENTRY_CLASSES+=("sub_entry")
            ENTRY_GRUB_IDS+=("${sub_top_index}>${sub_index}"); ENTRY_SOURCES+=("$src")
            continue
        fi

        if [[ $in_submenu -eq 0 ]] && [[ "$line" =~ ^menuentry[[:space:]] ]]; then
            ((top_index++)) || true
            local ename; ename=$(extract_entry_name "$line")
            local src="auto"; [[ -n "${CUSTOM_ENTRY_MAP[$ename]+_}" ]] && src="custom"
            ENTRY_NAMES+=("$ename"); ENTRY_CLASSES+=("toplevel")
            ENTRY_GRUB_IDS+=("$top_index"); ENTRY_SOURCES+=("$src")
            continue
        fi

    done < "$cfg_file"
}

print_entries() {
    if [[ ${#ENTRY_NAMES[@]} -eq 0 ]]; then
        warn "menuentry が見つかりませんでした。"
        return
    fi

    printf "\n  ${BOLD}%-4s  %-14s  %-12s  %-8s  %s${RESET}\n" \
        "No." "GRUB ID" "種別" "出所" "エントリー名"
    hr_thin

    for i in "${!ENTRY_NAMES[@]}"; do
        local class="${ENTRY_CLASSES[$i]}" src="${ENTRY_SOURCES[$i]}"
        local grub_id="${ENTRY_GRUB_IDS[$i]}" name="${ENTRY_NAMES[$i]}"

        local type_label
        case "$class" in
            toplevel)       type_label="${GREEN}エントリー${RESET}" ;;
            submenu_header) type_label="${YELLOW}サブメニュー${RESET}" ;;
            sub_entry)      type_label="${CYAN}  └子エントリ${RESET}" ;;
        esac

        local src_label
        [[ "$src" == "custom" ]] \
            && src_label="${GREEN}custom${RESET}" \
            || src_label="${CYAN}auto${RESET}  "

        local display_name="$name"
        [[ "$class" == "sub_entry" ]] && display_name="    ${name}"

        printf "  ${BOLD}%3d${RESET}   %-14s  %-22b  %-16b  %s\n" \
            "$i" "$grub_id" "$type_label" "$src_label" "$display_name"
    done

    echo ""
    info "出所: ${GREEN}custom${RESET} = 40_custom(削除可能)  ${CYAN}auto${RESET} = 自動生成(削除不可)"
}

print_current_settings() {
    hr
    echo -e "${BOLD}■ 現在の GRUB 設定${RESET}"
    hr_thin

    local fw; fw=$(detect_firmware)
    if [[ "$fw" == "uefi" ]]; then
        echo -e "  ファームウェア: ${GREEN}${BOLD}UEFI${RESET}"
    else
        echo -e "  ファームウェア: ${YELLOW}${BOLD}BIOS (Legacy)${RESET}"
    fi

    local default_grub="/etc/default/grub"
    if [[ -f "$default_grub" ]]; then
        local dv tv
        dv=$(grep '^GRUB_DEFAULT=' "$default_grub" | cut -d= -f2 | tr -d '"' || echo "未設定")
        tv=$(grep '^GRUB_TIMEOUT='  "$default_grub" | cut -d= -f2 | tr -d '"' || echo "未設定")
        echo -e "  GRUB_DEFAULT  : ${BOLD}${dv}${RESET}"
        echo -e "  GRUB_TIMEOUT  : ${BOLD}${tv}${RESET} 秒"
    fi

    local env_file
    for env_file in /boot/grub/grubenv /boot/grub2/grubenv; do
        [[ -f "$env_file" ]] || continue
        local saved next rf
        saved=$(grub-editenv "$env_file" list 2>/dev/null | grep '^saved_entry=' | cut -d= -f2 || true)
        next=$(grub-editenv  "$env_file" list 2>/dev/null | grep '^next_entry='   | cut -d= -f2 || true)
        rf=$(grub-editenv    "$env_file" list 2>/dev/null | grep '^recordfail='   | cut -d= -f2 || true)
        echo -e "  saved_entry   : ${BOLD}${saved:-(なし)}${RESET}"
        echo -e "  next_entry    : ${BOLD}${next:-(なし)}${RESET}"
        if [[ "${rf}" == "1" ]]; then
            echo -e "  recordfail    : ${RED}${BOLD}1(残存)${RESET}"
            warn "recordfail=1 が残っています。次回起動時に影響する場合があります。"
        else
            echo -e "  recordfail    : ${GREEN}なし${RESET}"
        fi
        break
    done
    echo ""
}

# ============================================================
#  ISOをループマウントしてantiXのカーネル/initrdパスと
#  メニュー設定ファイル(loopback.cfg等)の有無・iso_path対応可否を検出
#
#  戻り値: "vmlinuz:initrd:loopback_cfg:has_isopath:saved_cfg_tmp"
#    - loopback_cfg が "NONE" ならISO本来のメニュー設定なし
#    - has_isopath は loopback_cfg が iso_path 変数方式に対応しているか
#    - saved_cfg_tmp はメニュー設定ファイルをmount解除後も使えるように
#      一時保存したファイルのパス(loopback_cfgがNONEなら空)
# ============================================================
_detect_antix_boot_paths() {
    local iso_path="$1"
    local tmp="/mnt/_iso_inspect_$$"
    mkdir -p "$tmp"
    if ! mount -o loop,ro "$iso_path" "$tmp" 2>/dev/null; then
        rmdir "$tmp" 2>/dev/null || true
        echo "UNKNOWN:UNKNOWN:NONE:no:"; return
    fi

    local vmlinuz="" initrd=""
    if   [[ -f "$tmp/antiX/vmlinuz1" ]]; then vmlinuz="/antiX/vmlinuz1"; initrd="/antiX/initrd.gz"
    elif [[ -f "$tmp/antiX/vmlinuz" ]];  then vmlinuz="/antiX/vmlinuz";  initrd="/antiX/initrd.gz"
    else
        local found; found=$(find "$tmp/antiX" -maxdepth 1 -name "vmlinuz*" 2>/dev/null | head -1 || true)
        if [[ -n "$found" ]]; then
            vmlinuz="${found##"$tmp"}"
            local ird; ird=$(find "$tmp/antiX" -maxdepth 1 -name "initrd*" 2>/dev/null | head -1 || true)
            initrd="${ird##"$tmp"}"
        else
            vmlinuz="UNKNOWN"; initrd="UNKNOWN"
        fi
    fi

    local loopback_cfg="NONE"
    local c
    for c in "/boot/grub/loopback.cfg" "/isolinux/loopback.cfg" \
             "/EFI/BOOT/loopback.cfg" "/boot/grub/grub.cfg"; do
        [[ -f "${tmp}${c}" ]] && { loopback_cfg="$c"; break; }
    done

    local has_isopath="no" saved_cfg=""
    if [[ "$loopback_cfg" != "NONE" ]]; then
        saved_cfg=$(mktemp)
        cp "${tmp}${loopback_cfg}" "$saved_cfg"
        grep -qw 'iso_path' "$saved_cfg" 2>/dev/null && has_isopath="yes"
    fi

    umount "$tmp"; rmdir "$tmp"
    echo "${vmlinuz}:${initrd}:${loopback_cfg}:${has_isopath}:${saved_cfg}"
}

# ============================================================
#  1: ISOループブートエントリーの追加
# ============================================================
action_add_iso() {
    hr
    echo -e "${BOLD}■ antiX ISOループブートエントリーの追加${RESET}"
    hr_thin
    echo ""

    local fw; fw=$(detect_firmware)
    if [[ "$fw" == "uefi" ]]; then
        echo -e "  ファームウェア: ${GREEN}${BOLD}UEFI${RESET}"
    else
        echo -e "  ファームウェア: ${YELLOW}${BOLD}BIOS (Legacy)${RESET}  ${CYAN}(biosdisk / linux16 を自動付与)${RESET}"
    fi
    echo ""

    info "パーティション一覧:"
    echo ""

    mapfile -t part_list < <(
        lsblk -lno NAME,SIZE,FSTYPE,LABEL,MOUNTPOINTS \
        | grep -v '^loop' \
        | awk 'NF && $1 ~ /[0-9]$/ { print $0 }'
    )
    [[ ${#part_list[@]} -eq 0 ]] && die "パーティションが見つかりませんでした。"

    printf "  ${BOLD}%-4s  %s${RESET}\n" "No." "NAME  SIZE  FSTYPE  LABEL  MOUNTPOINTS"
    hr_thin
    for i in "${!part_list[@]}"; do
        printf "  ${CYAN}[%2d]${RESET}  %s\n" "$((i+1))" "${part_list[$i]}"
    done
    echo ""

    local part_dev part_uuid part_fstype part_pttype
    while true; do
        echo -en "${BOLD}番号を入力してください${RESET} [1-${#part_list[@]}]: "
        read -r sel
        if ! [[ "$sel" =~ ^[0-9]+$ ]] || (( sel < 1 || sel > ${#part_list[@]} )); then
            error "1〜${#part_list[@]} の番号を入力してください。"; continue
        fi
        local pname; pname=$(echo "${part_list[$((sel-1))]}" | awk '{print $1}')
        part_dev="/dev/${pname}"
        [[ -b "$part_dev" ]] || { error "${part_dev} はブロックデバイスではありません。"; continue; }

        part_fstype=$(lsblk -no FSTYPE "$part_dev" 2>/dev/null | head -1 || true)
        part_pttype=$(detect_partition_table "$part_dev")

        case "$part_fstype" in
            ext2|ext3|ext4|btrfs|xfs|vfat|fat16|fat32|fat|ntfs|ntfs-3g|exfat) ;;
            *)
                warn "${part_dev} のファイルシステム(${part_fstype:-不明})は動作確認済みではありません。"
                confirm "このまま続けますか?" || continue
                ;;
        esac

        echo ""
        lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTS "$part_dev" 2>/dev/null || true
        echo -e "  パーティションテーブル: ${BOLD}${part_pttype}${RESET}"
        echo ""
        confirm "${part_dev} を使用しますか?" && break
    done

    part_uuid=$(blkid -s UUID -o value "$part_dev")
    [[ -n "$part_uuid" ]] || die "UUID を取得できませんでした: ${part_dev}"
    success "パーティション  : ${part_dev}"
    success "UUID           : ${part_uuid}"
    success "FS 種別        : ${part_fstype:-不明}"
    success "パーティション表: ${part_pttype}"
    echo ""

    local insmod_str; insmod_str=$(fstype_to_grub_insmod "$part_fstype" "$part_pttype")
    local ins_fs="${insmod_str%%:*}"
    local ins_part="${insmod_str#*:}"
    local insmod_lines; insmod_lines=$(build_insmod_block "$ins_part" "$ins_fs" "$fw")

    local mount_point="/mnt/_isoboot_tmp_$$"
    mkdir -p "$mount_point"
    mount "$part_dev" "$mount_point"
    info "${part_dev} を ${mount_point} にマウントしました"
    echo ""

    mapfile -t iso_candidates < <(find "$mount_point" -maxdepth 2 -name "*.iso" 2>/dev/null)
    if [[ ${#iso_candidates[@]} -eq 0 ]]; then
        umount "$mount_point"; rmdir "$mount_point"
        die "ISO ファイルが見つかりませんでした。"
    fi

    info "見つかった ISO ファイル:"
    for i in "${!iso_candidates[@]}"; do
        local sz; sz=$(du -sh "${iso_candidates[$i]}" 2>/dev/null | cut -f1)
        printf "  ${CYAN}[%d]${RESET}  %s  (%s)\n" "$((i+1))" "${iso_candidates[$i]##"$mount_point"}" "$sz"
    done
    printf "  ${CYAN}[%d]${RESET}  何もせず終了\n" "$((${#iso_candidates[@]}+1))"
    echo ""

    local sel_input sel_nums=()
    while true; do
        echo -en "${BOLD}追加する番号を入力${RESET}(スペース区切り複数可): "
        read -r sel_input
        local cancel_num=$(( ${#iso_candidates[@]} + 1 ))
        local do_cancel=0
        for n in $sel_input; do
            [[ "$n" == "$cancel_num" ]] && do_cancel=1 && break
        done
        if [[ $do_cancel -eq 1 ]] || [[ -z "$sel_input" ]]; then
            umount "$mount_point"; rmdir "$mount_point"
            info "キャンセルしました。"; return
        fi
        local valid=1
        sel_nums=()
        for n in $sel_input; do
            if ! [[ "$n" =~ ^[0-9]+$ ]] || (( n < 1 || n > ${#iso_candidates[@]} )); then
                error "1〜${#iso_candidates[@]} の番号を入力してください。"
                valid=0; break
            fi
            sel_nums+=("$n")
        done
        [[ $valid -eq 1 ]] && break
    done

    local bak; bak=$(backup_file "$CUSTOM_FILE")
    success "バックアップ: ${bak}"
    echo ""

    for n in "${sel_nums[@]}"; do
        local iso_full="${iso_candidates[$((n-1))]}"
        local iso_rel="${iso_full##"$mount_point"}"
        local iso_name; iso_name=$(basename "$iso_full")
        local menu_label; menu_label=$(basename "$iso_rel" .iso)

        echo ""
        info "--- ${iso_name} を検査中 ---"
        local paths; paths=$(_detect_antix_boot_paths "$iso_full")
        local vmlinuz initrd loopback_cfg has_isopath saved_cfg
        IFS=':' read -r vmlinuz initrd loopback_cfg has_isopath saved_cfg <<< "$paths"

        if [[ "$vmlinuz" == "UNKNOWN" ]]; then
            warn "antiXのカーネルパス(/antiX/vmlinuz[1])を検出できませんでした。"
            warn "このISOはantiX系ではない可能性があります。手動入力してください。"
            echo -en "  vmlinuz のパス (例: /antiX/vmlinuz1): "; read -r vmlinuz
            echo -en "  initrd  のパス (例: /antiX/initrd.gz): "; read -r initrd
        else
            success "カーネル : ${vmlinuz}"
            success "initrd   : ${initrd}"
        fi

        local method="direct"
        if [[ "$loopback_cfg" != "NONE" ]]; then
            success "ISO本来のメニュー設定を検出: ${loopback_cfg}"
            echo ""
            echo -e "  ${BOLD}[1]${RESET} direct kernel boot ${GREEN}(推奨。日本語固定 lang=ja_JP を付与)${RESET}"
            echo -e "  ${BOLD}[2]${RESET} ISO本来のメニュー(configfile方式)"
            warn "  ※ configfile方式はGRUB側の設定を読むため、実CDのBIOS起動時に出る"
            warn "     F2言語選択メニュー(isolinux)とは異なる、UEFI機と同じメニューになります。"
            echo ""
            local mchoice
            while true; do
                echo -en "${BOLD}方式を選択${RESET} [1/2] (Enterで1): "
                read -r mchoice
                case "${mchoice:-1}" in
                    1) method="direct"; break ;;
                    2) method="configfile"; break ;;
                    *) error "1 または 2 を入力してください。" ;;
                esac
            done
        else
            info "ISO本来のメニュー設定は見つかりませんでした。direct kernel boot方式のみ利用可能です。"
        fi

        local grub_entry=""

        if [[ "$method" == "direct" ]]; then
            local linux_cmd="linux" initrd_cmd="initrd"
            if [[ "$fw" == "bios" ]]; then
                linux_cmd="linux16"; initrd_cmd="initrd16"
            fi
            local params="buuid=${part_uuid} fromiso=\$isofile quiet nocheckfs persist ${FIXED_LANG}"

            grub_entry=$(cat <<EOF

menuentry "${menu_label} (ISO Loop Boot)" {
${insmod_lines}
    search --no-floppy --fs-uuid --set=isodev ${part_uuid}
    set isofile="${iso_rel}"
    loopback loop (\$isodev)\$isofile
    ${linux_cmd}  (loop)${vmlinuz} ${params}
    ${initrd_cmd} (loop)${initrd}
}
EOF
)
        else
            if [[ "$has_isopath" == "yes" ]]; then
                success "iso_path変数方式のループブート対応cfgです。configfile呼び出し前にiso_pathを設定します。"
                grub_entry=$(cat <<EOF

menuentry "${menu_label} (ISO Loop Boot - 起動メニュー表示)" {
${insmod_lines}
    search --no-floppy --fs-uuid --set=isodev ${part_uuid}
    set isofile="${iso_rel}"
    loopback loop (\$isodev)\$isofile
    set root=(loop)
    set iso_path="${iso_rel}"
    export iso_path
    configfile (loop)${loopback_cfg}
}
EOF
)
                rm -f "$saved_cfg"
            else
                warn "iso_path方式に対応していないcfgです。テキストパッチ方式を適用します。"
                local dest_dir="${mount_point}/${PATCHED_CFG_DIR_NAME}"
                mkdir -p "$dest_dir"
                local dest_cfg="${dest_dir}/${menu_label}.cfg"
                local extra_codes="fromiso=${iso_rel} buuid=${part_uuid} nocheckfs persist ${FIXED_LANG}"

                sed -E \
                    "/^[[:space:]]*linux(16|efi)?[[:space:]]+.*vmlinuz/I { /fromiso=|findiso=/! s#\$# ${extra_codes}# }" \
                    "$saved_cfg" > "$dest_cfg"
                rm -f "$saved_cfg"

                local patched_count
                patched_count=$(grep -Fc "fromiso=${iso_rel}" "$dest_cfg" || true)

                if [[ "$patched_count" -eq 0 ]]; then
                    warn "起動行(linux/linux16/linuxefi ... vmlinuz)の自動検出に失敗しました。"
                    warn "direct kernel boot方式にフォールバックします。"
                    rm -f "$dest_cfg"
                    local linux_cmd="linux" initrd_cmd="initrd"
                    [[ "$fw" == "bios" ]] && linux_cmd="linux16" && initrd_cmd="initrd16"
                    local params="buuid=${part_uuid} fromiso=\$isofile quiet nocheckfs persist ${FIXED_LANG}"
                    grub_entry=$(cat <<EOF

menuentry "${menu_label} (ISO Loop Boot)" {
${insmod_lines}
    search --no-floppy --fs-uuid --set=isodev ${part_uuid}
    set isofile="${iso_rel}"
    loopback loop (\$isodev)\$isofile
    ${linux_cmd}  (loop)${vmlinuz} ${params}
    ${initrd_cmd} (loop)${initrd}
}
EOF
)
                else
                    success "起動行 ${patched_count} 件に起動コードを追記しました: ${extra_codes}"
                    local dest_rel="/${PATCHED_CFG_DIR_NAME}/${menu_label}.cfg"
                    grub_entry=$(cat <<EOF

menuentry "${menu_label} (ISO Loop Boot - 起動メニュー表示 / パッチ済み)" {
${insmod_lines}
    search --no-floppy --fs-uuid --set=isodev ${part_uuid}
    set isofile="${iso_rel}"
    loopback loop (\$isodev)\$isofile
    set root=(loop)
    configfile (\$isodev)${dest_rel}
}
EOF
)
                fi
            fi
        fi

        echo -e "${CYAN}追加予定エントリ(方式: ${method}, ファームウェア: ${fw}):${RESET}"
        echo "$grub_entry"
        echo ""
        if confirm "このエントリを追加しますか?"; then
            echo "$grub_entry" >> "$CUSTOM_FILE"
            success "追加しました: ${menu_label}"
        else
            warn "スキップ: ${menu_label}"
        fi
    done

    umount "$mount_point"; rmdir "$mount_point"
    success "アンマウント完了"

    _run_update_grub
}

# ============================================================
#  2: 不要エントリーの削除
# ============================================================
action_delete() {
    hr
    echo -e "${BOLD}■ 不要エントリーの削除${RESET}"
    hr_thin
    echo ""

    local deletable_indices=()
    for i in "${!ENTRY_SOURCES[@]}"; do
        [[ "${ENTRY_SOURCES[$i]}" == "custom" ]] \
        && [[ "${ENTRY_CLASSES[$i]}" != "submenu_header" ]] \
        && deletable_indices+=("$i")
    done

    if [[ ${#deletable_indices[@]} -eq 0 ]]; then
        warn "削除可能なエントリー(40_custom 由来)が見つかりませんでした。"
        return
    fi

    echo -e "  ${BOLD}削除可能なエントリー一覧:${RESET}"
    echo ""
    printf "  ${BOLD}%-4s  %-14s  %s${RESET}\n" "No." "GRUB ID" "エントリー名"
    hr_thin
    for i in "${deletable_indices[@]}"; do
        printf "  ${BOLD}%3d${RESET}   %-14s  %s\n" \
            "$i" "${ENTRY_GRUB_IDS[$i]}" "${ENTRY_NAMES[$i]}"
    done
    echo ""

    local sel_input to_delete=() valid
    while true; do
        echo -en "${BOLD}削除する番号を入力${RESET}(スペース区切り複数可、Enter でキャンセル): "
        read -r sel_input
        [[ -z "$sel_input" ]] && { info "キャンセルしました。"; return; }

        valid=1; to_delete=()
        for num in $sel_input; do
            if ! [[ "$num" =~ ^[0-9]+$ ]]; then
                error "無効な入力: ${num}"; valid=0; break
            fi
            local found=0
            for di in "${deletable_indices[@]}"; do
                [[ "$di" -eq "$num" ]] && found=1 && break
            done
            if [[ $found -eq 0 ]]; then
                error "番号 ${num} は削除可能なエントリーではありません。"; valid=0; break
            fi
            to_delete+=("$num")
        done
        [[ $valid -eq 1 ]] && break
    done

    echo ""
    echo -e "  ${BOLD}${RED}以下のエントリーを削除します:${RESET}"
    for num in "${to_delete[@]}"; do
        echo -e "    ${RED}・${ENTRY_NAMES[$num]}${RESET}"
    done
    echo ""
    confirm "本当に削除しますか?" || { info "キャンセルしました。"; return; }

    local bak; bak=$(backup_file "$CUSTOM_FILE")
    success "バックアップ: ${bak}"

    local stale_baks=()
    while IFS= read -r -d '' f; do
        stale_baks+=("$f")
    done < <(find /etc/grub.d/ -maxdepth 1 -name '40_custom.bak.*' -print0 2>/dev/null)
    if [[ ${#stale_baks[@]} -gt 0 ]]; then
        warn "/etc/grub.d/ に古いバックアップが残っています(エントリー復活の原因):"
        for f in "${stale_baks[@]}"; do warn "  ${f}"; done
        echo ""
        if confirm "古いバックアップを /etc/grub.d/ から削除しますか?"; then
            for f in "${stale_baks[@]}"; do rm -f "$f" && success "削除: $f"; done
        fi
        echo ""
    fi

    declare -A name_delete_occurrences=()
    for num in "${to_delete[@]}"; do
        local tname="${ENTRY_NAMES[$num]}"
        local occ=0
        for i in "${!ENTRY_NAMES[@]}"; do
            [[ "${ENTRY_SOURCES[$i]}" != "custom" ]] && continue
            [[ "${ENTRY_CLASSES[$i]}" == "submenu_header" ]] && continue
            [[ "${ENTRY_NAMES[$i]}" == "$tname" ]] && ((occ++)) || true
            [[ "$i" -eq "$num" ]] && break
        done
        name_delete_occurrences["$tname"]+="${occ} "
    done

    local tmp; tmp=$(mktemp)
    cp "$CUSTOM_FILE" "$tmp"

    for tname in "${!name_delete_occurrences[@]}"; do
        local sorted_occs
        sorted_occs=$(echo "${name_delete_occurrences[$tname]}" | tr ' ' '\n' | grep -v '^$' | sort -rn)
        for occ in $sorted_occs; do
            awk -v name="$tname" -v target_occ="$occ" '
            BEGIN { skip=0; depth=0; match_count=0 }
            {
                if (!skip && /menuentry/ && index($0, name) > 0) {
                    match_count++
                    if (match_count == target_occ) { skip=1; depth=0 }
                }
                if (skip) {
                    n = split($0, chars, "")
                    for (i=1; i<=n; i++) {
                        if (chars[i] == "{") depth++
                        else if (chars[i] == "}") {
                            depth--
                            if (depth <= 0) { skip=0; break }
                        }
                    }
                    next
                }
                print
            }' "$tmp" > "${tmp}.new"
            mv "${tmp}.new" "$tmp"
            success "削除: ${tname}(${occ}番目)"
        done
    done

    cat "$tmp" > "$CUSTOM_FILE"
    rm -f "$tmp"

    _run_update_grub
}

_run_update_grub() {
    if [[ -f "$CUSTOM_FILE" && ! -x "$CUSTOM_FILE" ]]; then
        warn "${CUSTOM_FILE} に実行権限が無かったため付与します(無いと update-grub に無視されます)。"
        chmod 755 "$CUSTOM_FILE"
    fi
    echo ""
    info "GRUB を更新中..."
    if update-grub 2>&1; then
        success "update-grub 完了"
        local grub_cfg; grub_cfg=$(find_grub_cfg)
        if [[ -n "$grub_cfg" ]]; then
            echo ""
            info "エントリー一覧を更新しました:"
            parse_grub_cfg "$grub_cfg"
            print_entries
        fi
    else
        warn "update-grub でエラーが発生しました。手動で確認してください: sudo update-grub"
    fi
}

show_action_menu() {
    hr
    echo -e "${BOLD}■ 操作を選択してください${RESET}"
    hr_thin
    echo ""
    echo -e "  ${BOLD}[1]${RESET}  ISO ループブートエントリーを追加"
    echo -e "  ${BOLD}[2]${RESET}  不要エントリーを削除  ${CYAN}(40_custom 由来のみ)${RESET}"
    echo -e "  ${BOLD}[3]${RESET}  何もせず終了"
    echo ""

    local choice
    while true; do
        echo -en "${BOLD}選択${RESET} [1/2/3]: "
        read -r choice
        case "$choice" in
            1) action_add_iso; break ;;
            2) action_delete; break ;;
            3) echo ""; info "変更なしで終了します。"; exit 0 ;;
            *) error "1、2、または 3 を入力してください。" ;;
        esac
    done
}

main() {
    clear
    hr
    echo -e "${BOLD}  antix-isoboot-grub.sh — antiX ISOループブート GRUB管理ツール(統合版)${RESET}"
    hr
    echo -e "  実行日時: $(date '+%Y-%m-%d %H:%M:%S')"
    echo ""

    check_root
    check_deps

    local grub_cfg; grub_cfg=$(find_grub_cfg)
    [[ -n "$grub_cfg" ]] || die "grub.cfg が見つかりませんでした。"
    info "設定ファイル: ${BOLD}${grub_cfg}${RESET}"

    parse_grub_cfg "$grub_cfg"
    print_current_settings

    hr
    echo -e "${BOLD}■ GRUB エントリー一覧${RESET}"
    hr_thin
    print_entries

    show_action_menu

    echo ""
    hr
    success "完了しました。"
    hr
    echo ""
}

main "$@"
タイトルとURLをコピーしました