#!/bin/bash

ScriptName=apt
ScriptVersion=2.12.2
ScriptDate=2025-03-12

# Copyright:  2011-2025 Frank & Arjen @ SolydXK
# Licence:    EUPL 1.2 (GPL 2 compatible)
#             https://joinup.ec.europa.eu/collection/eupl/introduction-eupl-licence
# Disclaimer: Use entirely at your own risk.

Info="$ScriptName - version $ScriptVersion - $ScriptDate
A plain bash apt wrapper script, roughly based on Linux Mint's apt script.
Uses the APT Team's apt where possible. Add the -g switch to enforce using
apt-get or aptitude (where possible/applicable).

Usage: $ScriptName command [options] [arguments]
       $ScriptName help|--help|-h command [options] [arguments]

Commands:
autoclean       Erase all unavailable (see below) package files from local cache
autopurge       As autoremove and also remove configuration files
autoremove      Remove all unused, automatically installed packages 
build           Build binary or source packages from sources
build-dep       Configure build-dependencies for source packages
changelog       View the changelogs of packages
check           Verify that there are no broken dependencies
clean           Delete all package files from the local cache
contains        List packages containing the specified files
content         List files contained in packages
deb             Install .deb packages
depends         Show raw dependency information for packages
dist-upgrade    Perform an upgrade, possibly installing and removing packages
diversions      List all diversions (you may pass a string to limit the list)
divert          Divert a file
download        Download package files including dependencies
dselect-upgrade Follow dselect selections
edit-sources    Edit /etc/apt/sources.list with your favourite text editor
fetch [-u]      Download package files WITHOUT dependencies - optionally unpack
fix             Try to fix broken and/or misconfigured packages
full-upgrade    Perform an upgrade, possibly installing and removing packages
held            List all held packages
hold            Hold packages
install         Install or upgrade packages
installed       List installed packages - all if no names are specified
list            List available packages - all if no names are specified
listrepo        List installed packages from given repository
modernize       Modernize the available sources list(s) (apt 2.9.26 and later)
obsolete        List obsolete packages
pass            Pass any argument directly to the default apt executable
policy          Show policy settings for packages
purge           Remove packages and their configuration files
rdepends        Show reverse dependency information for packages
recommends      List missing recommended packages for a package (top level only)
reinstall       Download and (possibly) reinstall already installed packages
reinstall-all   Reinstall packages, including dependencies and recommends 
remove          Remove packages
repair          Try to fix broken and/or misconfigured packages
satisfy         Satisfy dependency strings
search          Search for packages by name and/or expression
show            Display detailed information about packages
showsrc         Show all source package records that match a given package name
source          Download source archives
sources         Edit /etc/apt/sources.list with your favourite text editor
stats           Show some information about the cache
sync [-a] [-c]  Update, full-upgrade and -optionally- autopurge and/or clean
unavailable     List installed packages no longer available in any repository
undivert [-b]   Undivert a file or all files diverted by the package mentioned
unhold          Unhold packages
update          Download lists of new/upgradable packages
upgradable      List upgradable packages - all if no names are specified
upgrade         Perform a safe upgrade
version         Show the installed version of packages or the script/apt itself

If a command takes arguments such as names of packages or files, more than one
argument may be given, unless that makes no sense (or the command is diversion
related).
All upgrade related commands can take a -v option, for a more detailed list of
packages to upgrade. A -k option may be given, if dpkg is to automatically keep
any old configuration file which was modified by the user."

while [ $# -gt 0 ]; do
	if [ "${1:0:1}" != "-" ]; then
		[ "$Cmd" ] && Args+=" $1" || Cmd=$1
		[ "$Cmd" == help ] && { unset -v Cmd; OptH=1; }
	else
		if [ "${1:1:1}" == "-" ]; then
			case "${1:2}" in
			help)OptH=1;;
			install)OptI=1;;
			version)Cmd=version; break;;
			*)Opts+=" $1"
			esac
		else
			S="${1:1}"
			for ((I=0; I<${#S}; I++)); do
				case "${S:I:1}" in
				a)OptA=1;;
				b)OptB=1;;
				c)OptC=1;;
				g)OptG=1;;
				h)OptH=1;;
				k)OptK=" -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold";;
				t)Opts+=" -t $2"; shift;;
				u)OptU=1;;
				v)OptV=" --verbose-versions";;
				*)Opts+=" -${S:I:1}"
				esac
			done
		fi
	fi
	shift
done
Args+=$Opts; Args=${Args:1}

[ $EUID == 0 ] || Sudo='sudo '

Usr=$(readlink -f "$0"); Usr=${Usr%/*/*}
Apt=$Usr/lib/solydxk/apt; [ -f $Apt ] || { Apt=/usr/bin/apt; [[ $(file -bi $Apt) != *script* ]] || Apt=/usr/bin/apt-get; }

Fetch(){
	local A D F I N R V
	for I in "$Args"; do
		R=$(LANG=C apt-get download $I)
		if [ $? == 0 ] && [ "$R" ]; then
			echo "$R"
			I=${I%=*}
			I=${I%:*}
			R=${R#*$I }
			A=${R%% *}
			V=${R#* }
			V=${V%% *}
			D=${I}_${V}_$A
			F=$D.deb
			declare -i N=0
			while [ -d $D ]; do
				[ $N == 0 ] || D=${D%.*}
				N+=1
				D+=".$N"
			done
			mkdir -p $D || continue
			dpkg-deb -R ${F//:/%3a} $D && echo "unpacked to $D"
			[ $OptA ] && [ -d "$LOCAL_PACKAGE_ARCHIVE" ] && D="$LOCAL_PACKAGE_ARCHIVE"
			mv -t $D ${F//:/%3a} && echo "package moved to $D"
		else
			echo "$I not fetched"
		fi
	done
}

IsInstalled(){
	local I=$(LANG=C dpkg-query -W -f='${db:status-abbrev}' $1 2>&1)
	test "${I:0:2}" == "ii"
}

Recommends(){
	[ "$1" ] || { aptitude search '?broken-reverse-recommends(?installed)'; return; }
	local E F I L M N R IFS=$'\n'
	while read -r L; do L=${L#Recommends:}
		[ "${L:0:1}" == ' ' ] && R+="$L," || break
	done < <(sed -n '/Recommends:/,//'p <(apt-cache show $1 2>/dev/null))
	[ "$R" ] || { echo "Package '$1' not found or has no recommended packages"; return; }
	R="${R//,/$'\n'}"
	[ $OptI ] || echo "The package '$1' has the following recommended packages:"
	for L in $R; do L=${L# }
		if [[ $L == *"|"* ]]; then
			M=$L; L+="|"; F=''
			until [ ! "$L" ]; do E=${L%%|*}; E=${E%% *}; L=${L#*|}
				IsInstalled "$E" && I+="$E " || F+="$E "
			done
			[ $OptI ] || echo "$M"
			if [ "$I" ]; then
				[ $OptI ] || echo "- at least one of these is installed (${I% })"
			else
				[ $OptI ] || echo "- none of these is installed"
				N+="${F%% *} "
			fi
		else
			L=${L%% *}
			[ $OptI ] || echo -n "$L (is "
			IsInstalled "$L" || { N+="$L "; [ $OptI ] || echo -n "not "; }
			[ $OptI ] || echo "installed)"
		fi
	done
	if [ "$N" ]; then
		[ $OptI ] && { apt --install-recommends install ${Args#* } $N; return; }
		echo -e "\nYou can install the missing recommended packages with the command:"
		echo "apt install --install-recommends $N"
		echo "or:"
		echo "apt --install recommends $Args"
		IsInstalled "$1" || echo -e "\nBe aware that '$1' is not installed either"
	else
		echo -e "\nNone of the recommended packages is missing"
	fi
}


case $Cmd in
autoremove|auto-remove|install|reinstall|remove|purge|update|satisfy|showsrc|autopurge)
	[ $OptG ] && Apt=apt-get
	Command="${Sudo}$Apt $Cmd $Args";;
clean|dselect-upgrade|build-dep|check|autoclean|auto-clean)
	echo "[]"
	Command="${Sudo}apt-get $Cmd $Args";;
upg|upgrade)
	[ $OptG ] && Apt=apt-get
	Command="${Sudo}$Apt upgrade$OptK$OptV $Args";;
di|dist-upgrade|du|fu|full|full-upgrade)
	[ $OptG ] && Apt=apt-get
	Command="${Sudo}$Apt full-upgrade$OptK$OptV $Args";;
bu|build)
	Command="${Sudo}dpkg-buildpackage $Args";;
changelog|moo|source|indextargets)
	Command="apt-get $Cmd $Args";;
contains|has)
	Command="dpkg-query --search $Args";;
content)
	Command="dpkg-query --listfiles $Args";;
deb)
	Command="${Sudo}dpkg -i $Args";;
depends|rdepends|stats)
	Command="apt-cache $Cmd $Args";;
div|diversions)
	Command="dpkg-divert --list"; [ "$Args" ] && Command="grep $Args < <($Command)";;
divert)
	R=$(sed -n 's|^.*diversion of \('$Args'\) .*$|\1|p' < <(LANG=C dpkg-divert --list))
	[ "$R" ] && Command="echo already exists" || Command="${Sudo}dpkg-divert --add --rename --divert $Args.divert $Args"
	;;
dl|do|download)
	[ "$(dpkg --print-architecture)" == "amd64" ] && F=i386 || F=amd64
	Command="LANG=C apt-cache depends $Args | sed -r '/.+:'$F'|Breaks:|Conflicts:|Enhances:|Provides:|Replaces:|Suggests:.+/d; s/^ .+ (.+)$/\\1/; s/[<>]//g' | xargs apt-get --install-recommends download ";;
edit-sources|sources)
	for F in $(find /etc/apt -type f | grep 'sources.list'); do
		[ ${F: -3} == 'bak' ] || Command+="${Sudo}editor $F;"
	done
	[ "$Command" ] && Command+="${Sudo}$Apt update"
	;;
fe|fetch|get)
	if [ $OptU ]; then
		Comment="\nThe 'Fetch' command with -u is a bash function defined in this script."
		Command="Fetch $Args"
	else
		Command="apt-get download $Args"
	fi;;
fi|fix|repair)
	Command="${Sudo}dpkg --configure --pending; ${Sudo}apt-get install --fix-broken";;
he|held)
	Command='dpkg --get-selections | grep hold';;
ho|hold)
	Command="for I in $Args; do ${Sudo}dpkg --set-selections <<<\"\$I hold\"; done";;
installed)
	Command="$Apt list --installed $Args";;
list)
	Command="$Apt list $Args";;
listrepo)
	Command="aptitude search \"?narrow(~i, ~A$Args)\"";;
obsolete|listobsolete|orphaned)
	# Get not used kernel packages
	KERNELS=($(dpkg -l | awk '/^ii  linux-(image|headers|kbuild)-[0-9]/ {print $2}' | grep -v "$(uname -r | sed 's/-.*//')"))

	# Get autoremove candidates
	AUTORM=($(LC_ALL=C apt-get -s autoremove | grep -E -i "^Remv " | cut -d' ' -f2))

	# Get obsolete packages
	OBSOLETE=($(aptitude search --display-format='%p' '~o !~ahold' 2>/dev/null))

	# Combine and deduplicate
	COMBINED=($(printf "%s\n" "${KERNELS[@]}" "${AUTORM[@]}" "${OBSOLETE[@]}" | sort -u))

	# Output
	printf "%s\n" "${COMBINED[@]}"
	;;
modernize|modernize-sources)
	Command="$Apt modernize-sources $Args";;
pass)
	Comment="\nUse this for apt commands that have not (yet) been added to this script."
	Command="$Apt $Args";;
po|policy)
	Command="apt-cache policy $Args";;
rec|recommends)
	Comment="\nThe 'Recommends' command is a bash function defined in this script."
	Command="Recommends $Args";;
reinstall-all)
	Command="${Sudo}apt-get reinstall $Args $(apt-cache depends $Args | grep -Po '(Depends:|Recommends:)\s+\K[^ ]+$' | grep -v '<' | tr '\n' ' ')";;
search|show)
	[ $OptG ] && Apt=aptitude
	Command="$Apt -a $Cmd $Args";;
sy|sync)
	Command="${Sudo}apt-get update && ${Sudo}apt-get dist-upgrade$OptK$OptV $Args"
	[ $OptA ] && Command+=" && ${Sudo}apt-get autopurge -y"
	[ $OptC ] && Command+=" && ${Sudo}apt-get clean"
	;;
una|unavailable)
	Command="apt-show-versions | sed -n 's/^\(\S*\):.* .* available .*$/\\1/p'";;
und|undivert)
	if [ $OptB ]; then
		R=$(sed -n 's|^.*diversion of \(\S*\) .* by '$Args'.*$|\1|p' < <(LANG=C dpkg-divert --list))
	else
		R=$(sed -n 's|^.*diversion of \('$Args'\) .*$|\1|p' < <(LANG=C dpkg-divert --list))
	fi
	for D in $R; do Command+="${Sudo}dpkg-divert --rename --remove $D;${OptH:+$'\n'}"; done
	[ "$Command" ] && Command="${Command:0:-1}" || Command="echo no such diversion"
	;;
unh|unhold)
	Command="for I in $Args; do ${Sudo}dpkg --set-selections <<<\"\$I install\"; done";;
upg|upgradable)
	Command="$Apt list --upgradable $Args";;
ve|version)
	if [ "$Args" ]; then
		Command="LANG=C apt-cache policy $Args | sed -n 's/^ *Installed: \(.*\)$/\\1/p'"
	else
		Command="echo -en \"SolydXK : $ScriptName $ScriptVersion ($ScriptDate)\nAPT Team: \"; $Apt --version"
	fi;;
*)
	[ "$Cmd" ] && echo -e "$ScriptName: unknown command '$Cmd' - apt pass $Cmd may work\n"
	echo "$Info"
	exit
esac

if [ $OptH ]; then
	Cmd="$Cmd $Args"
	echo -e "\"$ScriptName ${Cmd% }\" is equivalent to:\n${Command/$Apt/apt}$Comment"
	Doc=$Usr/share/doc/solydxk-system/$ScriptName/info; [ -f $Doc ] && . $Doc "${Cmd% }"
else
	eval "$Command"
fi
