#!/bin/bash
# run this script with -h option to see help info
VERSION="1.7.2 [22 Mar 2026]"
COLUMNS=$(stty size 2>/dev/null||echo 80); COLUMNS=${COLUMNS##* }

ask_yes_no ()
{
	local ANS
	ANS=""
	read -p " (y/n)? " -t 60 -r ANS
	echo "$ANS"
	[[ "$ANS" == "y" ]] && return 0
	[[ "$ANS" == "n" ]] && return 1
	echo "Invalid Response, aborting"
	exit 1
}

while getopts ":olhw" optname; do
  case "$optname" in
    "l")	CHANGELOG=y;;
    "o")	OLDER=y;;
    "h")  HELP=y;;
    "w")	COLUMNS=30000;; #suppress line-breaking
    "?")  echo "Unknown option $OPTARG"; exit 1;;
    ":")  echo "No argument value for option $OPTARG"; exit 1;;
    *)    # Should not occur
          echo "Unknown error while processing options"; exit 1;;
  esac
done
shift $((OPTIND-1))
[[ -z $QUIET ]] && THIS="$(basename "$0")";echo -e "\n$THIS v$VERSION by Dominic (try -h for help)\n${THIS//?/=}\n"
if [[ -n $HELP ]]; then
        echo -e "Command line program to shows installed Linux kernels in a \
Debian-based distro. It \
can remove superfluous ones, including related packages, updating grub \
or grub2 appropriately. Does not require a GUI. \
You are prompted before any dangerous stuff happens, and it will not allow \
removal of the kernel that is currently in use, but should still be used with \
care: once a kernel is gone, it's gone!

Tested under Ubuntu 10.04 to 22.04. May not work under Lubuntu (because \
kernel packages are differently named).

Usage    : $(basename "$0") [options] kernelnumbertoremove-filter

Examples:
    ./$(basename "$0")\t\t# list kernel packages and exit
    ./$(basename "$0") 2.6.32-24\t# remove 2.6.32-24 kernel
    ./$(basename "$0") 2.6.32\t# remove all 2.6.32-* kernels

Options  : -h show this help and exit
           -l show changelog and exit
           -o remove all kernels older than the active kernel

License: Copyright © 2026 Dominic Raferd. Licensed under the Apache License, \
Version 2.0 (the \"License\"); you may not use this file except in compliance \
with the License. You may obtain a copy of the License at \
https://www.apache.org/licenses/LICENSE-2.0. Unless required by applicable \
law or agreed to in writing, software distributed under the License is \
distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY \
KIND, either express or implied. See the License for the specific language \
governing permissions and limitations under the License.
"|fold -sw"$COLUMNS"
fi
if [[ -n $CHANGELOG ]]; then
	[[ -n $HELP ]] && echo "Changelog:"
	echo "\
1.7 [27 Aug 2024] - allow removal of specified subdirectories of /lib/modules - which can be left behind after matching kernels are removed by apt
1.6 [17 Nov 2022] - conform (where appropriate) with shellcheck
1.5 [23 Mar 2022] - also remove any leftover directory/ies in /lib/modules
1.4 [12 May 2015] - allow deletion of any kernel series
1.3 [11 Dec 2014] - add -o option (new different code)
"|fold -sw"$COLUMNS"
fi
[[ -n $HELP$CHANGELOG ]] && exit 0
# failsafe on any error
set -e
[[ $(id -u) != 0 ]] && echo -e "Error: $THIS must be run as root\n" && exit 1
if [[ ! -f /etc/debian_version ]]; then
	echo -e "Warning       :\tOS does not seem to be based on Debian"
else
	echo -e "Debian base   :\t$(cat /etc/debian_version)"
	if [[ -f /etc/lsb-release ]]; then
		source /etc/lsb-release
		echo -e "Distro version:\t$DISTRIB_DESCRIPTION"
	fi
fi

TMPF=$(mktemp)
grub2-install --version >"$TMPF" 2>/dev/null || grub-install --version >"$TMPF" 2>/dev/null || { echo -e "Fatal error   :\tfailed dependency/ies: grub-install. Aborting."; exit 1; }
grep -q "GNU GRUB 0" "$TMPF" && GRUBMAJOR=1 || GRUBMAJOR=2
GRUBVER="$(grep -o "[0-9]*\..*" "$TMPF")"
rm -- "$TMPF"
echo -e "Grub$GRUBMAJOR         :\t$GRUBVER"

CURRENTKERNEL="$(uname -r)"
echo -e "Active kernel :\t$CURRENTKERNEL\n"
for DEPENDENCY in dpkg apt-get; do
	command -v "$DEPENDENCY" >/dev/null 2>&1 || MISSING="$MISSING $DEPENDENCY"
done
[[ -z $MISSING ]] || { echo -e "Fatal error   :\tfailed dependency/ies:$MISSING. Aborting."|fold -sw"$COLUMNS"; exit 1; }

if [[ -n "$OLDER" ]]; then
	TMPF=$(mktemp)
	# using code by Alaa Ali at https://askubuntu.com/questions/401581/bash-one-liner-to-delete-only-old-kernels
	dpkg -l linux-{image,headers}-* | awk '/^ii/{print $2}' | grep -E '[0-9]+\.[0-9]+\.[0-9]+' | awk 'BEGIN{FS="-"}; {if ($3 ~ /[0-9]+/) print $3"-"$4,$0; else if ($4 ~ /[0-9]+/) print $4"-"$5,$0}' | sort -k1,1 --version-sort -r | sed -e "1,/$(uname -r | cut -f1,2 -d"-")/d" | grep -v -e "$(uname -r | cut -f1,2 -d"-")" | awk '{print $2}' >"$TMPF"
	[[ -s "$TMPF" ]] || { echo "No older kernels found"; exit 0; }
	echo -n "Older kernels : "; sed '2,$s/^/                /' "$TMPF"
	read -rt 20 -p "Do you wish to remove these (y/-)? "
	[ "$REPLY" != "y" ] && { echo "Exiting, no changes made"; exit 0; }
	read -rt 20 -p "Double checking: this is irreversible, are you sure (y/-)? "
	[ "$REPLY" != "y" ] && { echo "Exiting, no changes made"; exit 0; }
	<"$TMPF" xargs sudo apt-get -y purge || { echo "An error occurred, sorry"; exit 1; }
	echo "Completed successfully"
	exit 0
fi

echo "Currently installed kernel packages:"
ALLPACKAGES="$(dpkg --get-selections|grep -E "^(linux|kernel).*-[0-9]\..*[^e]install$"|awk '{print $1}')"
for PACKAGE in $ALLPACKAGES; do echo "  $PACKAGE"; done; echo
[[ -z $1 ]] && { echo "No action taken"; exit 0; }
PACKAGES=$(dpkg --get-selections|grep -E "(linux|kernel).*-$1.*[^e]install$"|awk '{print $1}')
TOREMOVE=$(echo "$PACKAGES"|wc -w)
TOKEEP=$(( $(echo "$ALLPACKAGES"|wc -w)-TOREMOVE ))
if echo "$PACKAGES"|grep -qF "$CURRENTKERNEL"; then
	echo "Sorry, your selection \"$1\" would remove the active kernel $CURRENTKERNEL, which is not allowed. Action aborted"|fold -sw"$COLUMNS"
	exit 1
elif [ $TOKEEP -lt 1 ]; then
	echo "Sorry, your selection \"$1\" would remove $TOREMOVE kernel packages, leaving $TOKEEP [minimum 1 required]. Action aborted"|fold -sw"$COLUMNS"
	exit 1
fi
if [[ $TOREMOVE -gt 1 ]]; then
	echo -en "We are about to remove the following kernel packages:\n$PACKAGES\n\nThis is irreversible. Are you sure you want to proceed"
	if ask_yes_no; then
		echo -e "\nRemoving (purging) packages:"
		# shellcheck disable=SC2086
		apt-get -y purge $PACKAGES
		echo -e "\nRemoval of $1 kernel(s) completed successfully."
	else
		echo "No action taken"
	fi
fi
find /lib/modules -maxdepth 1 -type d -name "$1*" -printf "%f\n" >"$TMPF"
if [[ -s $TMPF ]]; then
	REMOVABLE_MODULES=y
	echo -en "We are about to remove the following $(<"$TMPF" wc -l) directories and their contents from /lib/modules:\n  "
	<"$TMPF" tr '\n' ' '
	echo -en "\nThis is irreversible. Are you sure you want to proceed"
	if ask_yes_no; then
		# sometimes the relevant /lib/modules directories do not get removed, so do it manually
		while read -r DDIR; do
			if [[ -d /lib/modules/$DDIR ]]; then
				echo -n "  Removing /lib/modules/$DDIR: "
				rm -rf -- "/lib/modules/$DDIR" && echo "OK" || echo "FAIL"
			fi
		done <"$TMPF"
	else
		echo "No action taken"
	fi
fi

if [[ $TOREMOVE -eq 0 && -z $REMOVABLE_MODULES ]]; then
	echo "Nothing to do, no installed kernels or directories in /lib/modules match '$1*'"
else
	echo "Currently installed kernel packages:"
	ALLPACKAGES="$(dpkg --get-selections|grep -E "^(linux|kernel).*-[0-9]\..*[^e]install$"|awk '{print $1}')"
	for PACKAGE in $ALLPACKAGES; do echo "  $PACKAGE"; done
fi