#!/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 "$@"