Files
profile/profile.d/packages.sh
T

562 lines
18 KiB
Bash

#!/usr/bin/env bash
# ------------------------------------------------------------------------------
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
# Protected by the BSD3 license. Please read bellow for details.
#
# * Redistribution and use in source and binary forms,
# * with or without modification, are permitted provided
# * that the following conditions are met:
# *
# * Redistributions of source code must retain the above
# * copyright notice, this list of conditions and the
# * following disclaimer.
# *
# * Redistributions in binary form must reproduce the above
# * copyright notice, this list of conditions and the following
# * disclaimer in the documentation and/or other materials
# * provided with the distribution.
# *
# * Neither the name of the copyright holder nor the names
# * of any other contributors may be used to endorse or
# * promote products derived from this software without
# * specific prior written permission.
# *
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# * OF SUCH DAMAGE.
# ------------------------------------------------------------------------------
load_conf "packages"
# Common helpers, then the ones private to this module (helpers/packages.sh)
# shellcheck source=../helpers/common.sh
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
_profile_load_helpers "${BASH_SOURCE[0]}"
# ------------------------------------------------------------------------------
# Detect the active package manager of the current distribution.
# Detection is based on /etc/os-release (ID / ID_LIKE), then falls back to
# checking available binaries in a fixed priority order.
# Echoes one of: apt dnf yum zypper pacman apk portage xbps nix
# Returns 1 if no known package manager could be identified.
# shellcheck disable=SC2120 # only --help is a possible argument
get_pkgmgr()
{
if [[ "${1:-}" == -h || "${1:-}" == --help ]]; then
printf "get_pkgmgr: print the package manager of the running distribution.\n"
printf "Usage: get_pkgmgr\n"
printf "Prints one of: apt dnf yum zypper pacman apk portage xbps nix (status 1 if unknown).\n"
return 0
fi
local distro_id="" distro_like=""
if [[ -r /etc/os-release ]]; then
# shellcheck disable=SC1091
distro_id=$( . /etc/os-release 2>/dev/null; printf '%s' "${ID:-}" )
# shellcheck disable=SC1091
distro_like=$( . /etc/os-release 2>/dev/null; printf '%s' "${ID_LIKE:-}" )
fi
# Map distro IDs/families to a package manager.
# ID_LIKE is space-separated and may list multiple families.
local id
for id in $distro_id $distro_like; do
case "${id,,}" in
debian|ubuntu|linuxmint|raspbian|pop|kali|elementary|zorin|neon|parrot)
echo "apt"
return 0
;;
fedora)
echo "dnf"
return 0
;;
rhel|centos|rocky|almalinux|ol|scientific|amzn)
command -v dnf >/dev/null 2>&1 && { echo "dnf"; return 0; }
echo "yum"
return 0
;;
opensuse*|suse|sles|sled)
echo "zypper"
return 0
;;
arch|manjaro|endeavouros|garuda|artix|cachyos)
echo "pacman"
return 0
;;
alpine)
echo "apk"
return 0
;;
gentoo)
echo "portage"
return 0
;;
void)
echo "xbps"
return 0
;;
nixos)
echo "nix"
return 0
;;
esac
done
# Fallback: check for binaries in priority order.
local bin
for bin in apt apt-get dnf yum zypper pacman apk emerge xbps-install nix-env; do
command -v "$bin" >/dev/null 2>&1 && {
case "$bin" in
apt-get) echo "apt" ;;
emerge) echo "portage" ;;
xbps-install) echo "xbps" ;;
nix-env) echo "nix" ;;
*) echo "$bin" ;;
esac
return 0
}
done
return 1
}
export -f get_pkgmgr
# ------------------------------------------------------------------------------
# Look for a package within installed one
# Usage: pkgs <string>
pkgs()
{
local ignore_case=${PKGS_DEFAULT_IGNORE_CASE:-0}
local PARSED
PARSED=$(getopt -o hi --long help,ignore-case -n 'pkgs' -- "$@")
# shellcheck disable=SC2181 # getopt return code is checked immediately after
if [[ $? -ne 0 ]]; then
disp E "Invalid options, use \"pkgs --help\" to display usage."
return 1
fi
local -a _pargs=()
_getopt_split _pargs "$PARSED"
set -- "${_pargs[@]}"
while true; do
case "$1" in
-h|--help)
printf "pkgs: Look for an installed package by its name.\n\n"
printf "Usage: pkgs [options] <string>\n\n"
printf "Options:\n"
printf "\t-h, --help\tDisplay this help screen\n"
printf "\t-i, --ignore-case\tIgnore case distinctions\n"
return 0
;;
-i|--ignore-case)
ignore_case=1
shift
;;
--)
shift
break
;;
*)
disp E "Invalid option: $1"
return 1
;;
esac
done
local pkg="$1"
[[ -z "$pkg" ]] && {
disp E "Please specify a package name, without space, eventually partial."
return 1
}
# Build grep command
local grep_opt=""
(( ignore_case )) && grep_opt="-i"
local pkgmgr
pkgmgr=$(get_pkgmgr) || {
disp E "No usable package manager could be detected on this system."
return 2
}
local -a list_cmd
case "$pkgmgr" in
apt) list_cmd=(dpkg-query -l) ;;
dnf|yum|zypper) list_cmd=(rpm -qa) ;;
pacman) list_cmd=(pacman -Q) ;;
apk) list_cmd=(apk list --installed) ;;
portage) list_cmd=(qlist -I) ;;
xbps) list_cmd=(xbps-query -l) ;;
nix) list_cmd=(nix-env -q) ;;
*)
disp E "Package manager '$pkgmgr' is not supported by pkgs."
return 2
;;
esac
# -e: a pattern starting with "-" must not be read as a grep option
"${list_cmd[@]}" | grep ${grep_opt:+"$grep_opt"} -e "$pkg"
}
export -f pkgs
# ------------------------------------------------------------------------------
# Find which installed package a file belongs to.
# Usage: pkgf <file>
pkgf()
{
local PARSED
PARSED=$(getopt -o h --long help -n 'pkgf' -- "$@")
# shellcheck disable=SC2181
if [[ $? -ne 0 ]]; then
disp E "Invalid options, use \"pkgf --help\" to display usage."
return 1
fi
local -a _pargs=()
_getopt_split _pargs "$PARSED"
set -- "${_pargs[@]}"
while true; do
case "$1" in
-h|--help)
printf "pkgf: Find which installed package owns a given file.\n\n"
printf "Usage: pkgf [options] <file>\n\n"
printf "Options:\n"
printf "\t-h, --help\tDisplay this help screen\n"
return 0
;;
--)
shift
break
;;
*)
disp E "Invalid option: $1"
return 1
;;
esac
done
local file="$1"
[[ -z "$file" ]] && {
disp E "Please specify a file path."
return 1
}
# A bare command name that is not a file here ("pkgf ls") is looked up in
# the PATH: the package managers expect a path, and would otherwise match
# the name as a pattern against every file of every package.
if [[ $file != */* && ! -e $file ]]; then
local _found
_found=$(command -v -- "$file" 2>/dev/null) && [[ $_found == /* ]] && file="$_found"
fi
# "./" keeps a name starting with "-" from being read as an option
[[ $file == -* ]] && file="./$file"
local pkgmgr
pkgmgr=$(get_pkgmgr) || {
disp E "No usable package manager could be detected on this system."
return 2
}
case "$pkgmgr" in
apt)
dpkg -S "$file"
;;
dnf|yum|zypper)
rpm -qf "$file"
;;
pacman)
pacman -Qo "$file"
;;
apk)
apk info --who-owns "$file"
;;
portage)
qfile "$file"
;;
xbps)
xbps-query -o "$file"
;;
nix)
nix-locate "$file"
;;
*)
disp E "Package manager '$pkgmgr' is not supported by pkgf."
return 2
;;
esac
}
export -f pkgf
# ------------------------------------------------------------------------------
# Install packages with the package manager of the distribution.
# Usage: pkgi [options] <package> [package...]
pkgi()
{
local assume_yes=0 dry_run=0
local PARSED
PARSED=$(getopt -o hyn --long help,yes,dry-run -n 'pkgi' -- "$@")
# shellcheck disable=SC2181 # getopt return code is checked immediately after
if [[ $? -ne 0 ]]; then
disp E "Invalid options, use \"pkgi --help\" to display usage."
return 1
fi
# Restore the getopt-normalised (shell-quoted) argument list
local -a _pargs=()
_getopt_split _pargs "$PARSED"
set -- "${_pargs[@]}"
while true; do
case "$1" in
-h|--help)
printf "pkgi: Install packages (distro-aware via get_pkgmgr).\n\n"
printf "Usage: pkgi [options] <package> [package...]\n\n"
printf "Options:\n"
printf "\t-h, --help\tDisplay this help screen\n"
printf "\t-y, --yes\tDo not ask for confirmation (where the manager asks)\n"
printf "\t-n, --dry-run\tOnly display the command that would be run\n\n"
printf "Root privileges are obtained with sudo (or doas) when needed.\n"
return 0
;;
-y|--yes)
assume_yes=1
shift
;;
-n|--dry-run)
dry_run=1
shift
;;
--)
shift
break
;;
*)
disp E "Invalid option: $1"
return 1
;;
esac
done
if [[ $# -eq 0 ]]; then
disp E "Please specify at least one package name."
return 1
fi
local pkg
for pkg in "$@"; do
# Not passed after "--": not every manager understands it
if [[ $pkg == -* ]]; then
disp E "Invalid package name: '$pkg'."
return 1
fi
done
local pkgmgr
pkgmgr=$(get_pkgmgr) || {
disp E "No usable package manager could be detected on this system."
return 2
}
local -a cmd=()
local needs_root=1
case "$pkgmgr" in
apt)
cmd=(apt-get install)
(( assume_yes )) && cmd+=(-y)
;;
dnf|yum)
cmd=("$pkgmgr" install)
(( assume_yes )) && cmd+=(-y)
;;
zypper)
cmd=(zypper install)
(( assume_yes )) && cmd+=(-y)
;;
pacman)
cmd=(pacman -S)
(( assume_yes )) && cmd+=(--noconfirm)
;;
apk)
# apk add never asks for confirmation
cmd=(apk add)
;;
portage)
cmd=(emerge)
(( assume_yes )) || cmd+=(--ask)
;;
xbps)
cmd=(xbps-install -S)
(( assume_yes )) && cmd+=(-y)
;;
nix)
# nix-env works on the user's profile: no root needed
cmd=(nix-env -i)
needs_root=0
;;
*)
disp E "Package manager '$pkgmgr' is not supported by pkgi."
return 2
;;
esac
_pkg_run "$needs_root" "$dry_run" "${cmd[@]}" "$@"
}
export -f pkgi
# ------------------------------------------------------------------------------
# Uninstall packages with the package manager of the distribution.
# Usage: pkgr [options] <package> [package...]
# -p, --purge also delete the configuration files (apt purge, pacman -n,
# apk --purge); managers without such a mode just remove
# -d, --deps also remove the dependencies no longer needed. Without it,
# the managers that do it by default (dnf, yum) are told not to
# (apk always cleans up: it has no way to prevent it)
pkgr()
{
local purge=0 deps=0 assume_yes=0 dry_run=0
local PARSED
PARSED=$(getopt -o hpdyn --long help,purge,deps,yes,dry-run -n 'pkgr' -- "$@")
# shellcheck disable=SC2181 # getopt return code is checked immediately after
if [[ $? -ne 0 ]]; then
disp E "Invalid options, use \"pkgr --help\" to display usage."
return 1
fi
# Restore the getopt-normalised (shell-quoted) argument list
local -a _pargs=()
_getopt_split _pargs "$PARSED"
set -- "${_pargs[@]}"
while true; do
case "$1" in
-h|--help)
printf "pkgr: Uninstall packages (distro-aware via get_pkgmgr).\n\n"
printf "Usage: pkgr [options] <package> [package...]\n\n"
printf "Options:\n"
printf "\t-h, --help\tDisplay this help screen\n"
printf "\t-p, --purge\tAlso delete configuration files (apt, pacman, apk)\n"
printf "\t-d, --deps\tAlso remove dependencies that are no longer needed\n"
printf "\t-y, --yes\tDo not ask for confirmation (where the manager asks)\n"
printf "\t-n, --dry-run\tOnly display the command that would be run\n\n"
printf "Root privileges are obtained with sudo (or doas) when needed.\n"
return 0
;;
-p|--purge)
purge=1
shift
;;
-d|--deps)
deps=1
shift
;;
-y|--yes)
assume_yes=1
shift
;;
-n|--dry-run)
dry_run=1
shift
;;
--)
shift
break
;;
*)
disp E "Invalid option: $1"
return 1
;;
esac
done
if [[ $# -eq 0 ]]; then
disp E "Please specify at least one package name."
return 1
fi
local pkg
for pkg in "$@"; do
if [[ $pkg == -* ]]; then
disp E "Invalid package name: '$pkg'."
return 1
fi
done
local pkgmgr
pkgmgr=$(get_pkgmgr) || {
disp E "No usable package manager could be detected on this system."
return 2
}
local -a cmd=() post=()
local needs_root=1 purge_done=0
case "$pkgmgr" in
apt)
# --auto-remove also drops the automatically installed packages
# nothing needs any more
if (( purge )); then cmd=(apt-get purge); purge_done=1; else cmd=(apt-get remove); fi
(( deps )) && cmd+=(--auto-remove)
(( assume_yes )) && cmd+=(-y)
;;
dnf|yum)
cmd=("$pkgmgr" remove)
# These managers remove the unneeded dependencies by default
(( deps )) || cmd+=(--setopt=clean_requirements_on_remove=False)
(( assume_yes )) && cmd+=(-y)
;;
zypper)
cmd=(zypper remove)
(( deps )) && cmd+=(--clean-deps)
(( assume_yes )) && cmd+=(-y)
;;
pacman)
# -n: no .pacsave backups of the configuration files;
# -s: also remove dependencies nothing else needs
local flags="-R"
(( purge )) && { flags+="n"; purge_done=1; }
(( deps )) && flags+="s"
cmd=(pacman "$flags")
(( assume_yes )) && cmd+=(--noconfirm)
;;
apk)
cmd=(apk del)
(( purge )) && { cmd+=(--purge); purge_done=1; }
;;
portage)
cmd=(emerge --unmerge)
(( assume_yes )) || cmd+=(--ask)
# depclean removes everything no longer needed, as autoremove does
(( deps )) && post=(emerge --depclean)
;;
xbps)
cmd=(xbps-remove)
(( deps )) && cmd+=(-R)
(( assume_yes )) && cmd+=(-y)
;;
nix)
cmd=(nix-env -e)
needs_root=0
# Dependencies live in the store: collect what is now unreachable
(( deps )) && post=(nix-collect-garbage)
;;
*)
disp E "Package manager '$pkgmgr' is not supported by pkgr."
return 2
;;
esac
if (( purge && ! purge_done )); then
disp W "$pkgmgr has no purge mode: the packages are removed normally."
fi
_pkg_run "$needs_root" "$dry_run" "${cmd[@]}" "$@" || return $?
if (( ${#post[@]} )); then
# Only reached when the removal itself succeeded
_pkg_run "$needs_root" "$dry_run" "${post[@]}"
fi
}
export -f pkgr
# ------------------------------------------------------------------------------
# EOF