562 lines
18 KiB
Bash
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
|