Skip to content

Instantly share code, notes, and snippets.

@DrSensor
Last active September 19, 2025 20:14
Show Gist options
  • Save DrSensor/b765f9cef67dadb3c6cb9eaa53df7833 to your computer and use it in GitHub Desktop.
Save DrSensor/b765f9cef67dadb3c6cb9eaa53df7833 to your computer and use it in GitHub Desktop.
pacman profile manager
#!/bin/bash
# pacprofile - Profile-aware wrapper for Arch package managers
# Supports multiple active profiles + pacman hook integration
# Default backend is pacman, override with PACMGR env var
PACMGR="${PACMGR:-pacman}"
BASE_DIR="/etc/pacprofile"
BASE_CONF="$BASE_DIR/pacman.conf"
ACTIVE_DIR="$BASE_DIR/active"
usage() {
cat <<EOF
Usage:
pacprofile upgrade [<profile...>|--system] # Upgrade active or given profiles
pacprofile list # List all profiles
pacprofile show <profile> # Show pkgs in profile
pacprofile set <profile...> # Set active profiles
pacprofile add <profile...> # Add to active profiles
pacprofile del <profile...> # Remove from active profiles
pacprofile current # Show active profiles
pacprofile install <profile> <pkg...> # Install into profile
pacprofile remove <profile> <pkg...> # Remove from profile
pacprofile status <profile> # Show status of profile
EOF
exit 1
}
profile_dir() { echo "$BASE_DIR/$1"; }
profile_pkgs() { echo "$(profile_dir "$1")/pkgs.txt"; }
profile_conf() {
local dir=$(profile_dir "$1")
[[ -f "$dir/pacman.conf" ]] && echo "$dir/pacman.conf" || echo "$BASE_CONF"
}
require_profile() {
local profile="$1"
[[ -f "$(profile_pkgs "$profile")" ]] || {
echo "Profile '$profile' not found in $BASE_DIR"
exit 1
}
}
all_profiles_union() {
find "$BASE_DIR" -mindepth 2 -maxdepth 2 -name "pkgs.txt" -exec cat {} + | sort -u
}
ignore_except() {
local allow_file="$1"
local installed=$(pacman -Qq | sort)
local allowed=$(sort "$allow_file")
comm -23 <(echo "$installed") <(echo "$allowed")
}
ignore_all_profiles() {
local installed=$(pacman -Qq | sort)
local all=$(all_profiles_union)
comm -23 <(echo "$installed") <(echo "$all")
}
active_profiles() {
[[ -d "$ACTIVE_DIR" ]] || return 0
find "$ACTIVE_DIR" -mindepth 1 -maxdepth 1 -type l -printf "%f\n" | sort
}
set_active_profiles() {
sudo rm -rf "$ACTIVE_DIR"
sudo mkdir -p "$ACTIVE_DIR"
for p in "$@"; do
require_profile "$p"
sudo ln -s "../$p" "$ACTIVE_DIR/$p"
done
}
add_active_profiles() {
sudo mkdir -p "$ACTIVE_DIR"
for p in "$@"; do
require_profile "$p"
sudo ln -sfn "../$p" "$ACTIVE_DIR/$p"
done
}
del_active_profiles() {
for p in "$@"; do
sudo rm -f "$ACTIVE_DIR/$p"
done
}
run_upgrade() {
local conf="$1"
local ignore="$2"
if [[ -n "$ignore" ]]; then
sudo $PACMGR --config "$conf" -Syu --ignore "${ignore%,}"
else
sudo $PACMGR --config "$conf" -Syu
fi
}
case "$1" in
list)
find "$BASE_DIR" -mindepth 1 -maxdepth 1 -type d -printf "%f\n"
;;
show)
[[ -z "$2" ]] && usage
require_profile "$2"
cat "$(profile_pkgs "$2")"
;;
set)
[[ $# -lt 2 ]] && usage
shift
set_active_profiles "$@"
echo "Active profiles set to: $*"
;;
add)
[[ $# -lt 2 ]] && usage
shift
add_active_profiles "$@"
echo "Added active profiles: $*"
;;
del)
[[ $# -lt 2 ]] && usage
shift
del_active_profiles "$@"
echo "Removed active profiles: $*"
;;
current)
active=$(active_profiles)
if [[ -z "$active" ]]; then
echo "No active profiles."
else
echo "Active profiles: $active"
fi
;;
install)
[[ $# -lt 3 ]] && usage
profile="$2"; shift 2
require_profile "$profile"
sudo $PACMGR -S "$@"
for pkg in "$@"; do
grep -qxF "$pkg" "$(profile_pkgs "$profile")" || echo "$pkg" | sudo tee -a "$(profile_pkgs "$profile")" >/dev/null
done
;;
remove)
[[ $# -lt 3 ]] && usage
profile="$2"; shift 2
require_profile "$profile"
sudo $PACMGR -Rns "$@"
for pkg in "$@"; do
sudo sed -i "/^$pkg$/d" "$(profile_pkgs "$profile")"
done
;;
upgrade)
shift
if [[ "$1" == "--system" ]]; then
IGNORE=$(ignore_all_profiles | tr '\n' ',')
run_upgrade "$BASE_CONF" "$IGNORE"
else
profiles=("$@")
if [[ ${#profiles[@]} -eq 0 ]]; then
profiles=($(active_profiles))
fi
[[ ${#profiles[@]} -eq 0 ]] && { echo "No profiles specified and no active profiles set."; exit 1; }
for profile in "${profiles[@]}"; do
require_profile "$profile"
CONF=$(profile_conf "$profile")
IGNORE=$(ignore_except "$(profile_pkgs "$profile")" | tr '\n' ',')
missing=$(comm -23 <(sort "$(profile_pkgs "$profile")") <(pacman -Qq | sort))
if [[ -n "$missing" ]]; then
echo "Installing missing packages for '$profile': $missing"
sudo $PACMGR --config "$CONF" -S $missing
fi
echo "Upgrading profile '$profile'..."
run_upgrade "$CONF" "$IGNORE"
done
fi
;;
status)
[[ -z "$2" ]] && usage
profile="$2"
require_profile "$profile"
installed=$(pacman -Qq | sort)
tracked=$(sort "$(profile_pkgs "$profile")")
missing=$(comm -23 <(echo "$tracked") <(echo "$installed"))
present=$(comm -12 <(echo "$tracked") <(echo "$installed"))
echo "==> Status for profile: $profile"
echo
echo "[Tracked packages]:"
echo "$tracked" | sed 's/^/ /'
echo
echo "[Installed packages in profile]:"
echo "$present" | sed 's/^/ /'
echo
echo "[Missing packages from system]:"
if [[ -n "$missing" ]]; then
echo "$missing" | sed 's/^/ /'
else
echo " (none)"
fi
echo
# Extras: only if user want global check
all_profiles=$(all_profiles_union)
extras=$(comm -23 <(echo "$installed") <(echo "$all_profiles"))
echo "[Extra installed packages (not in any profile)]:"
if [[ -n "$extras" ]]; then
echo "$extras" | sed 's/^/ /'
else
echo " (none)"
fi
;;
# --- Pacman hooks ---
hook-install)
pkg="$2"
for p in $(active_profiles); do
require_profile "$p"
grep -qxF "$pkg" "$(profile_pkgs "$p")" || echo "$pkg" | sudo tee -a "$(profile_pkgs "$p")" >/dev/null
done
;;
hook-remove)
pkg="$2"
for dir in $(find "$BASE_DIR" -mindepth 1 -maxdepth 1 -type d); do
sudo sed -i "/^$pkg$/d" "$dir/pkgs.txt" 2>/dev/null || true
done
;;
*)
usage
;;
esac
pkgname=pacprofile
pkgver=1.0.0
pkgrel=1
pkgdesc="pacman profile manager"
arch=('any')
url="https://gist.github.com/DrSensor/b765f9cef67dadb3c6cb9eaa53df7833"
license=('MIT')
depends=('pacman' 'bash')
backup=('etc/pacprofile/pacman.conf')
source=("$pkgname.sh")
sha256sums=('SKIP')
package() {
install -Dm755 "$srcdir/$pkgname.sh" "$pkgdir/usr/local/bin/$pkgname"
# config dir
install -d "$pkgdir/etc/pacprofile"
install -d "$pkgdir/etc/pacprofile/work"
install -d "$pkgdir/etc/pacprofile/gaming"
install -d "$pkgdir/etc/pacprofile/server"
# copy system pacman.conf as base
if [[ -f /etc/pacman.conf ]]; then
install -m644 /etc/pacman.conf "$pkgdir/etc/pacprofile/pacman.conf"
fi
# create empty pkgs.txt for profiles
touch "$pkgdir/etc/pacprofile/work/pkgs.txt"
touch "$pkgdir/etc/pacprofile/gaming/pkgs.txt"
touch "$pkgdir/etc/pacprofile/server/pkgs.txt"
# pacman hooks
install -d "$pkgdir/etc/pacman.d/hooks"
cat >"$pkgdir/etc/pacman.d/hooks/pacprofile-install.hook" <<'EOF'
[Trigger]
Operation = Install
Type = Package
Target = *
[Action]
Description = Update pacprofile pkgs.txt on install
When = PostTransaction
Exec = /usr/local/bin/pacprofile hook-install %n
EOF
cat >"$pkgdir/etc/pacman.d/hooks/pacprofile-remove.hook" <<'EOF'
[Trigger]
Operation = Remove
Type = Package
Target = *
[Action]
Description = Update pacprofile pkgs.txt on remove
When = PostTransaction
Exec = /usr/local/bin/pacprofile hook-remove %n
EOF
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment