After constructive community feedback to my post, I further tweaked, fine-tuned and improved my system update script that I want to share here - see end of this comment. First I want to expound on what the script does and does not do.
It is a Bash script I use on CachyOS for weekly system updates with deliberately conservative .pacnew handling.
This is not an attempt to fully automate system administration, and it does not merge or overwrite configuration files. The goal is to reduce noise while ensuring anything risky remains manual.
What It Does
- Runs a standard
pacman -Syu - Cleans package cache (
paccache) - Creates timestamped logs (with rotation)
- Skips execution entirely if the system is offline (
nm-online, suspend/resume safe)
.pacnew Handling (Key Point)
The script never applies .pacnew files automatically. Instead:
1. Identical / Comment-Only .pacnew
- Compared against the existing file (ignoring comments/blank lines)
- Deleted automatically
- Logged as safe cleanup
2. Critical Configuration Files
Paths related to boot, initramfs, networking, authentication, and system security are treated as critical, e.g.:
/etc/mkinitcpio.conf*
/etc/limine.conf
/etc/NetworkManager/
/etc/systemd/network/
/etc/pam.d/
/etc/sudoers
/etc/security/
For these:
- The
.pacnewis copied to a timestamped backup directory - The original config is left untouched
- A desktop notification is sent only if at least one critical pacnew exists
No merging, no overwriting.
3. Non-Critical .pacnew
- Removed automatically
- Logged for reference
Conditional Rebuilds Only
mkinitcpio -Pis run only if a critical mkinitcpio-related pacnew appears- Limine is updated only if Limine configs are involved
No unconditional rebuilds.
What This Script Does Not Do
No automatic .pacnewmerges
No config overwrites
No unattended system changes beyond pacman -Syu
No replacement for reading Arch/Cachy news
Manual review of critical files remains mandatory.
Summary
This is a noise-reduction helper, not a “set and forget” updater:
- Trivial
.pacnewfiles are cleaned up - Potentially dangerous ones are preserved and flagged
- Control stays with the user
If you already update manually and review configs, this just removes busywork.
Short FAQ / Anticipated Objections
Q: Why automate updates at all?
A: This script just ensures consistency and avoids forgetting, while keeping config changes manual.
Q: Why delete non-critical .pacnew files automatically?
A: They are defaults I have not customized. Critical paths are explicitly excluded.
Q: Why not use pacdiff / pacman-contrib?
A: The script does not replace them. It filters obvious no-ops and highlights when manual review is actually needed.
Q: Isn’t this against Arch philosophy?
A: The script does not automate decisions. It only automates detection and cleanup. All risky changes remain manual.
Q: What if the system is suspended or offline?
A: The script checks network availability first and exits cleanly if offline.
Note:
This script does not remove the need to understand or decide on .pacnew changes. It can be arguably safer for beginners than ad-hoc manual updates because
- critical configuration files are never overwritten automatically,
- identical or comment-only .pacnew files are filtered out to reduce noise,
- updates are skipped entirely when the system is offline, avoiding partial upgrades, and
- decisions about meaningful .pacnew changes remain manual — which beginners must make anyway, regardless of tooling.
It can be run manually with the noninteractive flag; I have a systemd timer to run it automatically in the background.
Disclaimer:
This script is provided as-is; review it before use and run it at your own risk, as with any custom update tooling on Arch-based systems.
Script
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# CachyOS update + pacnew manager
# - Notify ONLY when >=1 critical pacnew was backed up
# - Interactive & noninteractive modes
# - Auto-handling identical/comment-only pacnews
# - Auto backup of critical pacnews
# - mkinitcpio + Limine rebuild when needed
# - Log rotation, color output, emojis, clean summary
# - Network-aware (safe on suspend/resume)
# -----------------------------------------------------------------------------
# ------------------ Restore safe .bashrc first ------------------
if [ -f "$HOME/.bashrc_saved" ]; then
cp "$HOME/.bashrc_saved" "$HOME/.bashrc"
fi
# ------------- Configuration -------------
LOG_DIR="$HOME/logs/system-updates"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/update-$(date +%Y%m%d-%H%M%S).log"
# Keep last 5 update logs
ls -1t "$LOG_DIR"/update-*.log 2>/dev/null | tail -n +6 | xargs -r rm -f
# Tee output to terminal + log
exec > >(tee -a "$LOG_FILE") 2>&1
# -------- LOG HEADER --------
echo "============================================"
echo "System update run started at: $(date)"
echo "Host: $(hostname)"
echo "============================================"
echo
# -------- NETWORK GUARD (suspend/resume safe) --------
if command -v nm-online >/dev/null 2>&1; then
echo "Waiting for network (nm-online, timeout 60s)..."
if ! nm-online -q -t 60; then
echo
echo "============================================"
echo "UPDATE SKIPPED DUE TO NO NETWORK"
echo "Timestamp: $(date)"
echo "============================================"
exit 0
fi
else
echo "WARNING: nm-online not found; proceeding without network guard."
fi
BACKUP_DIR="$HOME/pacnew_backups"
ORPHAN_LOG_DIR="$HOME/orphan-removal-logs"
UPGRADE_LOG_DIR="$HOME"
SCRIPT_NAME="$(basename "$0")"
mkdir -p "$BACKUP_DIR" "$ORPHAN_LOG_DIR"
# Critical patterns
critical_patterns=(
"/etc/mkinitcpio.conf"
"/etc/mkinitcpio.conf.d/"
"/etc/systemd/resolved.conf"
"/etc/limine.conf"
"/etc/default/grub"
"/boot/loader/"
"/etc/NetworkManager/"
"/etc/systemd/network/"
"/etc/iwd/"
"/etc/dhcpcd.conf"
"/etc/wpa_supplicant/"
"/etc/nftables.conf"
"/etc/ufw/"
"/etc/pam.d/"
"/etc/sudoers"
"/etc/security/"
"/etc/locale.gen"
"/etc/locale.conf"
"/etc/vconsole.conf"
)
# Colors
RED="\e[31m"; GREEN="\e[32m"; YELLOW="\e[33m"; CYAN="\e[36m"
BOLD="\e[1m"; RESET="\e[0m"
# Flags & arrays
NONINTERACTIVE=false
REBUILD_MKINITCPIO=false
UPDATE_LIMINE=false
MKINIT_REQUIRED=false
LIMINE_REQUIRED=false
SCRIPT_SUCCESS=true
declare -a CRITICAL_BACKUPS
declare -a SAFE_DELETED
declare -a SKIPPED_PACNEW
# ------------- Helpers -------------
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [--noninteractive] [--help]
EOF
}
# Parse flags
while [[ $# -gt 0 ]]; do
case "$1" in
--noninteractive) NONINTERACTIVE=true; shift ;;
--help) usage; exit 0 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
print_heading() {
echo -e "${BOLD}${CYAN}=============================================="
echo -e " CachyOS Update & pacnew manager "
echo -e "==============================================${RESET}"
}
is_critical() {
local p
for p in "${critical_patterns[@]}"; do
if [[ "$p" == */ ]]; then [[ "$1" == $p* ]] && return 0
else [[ "$1" == "$p" || "$1" == "$p"* ]] && return 0
fi
done
return 1
}
effectively_identical() {
local a="$1" b="$2"
awk '!/^[[:space:]]*#/ && NF' "$a" > /tmp/.pa &&
awk '!/^[[:space:]]*#/ && NF' "$b" > /tmp/.pb
cmp -s /tmp/.pa /tmp/.pb
local r=$?
rm -f /tmp/.pa /tmp/.pb
return $r
}
color_diff() { diff --color=always -u "$1" "$2" || true; }
open_kwrite() {
command -v kwrite >/dev/null && kwrite "$@" >/dev/null 2>&1 &
}
backup_pacnew() {
local dest="$BACKUP_DIR/$(basename "$1").$(date +%Y%m%d-%H%M%S).bak"
sudo cp "$1" "$dest"
CRITICAL_BACKUPS+=("$1 -> $dest")
}
# ---------------- UPDATE -----------------
print_heading
command -v cachyos-rate-mirrors >/dev/null && sudo cachyos-rate-mirrors
echo -e "${CYAN}System upgrade...${RESET}"
sudo pacman -Syu --noconfirm || SCRIPT_SUCCESS=false
upgrade_log="$UPGRADE_LOG_DIR/cachyos-updates-$(date +%Y%m%d-%H%M%S).log"
awk '/upgraded/ {print}' /var/log/pacman.log | tail -n 20 > "$upgrade_log"
sudo paccache -rk3 <<< y
sudo paccache -ruk0 <<< y
echo -e "${CYAN}Checking for pacnews...${RESET}"
mapfile -t PACNEW_LIST < <(sudo find /etc -type f -name "*.pacnew")
for pac in "${PACNEW_LIST[@]}"; do
orig="${pac%.pacnew}"
[[ ! -f "$orig" ]] && backup_pacnew "$pac" && continue
if effectively_identical "$orig" "$pac"; then
sudo rm -f "$pac"
SAFE_DELETED+=("$pac (identical)")
continue
fi
if is_critical "$orig"; then
backup_pacnew "$pac"
[[ "$orig" == /etc/mkinitcpio.conf* ]] && MKINIT_REQUIRED=true
[[ "$orig" == *limine* ]] && LIMINE_REQUIRED=true
else
sudo rm -f "$pac"
SAFE_DELETED+=("$pac")
fi
done
# mkinitcpio
if $MKINIT_REQUIRED; then
sudo mkinitcpio -P || SCRIPT_SUCCESS=false
REBUILD_MKINITCPIO=true
fi
# Limine
if $LIMINE_REQUIRED; then
if command -v limine-update >/dev/null; then
sudo limine-update || SCRIPT_SUCCESS=false
elif command -v limine-mkinitcpio >/dev/null; then
sudo limine-mkinitcpio || SCRIPT_SUCCESS=false
fi
UPDATE_LIMINE=true
fi
# ---------- NOTIFICATION ----------
if [[ ${#CRITICAL_BACKUPS[@]} -gt 0 ]]; then
notify-send -u critical "Critical .pacnew Found" \
"⚠️ One or more CRITICAL pacnew files were saved to $BACKUP_DIR"
fi
# ---------- FINAL SUCCESS / FAILURE ----------
if $SCRIPT_SUCCESS; then
echo "SUMMARY: SUCCESS ✅"
notify-send -u normal "System Update Complete" "SUCCESS ✅"
else
echo "SUMMARY: FAILURE ❌"
notify-send -u critical "System Update FAILED" "FAILURE ❌ (see log)"
fi
exit 0