packages bug fix; added pkgi and pkgr

This commit is contained in:
2026-10-13 14:09:00 +02:00
parent 3fbe9e85e2
commit f78bfe5859
4 changed files with 326 additions and 2 deletions
+2
View File
@@ -128,6 +128,8 @@ A bar-style prompt showing current time, execution time of the last command
| `get_pkgmgr` | packages | Detect the active package manager of the running distribution (`apt`, `dnf`, `yum`, `zypper`, `pacman`, `apk`, `portage`, `xbps`, `nix`) |
| `pinfo` | info | Display system information read from `/proc` and `/sys` only |
| `pkgf` | packages | Find which installed package owns a given file (distro-aware via `get_pkgmgr`) |
| `pkgi` | packages | Install packages with the distribution's manager (distro-aware via `get_pkgmgr`); `-y` assumes yes, `-n` only shows the command; uses `sudo` (or `doas`) when not root |
| `pkgr` | packages | Uninstall packages; `-p`/`--purge` also deletes configuration files (apt, pacman, apk; other managers just remove), `-d`/`--deps` also removes dependencies no longer needed (`--auto-remove`, `-Rs`, `--clean-deps`, `depclean`, ... — apk always does, dnf/yum are told not to unless `-d`); `-y`, `-n` as `pkgi` |
| `pkgs` | packages | Search for a pattern in installed package names (distro-aware via `get_pkgmgr`) |
| `ppg` | processes | Look for the given pattern in running processes |
| `ppn` | processes | List processes matching an exact command name |
+5
View File
@@ -11,6 +11,9 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`).
### Added
- `pkgi` (install) and `pkgr` (uninstall, with `-p`/`--purge` and `-d`/`--deps`)
commands for apt, dnf/yum, zypper, pacman, apk, portage, xbps and nix; both
accept `-y`/`--yes` and `-n`/`--dry-run` and use `sudo` (or `doas`) when needed.
- `utaz -i` / `--ignore-case` and `-s` / `--case-sensitive` switches to match
archive extensions case-insensitively or not; the default is set by the new
`UTAZ_DEFAULT_IGNORE_CASE` key of the `[compress]` section (default `0`).
@@ -106,6 +109,8 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`).
403, ...) are now failures instead of saved error pages; `urlencode`
mis-encoded accents under `LC_ALL` and now uses `%20` for spaces; `isipv4`
leaked a variable.
- `packages.sh`: `pkgf ls` now looks the command up in the `PATH`; `pkgs`
accepts patterns starting with `-`; `suse` is recognised in `ID_LIKE`.
- `compress.sh`: option flags and loop variables no longer leak into the
interactive shell.
+2
View File
@@ -102,6 +102,8 @@ help()
printf "myextip\t\tGet information about your public IP address\n"
printf "pinfo\t\tDisplay system information read from /proc and /sys only\n"
printf "pkgf\t\tFind which installed package owns a given file (distro-aware)\n"
printf "pkgi\t\tInstall packages (distro-aware, sudo/doas when needed)\n"
printf "pkgr\t\tUninstall packages; -p purges configuration, -d removes unneeded dependencies\n"
printf "pkgs\t\tSearch for a pattern in installed package names (distro-aware)\n"
printf "ppg\t\tLook for the given pattern in running processes\n"
printf "ppn\t\tList processes matching an exact command name\n"
+317 -2
View File
@@ -71,7 +71,7 @@ get_pkgmgr()
echo "yum"
return 0
;;
opensuse*|sles|sled)
opensuse*|suse|sles|sled)
echo "zypper"
return 0
;;
@@ -193,7 +193,8 @@ pkgs()
;;
esac
"${list_cmd[@]}" | grep ${grep_opt:+"$grep_opt"} "$pkg"
# -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
# ------------------------------------------------------------------------------
@@ -240,6 +241,15 @@ pkgf()
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) || {
@@ -279,4 +289,309 @@ export -f pkgf
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Internal helper: run a package manager command, with root privileges when
# needed (sudo, else doas), or only display it in dry-run mode.
# Usage: _pkg_run <needs_root 0|1> <dry_run 0|1> <command> [args...]
_pkg_run()
{
local needs_root="$1" dry="$2"
shift 2
local -a prefix=()
if (( needs_root )) && [[ $EUID -ne 0 ]]; then
if command -v sudo >/dev/null 2>&1; then
prefix=(sudo)
elif command -v doas >/dev/null 2>&1; then
prefix=(doas)
elif (( ! dry )); then
disp E "Root privileges are required and neither sudo nor doas is available."
return 1
fi
fi
if (( dry )); then
# %q gives a copy-pasteable command line
printf '%q ' "${prefix[@]}" "$@"
printf '\n'
return 0
fi
if ! command -v "$1" >/dev/null 2>&1; then
disp E "'$1' is not installed."
return 1
fi
"${prefix[@]}" "$@"
}
export -f _pkg_run
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# 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
eval set -- "$PARSED"
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
eval set -- "$PARSED"
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