#! /usr/bin/env bash # # Written by: Antynea # BTC donation address: 1Lbvz244WA8xbpHek9W2Y12cakM6rDe5Rt # Github: https://github.com/Antynea/grub-btrfs # # Purpose: # Improves Grub by adding "btrfs snapshots" to the Grub menu. # You can boot your system on a "snapshot" from the Grub menu. # Supports manual snapshots, snapper, timeshift ... # Warning : booting on read-only snapshots can be tricky. # (Read about it, https://github.com/Antynea/grub-btrfs#warning-booting-on-read-only-snapshots-can-be-tricky) # # What this script does: # - Automatically List snapshots existing on root partition (btrfs). # - Automatically Detect if "/boot" is in separate partition. # - Automatically Detect kernel, initramfs and intel/amd microcode in "/boot" directory on snapshots. # - Automatically Create corresponding "menuentry" in grub.cfg. # - Automatically detect snapper and use snapper's snapshot description if available. # - Automatically generate grub.cfg if you use the provided systemd service. # # Installation: # - Refer to https://github.com/Antynea/grub-btrfs#installation- # # Customization: # You have the possibility to modify many parameters in /etc/default/grub-btrfs/config. # # Automatically update Grub # If you would like grub-btrfs menu to automatically update when a snapshot is created or deleted: # - Refer to https://github.com/Antynea/grub-btrfs#automatically-update-grub. # # Special thanks for assistance and contributions: # - My friends # - All contributors on Github # set -e datarootdir="/usr/share" sysconfdir="/etc" grub_btrfs_config="${sysconfdir}/default/grub-btrfs/config" [[ -f "$grub_btrfs_config" ]] && . "$grub_btrfs_config" [[ -f "${sysconfdir}/default/grub" ]] && . "${sysconfdir}/default/grub" ## Exit the script, if: disable_script() { # Disable Grub-btrfs is set to true (default=false) [[ "${GRUB_BTRFS_DISABLE:-"false"}" == "true" ]] && return 1 # btrfs-progs isn't installed if ! type btrfs >/dev/null 2>&1; then return 1; fi # grub-mkconfig_lib couldn't be found [[ -f "$datarootdir/grub/grub-mkconfig_lib" ]] && . "$datarootdir/grub/grub-mkconfig_lib" || return 1 # Root filesystem isn't btrfs root_fs=$(${grub_probe} --target="fs" / 2>/dev/null) [[ "$root_fs" != "btrfs" ]] && return 1 return 0 } disable_script ## Submenu name distro=$(awk -F "=" '/^NAME=/ {gsub(/"/, "", $2); print $2}' /etc/os-release) submenuname=${GRUB_BTRFS_SUBMENUNAME:-"${distro:-Linux} snapshots"} ## Prefix entry prefixentry=${GRUB_BTRFS_PREFIXENTRY:-"Snapshot:"} ## Limit snapshots to show in the Grub menu (default=50) limit_snap_show="${GRUB_BTRFS_LIMIT:-50}" ## How to sort snapshots list btrfssubvolsort=(--sort="${GRUB_BTRFS_SUBVOLUME_SORT:-"-rootid"}") ## Snapper's config name snapper_config=${GRUB_BTRFS_SNAPPER_CONFIG:-"root"} ## Customize GRUB directory, where "grub.cfg" file is saved grub_directory=${GRUB_BTRFS_GRUB_DIRNAME:-"/boot/grub"} ## Customize BOOT directory, where kernels/initrams/microcode is saved. boot_directory=${GRUB_BTRFS_BOOT_DIRNAME:-"/boot"} ## Password protection management for submenu # Protection support for submenu (--unrestricted) case "${GRUB_BTRFS_DISABLE_PROTECTION_SUBMENU:-"false"}" in true) unrestricted_access_submenu="--unrestricted ";; *) unrestricted_access_submenu="" esac # Authorized users (--users foo,bar) if [ -n "${GRUB_BTRFS_PROTECTION_AUTHORIZED_USERS}" ] ; then protection_authorized_users="--users ${GRUB_BTRFS_PROTECTION_AUTHORIZED_USERS} " fi ## Probe informations of Root and Boot devices # Probe info "Root partition" root_device=$(${grub_probe} --target=device /) # Root device root_uuid=$(${grub_probe} --device ${root_device} --target="fs_uuid" 2>/dev/null) # UUID of the root device # Probe info "Boot partition" boot_device=$(${grub_probe} --target=device ${boot_directory}) # Boot device boot_uuid=$(${grub_probe} --device ${boot_device} --target="fs_uuid" 2>/dev/null) # UUID of the boot device boot_hs=$(${grub_probe} --device ${boot_device} --target="hints_string" 2>/dev/null) # hints string boot_fs=$(${grub_probe} --device ${boot_device} --target="fs" 2>/dev/null) # Type filesystem of boot device ## Parameters passed to the kernel kernel_parameters="$GRUB_CMDLINE_LINUX $GRUB_CMDLINE_LINUX_DEFAULT" ## Mount point location gbgmp=$(mktemp -dt grub-btrfs.XXXXXXXXXX) ## Class for theme CLASS="--class snapshots --class gnu-linux --class gnu --class os" ## save IFS oldIFS=$IFS ## Detect uuid requirement (lvm,btrfs...) check_uuid_required() { if [ "x${root_uuid}" = "x" ] || [ "x${GRUB_DISABLE_LINUX_UUID}" = "xtrue" ] \ || ! test -e "/dev/disk/by-uuid/${root_uuid}" \ || ( test -e "${root_device}" && uses_abstraction "${root_device}" lvm ); then LINUX_ROOT_DEVICE=${root_device} else LINUX_ROOT_DEVICE=UUID=${root_uuid} fi } ## Detect rootflags detect_rootflags() { local fstabflags=$(grep -oE '^\s*[^#][[:graph:]]+\s+/\s+btrfs\s+[[:graph:]]+' "${gbgmp}/${snap_dir_name}/etc/fstab" \ | sed -E 's/^.*[[:space:]]([[:graph:]]+)$/\1/;s/,?subvol(id)?=[^,$]+//g;s/^,//') rootflags="rootflags=${fstabflags:+$fstabflags,}${GRUB_BTRFS_ROOTFLAGS:+$GRUB_BTRFS_ROOTFLAGS,}" } ## Path to grub-script-check grub_script_check="${bindir}/grub-script-check" ## Error Handling print_error() { local err_msg="$*" local bug_report="If you think an error has occurred , please file a bug report at \" https://github.com/Antynea/grub-btrfs \"" printf "%s\n" "${err_msg}" "${bug_report}" >&2 ; exit 0 } unmount_gbgmp() { if [[ -d "$gbgmp" ]]; then local wait=true local wait_max=0 printf "Unmount %s .." "$gbgmp" >&2; while $wait; do if grep -qs "$gbgmp" /proc/mounts; then wait_max=$((1+$wait_max)) if umount "$gbgmp" >/dev/null 2>&1; then wait=false # umount successful printf " Success\n" >&2; elif [[ $wait_max = 10 ]]; then printf "\nWarning: Unable to unmount %s in %s\n" "$root_device" "$gbgmp" >&2; break; else printf "." >&2 ; # output to show that the script is alive sleep 2 # wait 2 seconds before retry fi else wait=false # not mounted printf " Success\n" >&2; fi done if [[ "$wait" != true ]]; then if ! rm -d "$gbgmp" >/dev/null 2>&1; then printf "Unable to delete %s: Device or ressource is busy\n" "$gbgmp" >&2; fi fi fi } ## Create entry entry() { echo "$@" >> "$grub_directory/grub-btrfs.new" } ## menu entries make_menu_entries() { ## \" required for snap,kernels,init,microcode with space in their name entry "submenu '$title_menu' { submenu '---> $title_menu <---' { echo }" for k in "${name_kernel[@]}"; do [[ ! -f "${boot_dir}"/"${k}" ]] && continue; kversion=${k#*"-"} for i in "${name_initramfs[@]}"; do if [[ "${name_initramfs}" != "x" ]] ; then prefix_i=${i%%"-"*} suffix_i=${i#*"-"} alt_suffix_i=${i##*"-"} if [ "${kversion}" = "${suffix_i}" ]; then i="${i}"; elif [ "${kversion}.img" = "${suffix_i}" ]; then i="${i}"; elif [ "${kversion}-fallback.img" = "${suffix_i}" ]; then i="${i}"; elif [ "${kversion}.gz" = "${suffix_i}" ]; then i="${i}"; fi for u in "${name_microcode[@]}"; do if [[ "${name_microcode}" != "x" ]] ; then entry " menuentry '"${k}" & "${i}" & "${u}"' ${CLASS} "\$menuentry_id_option" 'gnulinux-snapshots-$boot_uuid' {" else entry " menuentry '"${k}" & "${i}"' ${CLASS} "\$menuentry_id_option" 'gnulinux-snapshots-$boot_uuid' {" fi entry "\ if [ x\$feature_all_video_module = xy ]; then insmod all_video fi set gfxpayload=keep insmod ${boot_fs} if [ x\$feature_platform_search_hint = xy ]; then search --no-floppy --fs-uuid --set=root ${boot_hs} ${boot_uuid} else search --no-floppy --fs-uuid --set=root ${boot_uuid} fi echo 'Loading Snapshot: "${snap_date_time}" "${snap_dir_name}"' echo 'Loading Kernel: "${k}" ...' linux \"${boot_dir_root_grub}/"${k}"\" root="${LINUX_ROOT_DEVICE}" ${kernel_parameters} ${rootflags}subvol=\""${snap_dir_name}"\"" if [[ "${name_microcode}" != "x" ]] ; then entry "\ echo 'Loading Microcode & Initramfs: "${u}" "${i}" ...' initrd \"${boot_dir_root_grub}/"${u}"\" \"${boot_dir_root_grub}/"${i}"\"" else entry "\ echo 'Loading Initramfs: "${i}" ...' initrd \"${boot_dir_root_grub}/"${i}"\"" fi entry " }" count_warning_menuentries=$((1+$count_warning_menuentries)) done else for u in "${name_microcode[@]}"; do if [[ "${name_microcode}" != "x" ]] ; then entry " menuentry '"${k}" & "${u}"' ${CLASS} "\$menuentry_id_option" 'gnulinux-snapshots-$boot_uuid' {" else entry " menuentry '"${k}"' ${CLASS} "\$menuentry_id_option" 'gnulinux-snapshots-$boot_uuid' {" fi entry "\ if [ x\$feature_all_video_module = xy ]; then insmod all_video fi set gfxpayload=keep insmod ${boot_fs} if [ x\$feature_platform_search_hint = xy ]; then search --no-floppy --fs-uuid --set=root ${boot_hs} ${boot_uuid} else search --no-floppy --fs-uuid --set=root ${boot_uuid} fi echo 'Loading Snapshot: "${snap_date_time}" "${snap_dir_name}"' echo 'Loading Kernel: "${k}" ...' linux \"${boot_dir_root_grub}/"${k}"\" root="${LINUX_ROOT_DEVICE}" ${kernel_parameters} ${rootflags}subvol=\""${snap_dir_name}"\"" if [[ "${name_microcode}" != "x" ]] ; then entry "\ echo 'Loading Microcode: "${u}" ...' initrd \"${boot_dir_root_grub}/"${u}"\"" fi entry " }" count_warning_menuentries=$((1+$count_warning_menuentries)) done fi done done entry "}" } ## Trim a string from leading and trailing whitespaces trim() { local var="$*" var="${var#"${var%%[![:space:]]*}"}" var="${var%"${var##*[![:space:]]}"}" echo -n "$var" } ## List of snapshots on filesystem snapshot_list() { # Query info from snapper if it is installed if type snapper >/dev/null 2>&1; then if [ -s "/etc/snapper/configs/$snapper_config" ]; then printf "Info: snapper detected, using config: %s\n" "$snapper_config" >&2 local snapper_ids=($(snapper --no-dbus -t 0 -c "$snapper_config" list --disable-used-space | tail -n +3 | cut -d'|' -f 1)) local snapper_types=($(snapper --no-dbus -t 0 -c "$snapper_config" list --disable-used-space | tail -n +3 | cut -d'|' -f 2)) IFS=$'\n' local snapper_descriptions=($(snapper --no-dbus -t 0 -c "$snapper_config" list --disable-used-space | tail -n +3 | rev | cut -d'|' -f 2 | rev)) else printf "Warning: snapper detected but config: %s does not exist\n" "$snapper_config" >&2 fi fi IFS=$'\n' # Parse btrfs snapshots local entries=() local ids=() local max_entry_length=0 for snap in $(btrfs subvolume list -sa "${btrfssubvolsort}" /); do IFS=$oldIFS snap=($snap) local snap_path_name=${snap[@]:13:${#snap[@]}} # Discard deleted snapshots if [ "$snap_path_name" = "DELETED" ]; then continue; fi [[ ${snap_path_name%%"/"*} == "" ]] && snap_path_name=${snap_path_name#*"/"} # ignore specific path during run "grub-mkconfig" if [ -n "${GRUB_BTRFS_IGNORE_SPECIFIC_PATH}" ] ; then for isp in ${GRUB_BTRFS_IGNORE_SPECIFIC_PATH[@]} ; do [[ "${snap_path_name}" == "${isp}" ]] && continue 2; done fi if [ -n "${GRUB_BTRFS_IGNORE_PREFIX_PATH}" ] ; then for isp in ${GRUB_BTRFS_IGNORE_PREFIX_PATH[@]} ; do [[ "${snap_path_name}" == "${isp}"/* ]] && continue 2; done fi # detect if /boot directory exists [[ ! -d "$gbgmp/$snap_path_name/boot" ]] && continue; local id="${snap_path_name//[!0-9]}" # brutal way to get id: remove everything non-numeric ids+=("$id") local entry="${snap[@]:10:2} | ${snap_path_name}" entries+=("$entry") # Find max length of a snapshot entry, needed for pretty formatting local length="${#entry}" [[ "$length" -gt "$max_entry_length" ]] && max_entry_length=$length done # Find max length of a snapshot type, needed for pretty formatting local max_type_length=0 for id in "${ids[@]}"; do for j in "${!snapper_ids[@]}"; do local snapper_id="${snapper_ids[$j]//[[:space:]]/}" if [[ "$snapper_id" == "$id" ]]; then local snapper_type=$(trim "${snapper_types[$j]}") local length="${#snapper_type}" [[ "$length" -gt "$max_type_length" ]] && max_type_length=$length fi done done for i in "${!entries[@]}"; do local id="${ids[$i]}" local entry="${entries[$i]}" for j in "${!snapper_ids[@]}"; do local snapper_id="${snapper_ids[$j]//[[:space:]]/}" # remove other non numeric characters snapper_id="${snapper_id//\*/}" snapper_id="${snapper_id//\+/}" snapper_id="${snapper_id//-/}" if [[ "$snapper_id" == "$id" ]]; then local snapper_type=$(trim "${snapper_types[$j]}") local snapper_description=$(trim "${snapper_descriptions[$j]}") # ignore snapper_type or snapper_description during run "grub-mkconfig" if [ -n "${GRUB_BTRFS_IGNORE_SNAPPER_TYPE}" ] ; then for ist in ${GRUB_BTRFS_IGNORE_SNAPPER_TYPE[@]} ; do [[ "${snapper_type}" == "${ist}" ]] && continue 3; done fi if [ -n "${GRUB_BTRFS_IGNORE_SNAPPER_DESCRIPTION}" ] ; then for isd in ${GRUB_BTRFS_IGNORE_SNAPPER_DESCRIPTION[@]} ; do [[ "${snapper_description}" == "${isd}" ]] && continue 3; done fi printf -v entry "%-${max_entry_length}s | %-${max_type_length}s | %s" "$entry" "$snapper_type" "$snapper_description" break fi done echo "$entry" done IFS=$oldIFS } ## Detect kernels in "/boot" detect_kernel() { list_kernel=() # Original kernel (auto-detect) for okernel in "${boot_dir}"/vmlinuz-* \ "${boot_dir}"/vmlinux-* \ "${boot_dir}"/kernel-* ; do [[ ! -f "${okernel}" ]] && continue; list_kernel+=("$okernel") done # Custom name kernel in GRUB_BTRFS_NKERNEL if [ -n "${GRUB_BTRFS_NKERNEL}" ] ; then for ckernel in "${boot_dir}/${GRUB_BTRFS_NKERNEL[@]}" ; do [[ ! -f "${ckernel}" ]] && continue; list_kernel+=("$ckernel") done fi } ## Detect initramfs in "/boot" detect_initramfs() { list_initramfs=() # Original initramfs (auto-detect) for oinitramfs in "${boot_dir}"/initrd.img-* \ "${boot_dir}"/initrd-*.img \ "${boot_dir}"/initrd-*.gz \ "${boot_dir}"/initramfs-*.img \ "${boot_dir}"/initramfs-*.gz ; do [[ ! -f "${oinitramfs}" ]] && continue; list_initramfs+=("$oinitramfs") done # Custom name initramfs in GRUB_BTRFS_NINIT if [ -n "${GRUB_BTRFS_NINIT}" ] ; then for cinitramfs in "${boot_dir}/${GRUB_BTRFS_NINIT[@]}" ; do [[ ! -f "${cinitramfs}" ]] && continue; list_initramfs+=("$cinitramfs") done fi if [ -z "${list_initramfs}" ]; then list_initramfs=(x); fi } ## Detect microcode in "/boot" detect_microcode() { list_ucode=() # Original intel/amd microcode (auto-detect) # See "https://www.gnu.org/software/grub/manual/grub/html_node/Simple-configuration.html" for oiucode in "${boot_dir}"/intel-uc.img \ "${boot_dir}"/intel-ucode.img \ "${boot_dir}"/amd-uc.img \ "${boot_dir}"/amd-ucode.img \ "${boot_dir}"/early_ucode.cpio \ "${boot_dir}"/microcode.cpio; do [[ ! -f "${oiucode}" ]] && continue; list_ucode+=("$oiucode") done # Custom name microcode in GRUB_BTRFS_CUSTOM_MICROCODE if [ -n "${GRUB_BTRFS_CUSTOM_MICROCODE}" ] ; then for cucode in "${boot_dir}/${GRUB_BTRFS_CUSTOM_MICROCODE[@]}" ; do [[ ! -f "${cucode}" ]] && continue list_ucode+=("$cucode") done fi if [ -z "${list_ucode}" ]; then list_ucode=(x); fi } ## Show full path snapshot or only name path_snapshot() { case "${GRUB_BTRFS_DISPLAY_PATH_SNAPSHOT:-"true"}" in true) name_snapshot=("${snap_full_name}");; *) name_snapshot=("${snap_full_name#*"/"}") esac } ## Title format in grub-menu title_format() { case "${GRUB_BTRFS_TITLE_FORMAT:-"p/d/n"}" in p/n/d) title_menu="${prefixentry} ${name_snapshot} ${snap_date_time}";; p/d) title_menu="${prefixentry} ${snap_date_time}";; p/n) title_menu="${prefixentry} ${name_snapshot}";; d/n) title_menu="${snap_date_time} ${name_snapshot}";; n/d) title_menu="${name_snapshot} ${snap_date_time}";; p) title_menu="${prefixentry}";; d) title_menu="${snap_date_time}";; n) title_menu="${name_snapshot}";; *) title_menu="${prefixentry} ${snap_date_time} ${name_snapshot}" esac } ## List of kernels, initramfs and microcode in snapshots boot_bounded() { # Initialize menu entries IFS=$'\n' for item in $(snapshot_list); do [[ ${limit_snap_show} -le 0 ]] && break; # fix: limit_snap_show=0 IFS=$oldIFS snap_full_name="$(echo "$item" | cut -d'|' -f2-)" # do not trim it to keep nice formatting snap_dir_name="$(echo "$item" | cut -d'|' -f2)" snap_dir_name="$(trim "$snap_dir_name")" snap_date_time="$(echo "$item" | cut -d' ' -f1-2)" snap_date_time="$(trim "$snap_date_time")" boot_dir="$gbgmp/$snap_dir_name$boot_directory" # Kernel (Original + custom kernel) detect_kernel if [ -z "${list_kernel}" ]; then continue; fi name_kernel=("${list_kernel[@]##*"/"}") # Detect rootflags detect_rootflags # Initramfs (Original + custom initramfs) detect_initramfs name_initramfs=("${list_initramfs[@]##*"/"}") # microcode (auto-detect + custom microcode) detect_microcode name_microcode=("${list_ucode[@]##*"/"}") # show snapshot found during run "grub-mkconfig" if [[ "${GRUB_BTRFS_SHOW_SNAPSHOTS_FOUND:-"true"}" = "true" ]]; then printf "Found snapshot: %s\n" "$item" >&2 ; fi # Show full path snapshot or only name path_snapshot # Title format in grub-menu title_format # convert /boot directory to root of GRUB (e.g /boot become /) boot_dir_root_grub="$(make_system_path_relative_to_its_root "${boot_dir}")" # Make menuentries make_menu_entries ### Limit snapshots found during run "grub-mkconfig" count_limit_snap=$((1+$count_limit_snap)) [[ $count_limit_snap -ge $limit_snap_show ]] && break; done IFS=$oldIFS } boot_separate() { boot_dir="${boot_directory}" # convert /boot directory to root of GRUB (e.g /boot become /) boot_dir_root_grub="$(make_system_path_relative_to_its_root "${boot_dir}")" # Kernel (Original + custom kernel) detect_kernel if [ -z "${list_kernel}" ]; then print_error "Kernels not found."; fi name_kernel=("${list_kernel[@]##*"/"}") # Initramfs (Original + custom initramfs) detect_initramfs name_initramfs=("${list_initramfs[@]##*"/"}") # microcode (auto-detect + custom microcode) detect_microcode name_microcode=("${list_ucode[@]##*"/"}") # Initialize menu entries IFS=$'\n' for item in $(snapshot_list); do [[ ${limit_snap_show} -le 0 ]] && break; # fix: limit_snap_show=0 IFS=$oldIFS snap_full_name="$(echo "$item" | cut -d'|' -f2-)" # do not trim it to keep nice formatting snap_dir_name="$(echo "$item" | cut -d'|' -f2)" snap_dir_name="$(trim "$snap_dir_name")" snap_date_time="$(echo "$item" | cut -d' ' -f1-2)" snap_date_time="$(trim "$snap_date_time")" # Detect rootflags detect_rootflags # show snapshot found during run "grub-mkconfig" if [[ "${GRUB_BTRFS_SHOW_SNAPSHOTS_FOUND:-"true"}" = "true" ]]; then printf "Found snapshot: %s\n" "$item" >&2 ; fi # Show full path snapshot or only name path_snapshot # Title format in grub-menu title_format # Make menuentries make_menu_entries # Limit snapshots found during run "grub-mkconfig" count_limit_snap=$((1+$count_limit_snap)) [[ $count_limit_snap -ge $limit_snap_show ]] && break; done IFS=$oldIFS } printf "Detecting snapshots ...\n" >&2 ; rm -f "$grub_directory/grub-btrfs.new" > "$grub_directory/grub-btrfs.new" # Create mount point then mounting [[ ! -d $gbgmp ]] && mkdir -p "$gbgmp" mount -o ro,subvolid=5 /dev/disk/by-uuid/"$root_uuid" "$gbgmp/" trap "unmount_gbgmp" EXIT # unmounting mount point on EXIT signal # Count menuentries count_warning_menuentries=0 # Count snapshots count_limit_snap=0 # detect uuid requirement check_uuid_required # Detects if /boot is a separate partition if [[ "${GRUB_BTRFS_OVERRIDE_BOOT_PARTITION_DETECTION:-"false"}" == "true" ]]; then printf "Info: Override boot partition detection : enable \n" >&2 ; boot_separate else if [[ "$root_uuid" != "$boot_uuid" ]]; then printf "Info: Separate boot partition detected \n" >&2 ; boot_separate else printf "Info: Separate boot partition not detected \n" >&2 ; boot_bounded fi fi # Show warn, menuentries exceeds 250 entries [[ $count_warning_menuentries -ge 250 ]] && printf "Generated %s total GRUB entries. You might experience issues loading snapshots menu in GRUB.\n" "${count_warning_menuentries}" >&2 ; # Show total found snapshots if [[ "${GRUB_BTRFS_SHOW_TOTAL_SNAPSHOTS_FOUND:-"true"}" = "true" && -n "${count_limit_snap}" && "${count_limit_snap}" != "0" ]]; then printf "Found %s snapshot(s)\n" "${count_limit_snap}" >&2 ; fi # if no snapshot found, exit if [[ "${count_limit_snap}" = "0" || -z "${count_limit_snap}" ]]; then print_error "No snapshots found." fi # Make a submenu in GRUB (grub.cfg) and move "grub-btrfs.new" to "grub-btrfs.cfg" if ${grub_script_check} "$grub_directory/grub-btrfs.new"; then cat "$grub_directory/grub-btrfs.new" > "$grub_directory/grub-btrfs.cfg" rm -f "$grub_directory/grub-btrfs.new" cat << EOF submenu '${submenuname}' ${protection_authorized_users}${unrestricted_access_submenu}{ configfile "\${prefix}/grub-btrfs.cfg" } EOF else print_error "Syntax errors are detected in generated grub-btrfs.cfg file." fi