#!/usr/bin/env bash # ------------------------------------------------------------------------------ # Copyright (c) 2013-2026 Geoffray Levasseur # 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 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] \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 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] \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...] 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...]\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...] # -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...]\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