Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- File: /usr/local/bin/yar
- USAGE: $ yar <-- Update the mirrors list.
- #!/usr/bin/env python3
- # Installation:
- # $ sudo cp ./yar /usr/local/bin/
- # $ sudo chmod +x /usr/local/bin/yar
- # ----- Colors -----
- BLACK = '\033[30m'
- RED = '\033[31m'
- GREEN = '\033[32m'
- YELLOW = '\033[33m'
- BLUE = '\033[34m'
- MAGENTA = '\033[35m'
- CYAN = '\033[36m'
- WHITE = '\033[37m'
- BRIGHT_BLACK = '\033[90m'
- BRIGHT_RED = '\033[91m'
- BRIGHT_GREEN = '\033[92m'
- BRIGHT_YELLOW = '\033[93m'
- BRIGHT_BLUE = '\033[94m'
- BRIGHT_MAGENTA = '\033[95m'
- BRIGHT_CYAN = '\033[96m'
- BRIGHT_WHITE = '\033[97m'
- # ----- Styles -----
- RESET = '\033[0m' # Reset all styles.
- BOLD = '\033[1m'
- HALF_BRIGHT = '\033[2m'
- ITALIC = '\033[3m'
- UNDERLINE = '\033[4m'
- REVERSED = '\033[7m' # Reverse foreground and background colors.
- import json
- import math
- import os
- import shutil
- import subprocess
- import sys
- import threading
- import time
- import urllib.request
- EARTH_RADIUS_KM = 6371.0
- MIN_DESIRED_MIRRORS = 20
- CURRENT_MIRRORLIST_PATH = '/etc/pacman.d/mirrorlist'
- BACKUP_MIRRORLIST_PATH = f'{CURRENT_MIRRORLIST_PATH}.backup'
- TEMP_MIRRORLIST_PATH = f'{CURRENT_MIRRORLIST_PATH}.new'
- def sudo_keep_alive():
- """Silently updates the sudo timestamp in the background."""
- while True:
- subprocess.run(['sudo', '-n', 'true'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
- time.sleep(60)
- def authenticate_sudo():
- """Prompts for sudo upfront and starts the background keep-alive thread."""
- if subprocess.run(['sudo', '-v']).returncode != 0:
- print(f"{RED}::{RESET} This script requires sudo privileges. Exiting.")
- sys.exit(1)
- # daemon=True ensures this thread dies instantly when the script exits
- keep_alive = threading.Thread(target=sudo_keep_alive, daemon=True)
- keep_alive.start()
- # Generated country coordinates (lat, lon).
- COUNTRY_COORDS = {
- 'Argentina': (-34, -64),
- 'Armenia': (40, 45),
- 'Australia': (-27, 133),
- 'Austria': (47.33333333, 13.33333333),
- 'Azerbaijan': (40.5, 47.5),
- 'Bangladesh': (24, 90),
- 'Belarus': (53, 28),
- 'Belgium': (50.83333333, 4),
- 'Brazil': (-10, -55),
- 'Bulgaria': (43, 25),
- 'Cambodia': (13, 105),
- 'Canada': (60, -95),
- 'Chile': (-30, -71),
- 'China': (35, 105),
- 'Colombia': (4, -72),
- 'Croatia': (45.16666666, 15.5),
- 'Czechia': (49.75, 15.5),
- 'Denmark': (56, 10),
- 'Ecuador': (-2, -77.5),
- 'Estonia': (59, 26),
- 'Finland': (64, 26),
- 'France': (46, 2),
- 'Georgia': (42, 43.5),
- 'Germany': (51, 9),
- 'Greece': (39, 22),
- 'Hong Kong': (22.267, 114.188),
- 'Hungary': (47, 20),
- 'Iceland': (65, -18),
- 'India': (20, 77),
- 'Indonesia': (-5, 120),
- 'Iran': (32, 53),
- 'Israel': (31.47, 35.13),
- 'Italy': (42.83333333, 12.83333333),
- 'Japan': (36, 138),
- 'Kazakhstan': (48, 68),
- 'Kenya': (1, 38),
- 'Latvia': (57, 25),
- 'Lithuania': (56, 24),
- 'Luxembourg': (49.75, 6.16666666),
- 'Malaysia': (2.5, 112.5),
- 'Mauritius': (-20.28333333, 57.55),
- 'Mexico': (23, -102),
- 'Moldova': (47, 29),
- 'Morocco': (32, -5),
- 'Nepal': (28, 84),
- 'Netherlands': (52.5, 5.75),
- 'New Caledonia': (-21.5, 165.5),
- 'New Zealand': (-41, 174),
- 'North Macedonia': (41.83333333, 22),
- 'Norway': (62, 10),
- 'Paraguay': (-23, -58),
- 'Philippines': (13, 122),
- 'Poland': (52, 20),
- 'Portugal': (39.5, -8),
- 'Romania': (46, 25),
- 'Russia': (60, 100),
- 'Réunion': (-21.15, 55.5),
- 'Saudi Arabia': (25, 45),
- 'Serbia': (44, 21),
- 'Singapore': (1.36666666, 103.8),
- 'Slovakia': (48.66666666, 19.5),
- 'Slovenia': (46.11666666, 14.81666666),
- 'South Africa': (-29, 24),
- 'South Korea': (37, 127.5),
- 'Spain': (40, -4),
- 'Sweden': (62, 15),
- 'Switzerland': (47, 8),
- 'Taiwan': (23.5, 121),
- 'Thailand': (15, 100),
- 'Tunisia': (34, 9),
- 'Türkiye': (39, 35),
- 'Ukraine': (49, 32),
- 'United Arab Emirates': (24, 54),
- 'United Kingdom': (54, -2),
- 'United States': (38, -97),
- 'Uzbekistan': (41, 64),
- 'Vietnam': (16.16666666, 107.83333333),
- }
- # Calculate the great-circle distance between two points on a sphere using the Haversine formula.
- def haversine_distance(lat1, lon1, lat2, lon2, radius):
- # Convert latitude and longitude from degrees to radians.
- lat1 = math.radians(lat1)
- lon1 = math.radians(lon1)
- lat2 = math.radians(lat2)
- lon2 = math.radians(lon2)
- # Calculate the differences.
- dlat = lat2 - lat1
- dlon = lon2 - lon1
- # Haversine formula.
- # SOURCE: https://www.movable-type.co.uk/scripts/latlong.html
- # The square of half the chord length between the points.
- a = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2
- # The angular distance in radians.
- c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
- d = radius * c
- return d
- # Generate a list of nearest countries.
- def generate_sorted_countries_list(lat, lon, max_countries=5):
- distances = []
- for country, (country_lat, country_lon) in COUNTRY_COORDS.items():
- dist = haversine_distance(lat, lon, country_lat, country_lon, EARTH_RADIUS_KM)
- distances.append((dist, country))
- distances.sort(key=lambda x: x[0])
- nearest = [country for _, country in distances[:max_countries]]
- return ','.join(nearest)
- # Count mirrors in a file.
- def count_mirrors(path):
- try:
- with open(path, 'r') as f:
- return sum(1 for line in f if line.strip().startswith('Server ='))
- except FileNotFoundError:
- return 0
- # Backup the current mirror list and update to a new one.
- def update_mirrorlist(source_path):
- if not os.path.exists(source_path):
- print(f'{RED}::{RESET} ERROR: Temp mirror list not found: {source_path}')
- print(f'{RED} Keeping existing mirror list.{RESET}')
- return False
- # Backup using sudo (using -p to preserve attributes of original fiile).
- backup_cmd = ['sudo', 'cp', '-p', CURRENT_MIRRORLIST_PATH, BACKUP_MIRRORLIST_PATH]
- if subprocess.call(backup_cmd) != 0:
- print(f'{RED}::{RESET} ERROR: Backup failed. Aborting mirror list update.{RESET}')
- return False
- print(f'{BRIGHT_BLUE}::{RESET} Backup created of the current mirror list: {BACKUP_MIRRORLIST_PATH}')
- # Replace using sudo (atomic mv).
- replace_cmd = ['sudo', 'mv', source_path, CURRENT_MIRRORLIST_PATH]
- if subprocess.call(replace_cmd) != 0:
- print(f'{RED}::{RESET} ERROR: Failed to replace mirror list. Aborting.{RESET}')
- print(f'{RED} Your backup is still available at: {BACKUP_MIRRORLIST_PATH}{RESET}')
- return False
- return True
- # Run reflector with closest countries.
- def update_arch_mirrors(user_lat, user_lon):
- if shutil.which('reflector') is None:
- print(f'{RED}::{RESET} Reflector could not be found. Install with:')
- print(' $ sudo pacman -S reflector')
- sys.exit(1)
- # Remove temp file using sudo (root owns it)
- subprocess.call(['sudo', 'rm', '-f', TEMP_MIRRORLIST_PATH])
- print(f'{BRIGHT_BLUE}::{RESET} Locating new pacman mirrors . . .')
- countries = generate_sorted_countries_list(user_lat, user_lon, 8)
- print(f'{BRIGHT_CYAN}Nearby Countries:{RESET} {countries}')
- cmd = [
- 'sudo', 'reflector',
- '--verbose',
- '--country', countries,
- '--protocol', 'https',
- '--sort', 'rate',
- '--age', '1',
- '--save', TEMP_MIRRORLIST_PATH,
- ]
- proc = subprocess.Popen(cmd)
- try:
- proc.wait()
- except KeyboardInterrupt:
- # Gracefully stop reflector if the user hits Ctrl+C
- proc.terminate()
- proc.wait()
- print(f'{RED}:: Mirror list update canceled by user . . .{RESET}')
- sys.exit(1)
- if proc.returncode != 0:
- print(f'{RED}:: Mirror list update failed (Reflector Return Code: {proc.returncode}). No changes made to mirror list.{RESET}')
- sys.exit(1)
- mirror_count = count_mirrors(TEMP_MIRRORLIST_PATH)
- print(f'{BRIGHT_BLUE}::{RESET} Reflector returned {mirror_count} mirrors.')
- if mirror_count == 0:
- # Delete empty mirrorlist.
- subprocess.call(['sudo', 'rm', '-f', TEMP_MIRRORLIST_PATH])
- sys.exit(0)
- elif mirror_count < MIN_DESIRED_MIRRORS:
- print(f'{RED}:: WARNING: Only {mirror_count} mirrors found but MIN_DESIRED_MIRRORS = {MIN_DESIRED_MIRRORS}.{RESET}')
- choice = input(f'{BRIGHT_BLUE}::{RESET} Apply new mirror list located at "{TEMP_MIRRORLIST_PATH}" [y/N] ? ').strip().lower()
- if choice != 'y':
- # TODO: Delete temp mirrorlist, or not so the user can look at it?
- #subprocess.call(['sudo', 'rm', '-f', TEMP_MIRRORLIST_PATH])
- sys.exit(0)
- if not update_mirrorlist(TEMP_MIRRORLIST_PATH):
- sys.exit(1)
- print(f'{BRIGHT_GREEN}:: Mirror list updated successfully . . .{RESET}')
- def get_user_location():
- # Fallback location: New York City
- lat = 40.730610
- lon = -73.935242
- try:
- # 3-second timeout so the script doesn't hang if offline
- with urllib.request.urlopen('http://ip-api.com/json/', timeout=3) as response:
- data = json.loads(response.read().decode())
- lat = data['lat']
- lon = data['lon']
- # Safely extract the city and region for terminal output
- city = data.get('city', 'Unknown City')
- region = data.get('regionName', 'Unknown Region')
- print(f'{BRIGHT_BLUE}::{RESET} User location detected as: {city}, {region}')
- except Exception:
- print(f'{YELLOW}::{RESET} User location detection failed. Defaulting to New York City.')
- return lat, lon
- def generate_country_coords():
- # Ask reflector for supported countries.
- result = subprocess.run(
- ['reflector', '--list-countries'],
- capture_output=True, text=True, check=True
- )
- reflector_lines = result.stdout.splitlines()[3:] # Skip headers.
- valid_countries = {
- ' '.join(line.split()[:-2]) for line in reflector_lines if line.strip()
- }
- # Query the static, open-source country dataset directly from GitHub.
- url = 'https://raw.githubusercontent.com/mledoze/countries/master/countries.json'
- req = urllib.request.Request(
- url,
- headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
- )
- try:
- with urllib.request.urlopen(req) as response:
- data = json.loads(response.read().decode())
- except urllib.error.URLError as e:
- print(f"Error fetching country dataset: {e}")
- return
- if not isinstance(data, list):
- print(f"Dataset Error: Expected list, got {type(data)}")
- return
- # Map country names to match reflector spelling.
- NAME_MAP = {
- # Not needed for Turkey anymore with this new source of country locations but
- # still a good example for how to override if other countries become a problem.
- # 'Turkey': 'Türkiye',
- }
- # Print as Python dict.
- print('COUNTRY_COORDS = {')
- for country in sorted(data, key=lambda c: c.get('name', {}).get('common', '') if isinstance(c, dict) else ''):
- if not isinstance(country, dict):
- continue
- name = country.get('name', {}).get('common')
- latlng = country.get('latlng')
- if not (name and latlng) or len(latlng) < 2:
- continue
- reflector_name = NAME_MAP.get(name, name)
- if reflector_name in valid_countries:
- lat, lon = latlng[0], latlng[1]
- print(f" '{reflector_name}': ({lat}, {lon}),")
- print('}')
- if __name__ == '__main__':
- # Uncomment to generate COUNTRY_COORDS dictionary.
- # generate_country_coords()
- # sys.exit(0)
- authenticate_sudo()
- user_lat, user_lon = get_user_location()
- update_arch_mirrors(user_lat, user_lon)
- File: /usr/local/bin/yip
- USAGE: $ yip <-- Update installed packages, delete downloaded packages, purge the orphans, and check if the system can boot, like a boss.
- #!/bin/bash
- # Installation:
- # $ sudo cp ./yip /usr/local/bin/
- # $ sudo chmod +x /usr/local/bin/yip
- # ----- Colors -----
- BLACK="\033[30m"
- RED="\033[31m"
- GREEN="\033[32m"
- YELLOW="\033[33m"
- BLUE="\033[34m"
- MAGENTA="\033[35m"
- CYAN="\033[36m"
- WHITE="\033[37m"
- BRIGHT_BLACK="\033[90m"
- BRIGHT_RED="\033[91m"
- BRIGHT_GREEN="\033[92m"
- BRIGHT_YELLOW="\033[93m"
- BRIGHT_BLUE="\033[94m"
- BRIGHT_MAGENTA="\033[95m"
- BRIGHT_CYAN="\033[96m"
- BRIGHT_WHITE="\033[97m"
- # ----- Styles -----
- RESET="\033[0m" # Reset all styles.
- BOLD="\033[1m"
- HALF_BRIGHT="\033[2m"
- ITALIC="\033[3m"
- UNDERLINE="\033[4m"
- REVERSED="\033[7m" # Reverse foreground/background.
- # ----- Configuration -----
- PKG_MGR="sudo pacman" # Repo packages only.
- # PKG_MGR="yay" # My favorite AUR helper.
- # TODO: These might work. AI says they wrap pacman syntax close enough to work with this script but untested.
- # PKG_MGR="paru" # Rust-based AUR helper.
- # PKG_MGR="pikaur" # Python-based AUR helper.
- # PKG_MGR="trizen" # Lightweight Perl AUR helper.
- # Prompt for sudo at the start.
- if ! sudo -v; then
- echo -e "${BRIGHT_BLUE}::${RESET} This script requires sudo privileges. Exiting."
- exit 1
- fi
- # Keep-alive: update existing sudo time stamp in the background until the script has finished.
- while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null &
- REFRESH_PID=$!
- # Kill the background Keep-alive loop when this script exits.
- # NOTE: If this fails the loop will kill itself after its sleep finishes.
- trap 'kill $REFRESH_PID 2>/dev/null' EXIT
- # Get the directory containing this script.
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
- tty_check() {
- # If running in a graphical session.
- if [[ -n $DISPLAY ]]; then
- while true; do
- echo
- echo -e "${CYAN}:: WARNING: Close ALL programs before continuing. Ideally you should boot to a TTY session before running this script.${RESET}"
- echo -e "${CYAN} Having programs running during an update can cause errors due to old libraries still being in use.${RESET}"
- echo
- read -p "Do you want to continue? [Y/n]: " response
- # Set response to 'Y' if you didn't press Y|N so Enter allows you to continue like with Yay/Pacman.
- response=${response:-Y}
- case "$response" in
- y|Y) break ;;
- n|N) exit 1 ;; # Exit script with status 1 to indicate failure.
- *) echo; echo "Please answer Y or N." ;;
- esac
- done
- fi
- }
- tty_check
- check_for_update_news() {
- echo -e "${BRIGHT_BLUE}::${RESET} Checking Arch Linux RSS feed . . ."
- "$SCRIPT_DIR/yap" --current
- status=$?
- case $status in
- 2) # News detected.
- echo
- echo -e "${BRIGHT_CYAN}:: WARNING: News event detected.${RESET}"
- ;;
- 1) # Error detected.
- # No need for a error message because the called script will display its own.
- # echo -e "${BRIGHT_RED}:: ERROR: Could not fetch Arch Linux news!${RESET}
- ;;
- 0) # No news or error detected.
- return
- ;;
- *) # Unexpected exit code.
- echo -e "${BRIGHT_RED}:: Unknown error occurred (exit code $status)!${RESET}"
- ;;
- esac
- while true; do
- echo
- read -p "Do you want to continue? [Y/n]: " response
- response=${response:-Y}
- case "$response" in
- y|Y) break ;;
- n|N) exit 1 ;; # Exit script with status 1 to indicate failure.
- *) echo; echo "Please answer Y or N." ;;
- esac
- done
- }
- check_for_update_news
- free_space_check() {
- local directory=$1
- local low_space_mb=$2
- # If directory does not exist or is not a directory.
- if [[ ! -d "$directory" ]]; then
- echo
- echo -e "${RED}::${RESET} ERROR: Directory '$directory' does not exist."
- exit 1 # Exit script with status 1 to indicate failure.
- fi
- # Get all space values in bytes.
- local low_space_bytes=$(awk "BEGIN {print $low_space_mb * 1024 * 1024}") # Using awk because bash will overflow: $ echo $((9999999999999 * 1024 * 1024)) # will display -7960984073710600192.
- local free_space_bytes=$(df --block-size=1 --output=avail "$directory" 2>/dev/null | tail -n1)
- local total_space_bytes=$(df --block-size=1 --output=size "$directory" 2>/dev/null | tail -n1)
- # If free space is less than low space.
- if awk -v free="$free_space_bytes" -v low="$low_space_bytes" 'BEGIN { exit !(free < low) }'; then
- local si_scalars=(1208925819614629174706176 1180591620717411303424 1152921504606846976 1125899906842624 1099511627776 1073741824 1048576 1024)
- local si_prefixes=("YiB" "ZiB" "EiB" "PiB" "TiB" "GiB" "MiB" "KiB")
- # Find the appropriate SI prefix.
- local scalar=1
- local prefix="KiB"
- for i in ${!si_scalars[@]}; do
- if awk -v space="$total_space_bytes" -v scalar="${si_scalars[$i]}" 'BEGIN { exit !(space >= scalar) }'; then
- scalar="${si_scalars[$i]}"
- prefix="${si_prefixes[$i]}"
- break
- fi
- done
- # Calculate output space info.
- local free_space_scaled=$(awk -v bytes="$free_space_bytes" -v scalar="$scalar" 'BEGIN { printf "%.1f", bytes / scalar }')
- local total_space_scaled=$(awk -v bytes="$total_space_bytes" -v scalar="$scalar" 'BEGIN { printf "%.1f", bytes / scalar }')
- local used_percent=$(awk -v free="$free_space_bytes" -v total="$total_space_bytes" 'BEGIN { printf "%.f", (1 - free / total) * 100 }')
- while true; do
- # Displays disk space info like Dolphin properties window.
- echo
- echo -e "${RED}:: WARNING: Low disk space detected. ${free_space_scaled} ${prefix} free of ${total_space_scaled} ${prefix} (${used_percent}% used) on drive '${directory}'.${RESET}"
- echo -e "${RED} Running out of room during an update can cause incomplete installations, system crashes, or render the system unbootable.${RESET}"
- echo
- read -p "Do you want to continue? [Y/n]: " response
- # Set response to 'Y' if you didn't press Y|N so Enter allows you to continue like with Yay/Pacman.
- response=${response:-Y}
- case "$response" in
- y|Y) break ;;
- n|N) exit 1 ;; # Exit script with status 1 to indicate failure.
- *) echo; echo "Please answer Y or N." ;;
- esac
- done
- fi
- }
- # WIP low-space checks. Feel free to mod.
- free_space_check /efi 50 # If you use /boot/efi then change "/efi" to "/boot/efi".
- free_space_check /boot 500
- free_space_check / 10240
- free_space_check "$HOME" 5120 # Probably not needed for updates but important to use computer.
- is_grub_installed() {
- command -v grub-install &>/dev/null || find /boot /efi /EFI -type d -iname "grub" -quit &>/dev/null
- }
- is_refind_installed() {
- command -v refind-install &>/dev/null || find /boot /efi /EFI -type d -iname "refind" -quit &>/dev/null
- }
- is_systemd_boot_installed() {
- # If the bootctl command does not exist return false (1).
- # This command is very likely to exist since all systems that installed the "base" arch package have systemd which installs bootctl.
- command -v bootctl &>/dev/null || return 1
- # Ask bootctl if systemd-boot is installed, otherwise look for it.
- sudo bootctl is-installed &>/dev/null || find /boot /efi /EFI -type d -iname "loader" -quit &>/dev/null
- }
- is_pacman_hanging() {
- local wait_count=0
- local wait_time=7
- while pgrep -x "pacman" >/dev/null || [ -f /var/lib/pacman/db.lck ]; do
- case $wait_count in
- 0)
- # First time don't print any warning.
- ;;
- 1)
- echo
- echo -e "${RED}:: WARNING: Pacman is still running or its lock is still active. Waiting . . .${RESET}"
- wait_time=$((wait_time * 2))
- ;;
- 2)
- wait_time=1
- echo
- echo -e "${RED}Pacman seems to be misbehaving. Waiting . . .${RESET}"
- echo
- echo -e "${RED}If you restart now you might find an unbootable machine due to missing or outdated initramfs and kernel modules.${RESET}"
- echo
- echo -e "${RED}Here are some steps that might help:${RESET}"
- echo
- echo -e "${RED}1. Kill the running pacman process and remove the lock:${RESET}"
- echo -e "${RED} $ sudo killall pacman${RESET}"
- echo -e "${RED} $ sudo rm /var/lib/pacman/db.lck${RESET}"
- echo -e "${RED} This is necessary to run pacman again.${RESET}"
- echo
- echo -e "${RED}2. Reinstall critical packages:${RESET}"
- echo -e "${RED} $ sudo pacman -S udev mkinitcpio linux${RESET}"
- echo -e "${RED} $ sudo mkinitcpio -P${RESET}"
- echo -e "${RED} This will reinstall essential packages and ensure the kernel and initramfs are created.${RESET}"
- echo
- echo -e "${RED}3. Verify package integrity:${RESET}"
- echo -e "${RED} $ sudo pacman -Qkk${RESET}"
- echo -e "${RED} This will check for any missing files or integrity issues with installed packages.${RESET}"
- echo
- if is_grub_installed; then
- echo -e "${RED}4. Regenerate the GRUB configuration:${RESET}"
- echo -e "${RED} $ sudo grub-mkconfig -o /boot/grub/grub.cfg${RESET}"
- elif is_refind_installed; then
- echo -e "${RED}4. Regenerate the rEFInd configuration:${RESET}"
- echo -e "${RED} $ sudo refind-install${RESET}"
- elif is_systemd_boot_installed; then
- echo -e "${RED}4. Regenerate the systemd-boot configuration:${RESET}"
- echo -e "${RED} $ sudo bootctl update${RESET}"
- else
- echo -e "${RED}4. Reinstall your boot loader.${RESET}"
- fi
- echo
- echo -e "${RED}5. After performing these steps try running this script again.${RESET}"
- echo
- echo -e "${RED}Press Ctrl+C to stop waiting for pacman and exit this script.${RESET}"
- ;;
- esac
- sleep $wait_time
- ((wait_count++))
- done
- if ((wait_count > 1)); then
- echo -e "${GREEN}:: Pacman seems to have recovered. Continuing with the script . . .${RESET}"
- fi
- }
- update_arch() {
- # Refresh package databases and ensure keyring is up-to-date.
- # SOURCE: https://www.reddit.com/r/archlinux/comments/1leg9ds/why_doesnt_pacman_just_install_archlinuxkeyring/
- is_pacman_hanging
- sudo pacman -Sy --noconfirm --needed archlinux-keyring # Skips force-refresh of package databases.
- # sudo pacman -Syy --noconfirm --needed archlinux-keyring # Uses -Syy to force-refresh package databases.
- # Update packages.
- is_pacman_hanging
- if [[ -n $DISPLAY ]]; then
- # Graphical environment.
- $PKG_MGR -Syu # Asks for confirmation to update packages.
- else
- # TTY environment.
- # $PKG_MGR -Syu --noconfirm # Skip confirmation prompt and update packages.
- $PKG_MGR -Syu # Asks for confirmation to update packages.
- fi
- echo
- echo -e "${BRIGHT_BLUE}::${RESET} Searching for *.pacnew, *.pacorig, and *.pacsave files . . ."
- sudo find /etc /usr -type f \( -iname '*.pacnew' -o -iname '*.pacorig' -o -iname '*.pacsave' \) | grep --color=always -E '.*'
- }
- update_arch
- check_pacman_database() {
- is_pacman_hanging
- echo
- echo -e "${BRIGHT_BLUE}::${RESET} Checking Pacman database for issues . . ."
- local output
- if output=$(sudo pacman -Dk 2>&1); then
- # echo
- # echo -e "${BRIGHT_BLUE}::${RESET} No Pacman database issues found . . ."
- return
- fi
- # If sudo privilege timeout, e.g.
- # sudo: timed out reading password
- # sudo: a password is required
- if [[ "$output" == *"sudo:"* ]]; then
- echo
- echo -e "${BRIGHT_RED}:: ERROR: sudo authorization was lost during execution.${RESET}"
- echo -e " ${BRIGHT_RED}$output${RESET}"
- exit 1
- fi
- echo
- echo -e "${BRIGHT_RED}Pacman database corruption detected:${RESET}"
- echo
- echo -e " ${BRIGHT_RED}$output"
- echo
- echo -e " ${BRIGHT_RED}Possible causes:${RESET}"
- echo -e " ${BRIGHT_RED}• System crash during a package transaction."
- echo -e " ${BRIGHT_RED}• Power loss during a package transaction."
- echo -e " ${BRIGHT_RED}• Disk corruption."
- echo
- echo -e " ${BRIGHT_RED}Recommendation:${RESET}"
- echo -e " ${BRIGHT_RED}Reinstall the affected package(s)."
- echo -e " ${BRIGHT_RED}You may need to force the reinstall if files exist in the install directory:"
- echo -e " ${BRIGHT_RED}$ sudo pacman -S --overwrite \* <package_name>"
- exit 1
- }
- check_pacman_database
- remove_orphaned_packages() {
- # Remove unneeded dependencies.
- # NOTE: Strangely both [$ yes | yay -Scc] and [$ yay -Scc --noconfirm] will prompt you to remove dependencies so [yay -Ycc --noconfirm] needs to be called first to prevent that prompt from displaying.
- # NOTE: The -Ycc flag specifically tells yay to clean up its own AUR build directory (usually ~/.cache/yay/) and remove "make dependencies" (packages that were only downloaded to compile an AUR package and are no longer needed).
- # Since pacman only handles pre-compiled binaries from the official repositories, it doesn't do any building, meaning it has no build cache or make dependencies to clean up.
- if [[ "$PKG_MGR" == "yay" ]]; then
- is_pacman_hanging
- echo
- echo -e "${BRIGHT_BLUE}::${RESET} Remove unneeded AUR build dependencies . . ."
- $PKG_MGR -Ycc --noconfirm
- fi
- # Clean Pacman cache directory /var/cache/pacman/pkg/, Pacman database directory /var/lib/pacman/, and Yay AUR build directory $HOME/.cache/yay/.
- # If you need to downgrade or install an outdated package then you will need to download it again.
- # NOTE: To just clean the Pacman directories the command is [$ yes | sudo pacman -Scc].
- is_pacman_hanging
- echo
- echo -e "${BRIGHT_BLUE}::${RESET} Clean package caches . . ."
- # Had to add this because [yay -Scc] calls [pacman -Scc] under the hood.
- # Pacman's cache directory normally contains files, but sometimes also directories.
- # When [pacman -Scc] tries to clear the cache it doesn't use [rm -r] so you get this error:
- # error: could not remove /var/cache/pacman/pkg/download-UuJ686: Is a directory
- # error: could not remove /var/cache/pacman/pkg/download-iKYDbe: Is a directory
- # error: could not remove /var/cache/pacman/pkg/download-6MC6vA: Is a directory
- # Remove directories that pacman struggles with removing.
- (
- shopt -s nullglob
- downloads=(/var/cache/pacman/pkg/download-*)
- (( ${#downloads[@]} )) && sudo rm -rf "${downloads[@]}"
- )
- yes 2>/dev/null | $PKG_MGR -Scc
- # Purge the orphans.
- is_pacman_hanging
- mapfile -t orphans < <(pacman -Qdtq)
- if [[ ${#orphans[@]} -gt 0 ]]; then
- echo
- echo -e "${BRIGHT_BLUE}::${RESET} Removing orphaned packages . . ."
- # sudo pacman -Rns "${orphans[@]}"
- sudo pacman -Rns --noconfirm "${orphans[@]}"
- else
- echo
- echo -e "${BRIGHT_BLUE}::${RESET} No orphaned packages found . . ."
- fi
- }
- remove_orphaned_packages
- remove_orphaned_kernel_modules() {
- # Build a map of installed kernel versions based on current /boot images.
- declare -A kernel_versions
- for img in /boot/vmlinuz-*; do
- [[ -f "$img" ]] || continue
- pkgname="${img#/boot/vmlinuz-}" # Strip '/boot/vmlinuz-' to get the kernel package name e.g. 'linux', 'linux-lts', 'linux-hardened', 'linux-zen' etc.
- # If a package named $pkgname is installed.
- if pacman -Q "$pkgname" &>/dev/null; then
- # Add an associative array entry to map a kernel package to its installed version.
- kernel_versions["$pkgname"]="$(pacman -Q "$pkgname" | awk '{print $2}')"
- fi
- done
- # Collect orphaned module directories.
- local orphans=()
- for dir in /usr/lib/modules/*/; do # The trailing '/' ensures only directories are matched.
- [[ -d "$dir" ]] || continue # Skip if this isn't a directory.
- module_dir=$(basename "$dir")
- skip=false
- for version in "${kernel_versions[@]}"; do
- [[ "$module_dir" == "$version" ]] && { skip=true; break; }
- done
- [[ $skip == true ]] && continue # Skip if this module's kernel is installed.
- # If no installed package owns this module directory.
- if ! pacman -Qo "/usr/lib/modules/${module_dir}" &>/dev/null; then
- orphans+=("${module_dir}") # Mark this module directory as orphaned.
- fi
- done
- # If no orphaned kernel modules found.
- if [[ ${#orphans[@]} -eq 0 ]]; then
- echo
- echo -e "${BRIGHT_BLUE}::${RESET} No orphaned kernel modules found . . ."
- return
- fi
- echo
- echo -e "${BRIGHT_BLUE}::${RESET} Remove orphaned kernel modules . . ."
- # Print installed kernels.
- echo
- echo -e " ${BRIGHT_CYAN}Installed kernels:${RESET}"
- for pkgname in "${!kernel_versions[@]}"; do
- echo -e " ${BRIGHT_CYAN}${pkgname}: ${kernel_versions[$pkgname]}${RESET}"
- done
- # Print orphaned modules.
- echo
- echo -e " ${BRIGHT_RED}Orphaned kernel modules:${RESET}"
- for orphan in "${orphans[@]}"; do
- echo -e " ${BRIGHT_RED}\"/usr/lib/modules/${orphan}\"${RESET}"
- done
- # Read DKMS status once and collect a list of unique module names.
- local dkms_lines=()
- local modules=()
- if command -v dkms &>/dev/null; then
- mapfile -t dkms_lines < <(dkms status)
- mapfile -t modules < <(printf '%s\n' "${dkms_lines[@]}" | awk -F',' '{print $1}' | sort -u)
- fi
- # Remove orphaned kernel modules.
- local remove_all=false
- for orphan in "${orphans[@]}"; do
- echo
- echo -e " ${BRIGHT_YELLOW}Remove \"/usr/lib/modules/${orphan}\"${RESET}"
- # Show DKMS modules associated with this orphan.
- matched_modules=()
- if [[ ${#modules[@]} -gt 0 ]]; then
- # Loop through each unique module.
- for mod in "${modules[@]}"; do
- for line in "${dkms_lines[@]}"; do
- # If module is built for the orphaned kernel.
- if [[ "$line" == "${mod}, ${orphan},"* ]]; then
- matched_modules+=("$mod")
- break
- fi
- done
- done
- # If DKMS modules exist which are associated with the orphaned kernel.
- if [[ ${#matched_modules[@]} -gt 0 ]]; then
- echo
- echo -e " ${BRIGHT_YELLOW}Remove DKMS modules associated with '${orphan}':${RESET}"
- for mod in "${matched_modules[@]}"; do
- echo -e " ${BRIGHT_YELLOW}${mod}${RESET}"
- done
- fi
- fi
- # Ask the user if they want to delete or skip.
- if [[ $remove_all == false ]]; then
- while true; do
- echo
- read -p " Do you want to continue? [Y/N/A]: " response
- case "$response" in
- y|Y) break ;; # Remove this orphan.
- n|N) continue 2 ;; # Skip to next orphan.
- a|A) remove_all=true; break ;; # Remove all orphans.
- *) echo; echo " Please answer Y, N, or A." ;;
- esac
- done
- fi
- sudo rm -rf "/usr/lib/modules/${orphan}"
- # Remove DKMS records.
- if [[ ${#matched_modules[@]} -gt 0 ]]; then
- for mod in "${matched_modules[@]}"; do
- echo -e " ${BRIGHT_CYAN}Removing DKMS record for '${mod}' on '${orphan}'${RESET}"
- sudo dkms remove "${mod}" -k "${orphan}" --all
- done
- fi
- done
- echo
- echo -e " ${BRIGHT_GREEN}Orphaned kernel module cleanup complete . . .${RESET}"
- }
- remove_orphaned_kernel_modules
- is_system_bootable() {
- # Find kernel images.
- # local kernel_images=($(ls /boot/vmlinuz-* 2>/dev/null))
- local kernel_images=($(shopt -s nullglob; echo /boot/vmlinuz-*))
- if [[ ${#kernel_images[@]} -eq 0 ]]; then
- echo
- echo -e "${MAGENTA}:: WARNING: No kernel images found in /boot${RESET}"
- exit 1
- fi
- # Create a list of expected initramfs files based on the list of detected kernels.
- local initramfs_images=()
- for kernel in "${kernel_images[@]}"; do
- local base_name="${kernel#/boot/vmlinuz-}"
- initramfs_images+=("/boot/initramfs-${base_name}.img")
- # As of version 40 of mkinitcpio, fallback initramfs generation is disabled by default: https://wiki.archlinux.org/title/Mkinitcpio
- # initramfs_images+=("/boot/initramfs-${base_name}-fallback.img")
- done
- # Check for missing kernel and initramfs files.
- local missing_files=()
- for file in "${kernel_images[@]}" "${initramfs_images[@]}"; do
- [[ ! -f "$file" ]] && missing_files+=("$file")
- done
- # Check for CPU-specific microcode image.
- local expected_ucode=""
- if grep -q "GenuineIntel" /proc/cpuinfo; then
- expected_ucode="/boot/intel-ucode.img"
- elif grep -q "AuthenticAMD" /proc/cpuinfo; then
- expected_ucode="/boot/amd-ucode.img"
- fi
- # If a known CPU is detected, verify its microcode file exists.
- if [[ -n "$expected_ucode" && ! -f "$expected_ucode" ]]; then
- missing_files+=("$expected_ucode")
- fi
- # Check for missing bootloader files.
- if is_grub_installed; then
- local grub_cfg
- grub_cfg=$(find /boot /efi /EFI -type f -path "*/grub/grub.cfg" -print -quit 2>/dev/null)
- [[ -z "$grub_cfg" ]] && missing_files+=("esp/grub.cfg")
- elif is_refind_installed; then
- local refind_conf
- refind_conf=$(find /boot /efi /EFI -type f -path "*/refind/refind.conf" -print -quit 2>/dev/null)
- [[ -z "$refind_conf" ]] && missing_files+=("esp/refind/refind.conf")
- elif is_systemd_boot_installed; then
- local loader_conf
- loader_conf=$(find /boot /efi /EFI -type f -path "*/loader/loader.conf" -print -quit 2>/dev/null)
- [[ -z "$loader_conf" ]] && missing_files+=("esp/loader/loader.conf")
- if ! find /boot /efi /EFI -type f -path "*/loader/entries/*.conf" -quit &>/dev/null; then
- missing_files+=("esp/loader/entries/*.conf")
- fi
- fi
- if [[ ${#missing_files[@]} -ne 0 ]]; then
- echo
- echo -e "${MAGENTA}:: WARNING: System boot files are missing:${RESET}"
- for file in "${missing_files[@]}"; do
- echo -e "${MAGENTA} $file${RESET}"
- done
- echo
- echo -e "${BOLD}${GREEN} Suggested repairs:${RESET}"
- echo
- echo -e "${BOLD}${GREEN} $ sudo mkinitcpio -P${RESET}"
- echo
- if is_grub_installed; then
- [[ -z "$grub_cfg" ]] && echo -e "${BOLD}${GREEN} $ sudo grub-mkconfig -o /boot/grub/grub.cfg${RESET}" && echo
- elif is_refind_installed; then
- [[ -z "$refind_conf" ]] && echo -e "${BOLD}${GREEN} $ sudo refind-install${RESET}" && echo
- elif is_systemd_boot_installed; then
- [[ -z "$loader_conf" ]] && echo -e "${BOLD}${GREEN} $ sudo bootctl update${RESET}" && echo
- else
- echo -e "${BOLD}${GREEN} Reinstall your boot loader.${RESET}" && echo
- fi
- exit 1
- fi
- echo
- echo -e "${BRIGHT_BLUE}::${RESET} No boot issues detected . . ."
- }
- is_system_bootable
- check_ssd_trim_timer() {
- if ! systemctl is-active --quiet fstrim.timer; then
- echo
- echo -e "${RED}::${RESET} WARNING: fstrim.timer not active! SSD trim will not run automatically . . ."
- echo -e "${RED}::${RESET} Run $ sudo systemctl enable fstrim.timer --now"
- else
- echo
- echo -e "${BRIGHT_BLUE}::${RESET} SSD trim timer is active . . ."
- fi
- }
- check_ssd_trim_timer
- File: /usr/local/bin/yap
- USAGE: $ yap <-- Show recent Arch Linux news events, highlighting events since the last system upgrade.
- #!/usr/bin/env python3
- # Installation:
- # $ sudo cp ./yap /usr/local/bin/
- # $ sudo chmod +x /usr/local/bin/yap
- import os
- # ----- Colors -----
- BLACK = '\033[30m'
- RED = '\033[31m'
- GREEN = '\033[32m'
- YELLOW = '\033[33m'
- BLUE = '\033[34m'
- MAGENTA = '\033[35m'
- CYAN = '\033[36m'
- WHITE = '\033[37m'
- BRIGHT_BLACK = '\033[90m'
- BRIGHT_RED = '\033[91m'
- BRIGHT_GREEN = '\033[92m'
- BRIGHT_YELLOW = '\033[93m'
- BRIGHT_BLUE = '\033[94m'
- BRIGHT_MAGENTA = '\033[95m'
- BRIGHT_CYAN = '\033[96m'
- BRIGHT_WHITE = '\033[97m'
- # ----- Styles -----
- RESET = '\033[0m' # Reset all styles.
- BOLD = '\033[1m'
- HALF_BRIGHT = '\033[2m'
- ITALIC = '\033[3m'
- UNDERLINE = '\033[4m'
- REVERSED = '\033[7m' # Reverse foreground and background colors.
- import html
- import re
- import shutil
- import subprocess
- import sys
- import time
- import xml.etree.ElementTree as ET
- from datetime import datetime
- from urllib.error import URLError, HTTPError
- from urllib.request import urlopen, Request
- NEWS_FEED_URL = "https://archlinux.org/feeds/news/"
- PACMAN_LOG = "/var/log/pacman.log"
- import argparse
- def usage():
- parser = argparse.ArgumentParser(
- description="Show recent Arch Linux news events, highlighting ones since your last system upgrade.",
- add_help=False
- )
- parser.add_argument("--brief", action="store_true", help="Show titles and URLs only (omit descriptions).")
- parser.add_argument("--current", action="store_true", help="Only show news since your last full upgrade.")
- parser.add_argument("--max", type=int, default=0, help="Limit number of news items shown.")
- parser.add_argument("--debug", action="store_true", help="Force all news to appear new (for testing).")
- parser.add_argument("--help", "-h", action="help", help="Show this help message and exit.")
- try:
- global args
- args = parser.parse_args()
- except SystemExit:
- sys.exit(0) # No news.
- def get_last_upgrade_time(debug=False):
- if debug:
- return 0
- try:
- with open(PACMAN_LOG, "r") as f:
- for line in reversed(f.readlines()):
- if "starting full system upgrade" in line:
- ts_match = re.search(r"\[(.*?)\]", line)
- if ts_match:
- ts_str = ts_match.group(1)
- return int(datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z").timestamp())
- except Exception:
- pass
- return 0 # Fallback if no match.
- def fetch_rss_feed(url):
- try:
- req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
- with urlopen(req, timeout=5) as response:
- return response.read().decode("utf-8")
- except HTTPError as e:
- print(f"{BRIGHT_RED}HTTP Error {e.code}: {e.reason}{RESET}", file=sys.stderr)
- except URLError as e:
- print(f"{BRIGHT_RED}URL Error: {e.reason}{RESET}", file=sys.stderr)
- return None
- def simplify_links(text):
- # # Convert <a href="url">text</a> to [text](url)
- # text = re.sub(r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)</a>', r'[\2](\1)', text)
- # # Simplify [text](url) to just text if text == url
- # text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', lambda m: m.group(1) if m.group(1) == m.group(2) else m.group(0), text)
- # Convert <a href="url">text</a> to [text](url), or just text if text == url.
- text = re.sub(
- r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)</a>',
- lambda m: m.group(1) if m.group(1) == m.group(2) else f'[{m.group(2)}]({m.group(1)})',
- text
- )
- # Unescape HTML entities recursively.
- prev = None
- while prev != text:
- prev = text
- text = html.unescape(text)
- # Strip remaining HTML tags.
- text = re.sub(r"<[^>]+>", "", text)
- return text.strip()
- def parse_feed(xml_data):
- root = ET.fromstring(xml_data)
- channel = root.find("channel")
- return channel.findall("item") if channel is not None else []
- def day_label(target_ts, now_ts=None):
- """
- Return TODAY, YESTERDAY, TOMORROW, or D-/D+ labels.
- Mirrors the behavior of the yow script.
- """
- if now_ts is None:
- # Midnight today
- today = datetime.now().strftime("%Y-%m-%d")
- now_ts = int(datetime.strptime(today, "%Y-%m-%d").timestamp())
- # Convert target timestamp to midnight of its date
- target_date = datetime.fromtimestamp(target_ts).strftime("%Y-%m-%d")
- target_midnight = int(datetime.strptime(target_date, "%Y-%m-%d").timestamp())
- # Full-day difference (floor division)
- days_diff = (target_midnight - now_ts) // 86400
- if days_diff == 0:
- return "TODAY"
- elif days_diff == -1:
- return "YESTERDAY"
- elif days_diff == 1:
- return "TOMORROW"
- elif days_diff < 0:
- return f"D{days_diff}"
- else:
- return f"D+{days_diff}"
- def display_news(items, last_upgrade, brief=False, current_only=False, max_events=0):
- now = int(time.time())
- count = 0
- for item in items:
- title = simplify_links(item.findtext("title", default=""))
- link = simplify_links(item.findtext("link", default=""))
- desc = simplify_links(item.findtext("description", default=""))
- pub_date = item.findtext("pubDate", default="")
- try:
- pub_ts = int(datetime.strptime(pub_date, "%a, %d %b %Y %H:%M:%S %z").timestamp())
- except Exception:
- continue
- if current_only and pub_ts <= last_upgrade:
- continue
- color = BRIGHT_RED if pub_ts > last_upgrade else BRIGHT_GREEN
- status = "[NEW] " if pub_ts > last_upgrade else ""
- label = day_label(pub_ts, now)
- iso_date = datetime.fromtimestamp(pub_ts).strftime("%Y-%m-%d")
- print(f"\n{color}{status}{label}/{iso_date} {title}{RESET}")
- print(f"{color}@ {RESET}{link}")
- if not brief:
- print(f"{color}>{RESET} {desc}")
- count += 1
- if max_events > 0 and count >= max_events:
- break
- return count > 0
- def main():
- usage()
- # Paging support.
- if sys.stdout.isatty() and "YAP_IN_LESS" not in os.environ and len(sys.argv) == 1:
- os.environ["YAP_IN_LESS"] = "1"
- if shutil.which("less"):
- pager = ["less", "-R"]
- shell = False
- else:
- pager = None
- shell = False
- if pager:
- proc = subprocess.Popen([sys.executable] + sys.argv, stdout=subprocess.PIPE)
- try:
- subprocess.run(pager, stdin=proc.stdout, shell=shell)
- except KeyboardInterrupt:
- pass
- if sys.platform != 'win32':
- proc.wait()
- code = proc.returncode
- if code in (0, 2): # Preserve 0 (no news) / 1 (error) / 2 (news) codes.
- sys.exit(code)
- else: # Convert all other return codes e.g. if the child process was killed by a signal, proc.returncode will be negative, to 1 (error).
- sys.exit(1) # Error.
- else:
- sys.exit(0) # No news.
- # Fetch feed and display.
- feed = fetch_rss_feed(NEWS_FEED_URL)
- if feed is None:
- sys.exit(1) # Error.
- last_upgrade = 0 if args.debug else get_last_upgrade_time()
- upgrade_iso = datetime.fromtimestamp(last_upgrade).isoformat()
- label = day_label(last_upgrade)
- print(f"{BRIGHT_GREEN}Last full system upgrade:{RESET} {label}/{upgrade_iso}")
- items = parse_feed(feed)
- try:
- success = display_news(items, last_upgrade, brief=args.brief, current_only=args.current, max_events=args.max)
- except BrokenPipeError:
- try:
- sys.stdout.close()
- except Exception:
- pass
- finally:
- sys.exit(0) # No news. The user quit by pressing Q.
- if success:
- sys.exit(2) # News.
- else:
- if args.current:
- print("No news detected since last upgrade")
- else:
- print("No news items found in the feed")
- sys.exit(0) # No news.
- if __name__ == "__main__":
- main()
- File: /usr/local/bin/yow
- USAGE: $ yow <-- Show a colorized version of /var/log/pacman.log.
- #!/bin/bash
- # Installation:
- # $ sudo cp ./yow /usr/local/bin/
- # $ sudo chmod +x /usr/local/bin/yow
- # ----- Colors -----
- BLACK="\033[30m"
- RED="\033[31m"
- GREEN="\033[32m"
- YELLOW="\033[33m"
- BLUE="\033[34m"
- MAGENTA="\033[35m"
- CYAN="\033[36m"
- WHITE="\033[37m"
- BRIGHT_BLACK="\033[90m"
- BRIGHT_RED="\033[91m"
- BRIGHT_GREEN="\033[92m"
- BRIGHT_YELLOW="\033[93m"
- BRIGHT_BLUE="\033[94m"
- BRIGHT_MAGENTA="\033[95m"
- BRIGHT_CYAN="\033[96m"
- BRIGHT_WHITE="\033[97m"
- # ----- Styles -----
- RESET="\033[0m" # Reset all styles.
- BOLD="\033[1m"
- HALF_BRIGHT="\033[2m"
- ITALIC="\033[3m"
- UNDERLINE="\033[4m"
- REVERSED="\033[7m" # Reverse foreground/background.
- PACMAN_LOG='/var/log/pacman.log'
- package_name=""
- mode=""
- date_value=""
- show_all=false
- no_color=false
- # ----- Members -----
- display_help() {
- cat <<EOF
- Display and colorize $PACMAN_LOG entries.
- USAGE: $ yow [OPTION]
- NOTE: If no option is provided then today's $(date +%Y-%m-%d) pacman.log entries will be displayed.
- Options:
- --all Show the entire pacman log colorized.
- --no-color Skip colorizing output.
- --date=YYYY-MM-DD Show entries for the given date.
- --day=N Show entries for N days ago.
- --day=today|yesterday Show entries for the named day.
- --since=YYYY-MM-DD Show entries starting from the given date.
- --since=N Show entries starting from N days ago.
- --since=today|yesterday Show entries starting from the named day.
- --today Alias for --day=today.
- --yesterday Alias for --day=yesterday.
- --related=package Only show entries related to the package and its dependencies.
- -h, --help Show this help message and exit.
- EOF
- }
- colorize_line() {
- local line="$1"
- # Skip colorizing output.
- if [[ "$no_color" == true ]]; then
- echo "$line"
- return
- fi
- case "$line" in
- *"error:"*|*"warning:"*)
- echo -e "${BRIGHT_YELLOW}${line}${RESET}" ;;
- *"[ALPM-SCRIPTLET]"*|*".pacnew"*|*".pacsave"*|*".pacorig"*)
- echo -e "${BRIGHT_BLACK}${line}${RESET}" ;;
- *"[ALPM] installed "*|*"[ALPM] upgraded "*)
- echo -e "${BRIGHT_CYAN}${line}${RESET}" ;;
- *"[ALPM] removed "*)
- echo -e "${BRIGHT_RED}${line}${RESET}" ;;
- *"[ALPM] downgraded "*)
- echo -e "${BRIGHT_MAGENTA}${line}${RESET}" ;;
- *)
- echo -e "$line" ;;
- esac
- }
- # ----- Date handling -----
- normalize_date() {
- local input="$1"
- date -d "$input" +%Y-%m-%d 2>/dev/null || return 1
- }
- resolve_date_arg() {
- local value="$1"
- case "$value" in
- today) date +%Y-%m-%d ;;
- yesterday) date -d "yesterday" +%Y-%m-%d ;;
- ''|*[!0-9]*) date -d "$value" +%Y-%m-%d 2>/dev/null ;; # full date string
- *) date -d "$value days ago" +%Y-%m-%d ;; # numeric days ago
- esac
- }
- create_date_label() {
- local log_date="$1"
- local now_ts log_ts days_diff
- # Convert log_date to seconds since epoch (midnight of that day)
- log_ts=$(date -d "$log_date" +%s 2>/dev/null) || return
- # Use midnight of today for comparison
- now_ts=$(date -d "$(date +%Y-%m-%d)" +%s)
- # Calculate difference in full days
- days_diff=$(( (log_ts - now_ts) / 86400 ))
- case $days_diff in
- -1) echo "YESTERDAY/" ;;
- 0) echo "TODAY/" ;;
- 1) echo "TOMORROW/" ;;
- *)
- if (( days_diff < 0 )); then
- echo "D${days_diff}/" # Negative days as D-3, D-4, etc.
- else
- echo "D+${days_diff}/" # Future days.
- fi ;;
- esac
- }
- create_dependency_regex() {
- local pkg="$1"
- if [[ -z "$pkg" ]]; then
- echo "" # Return "".
- return 0
- fi
- if ! pacman -Q "$pkg" &>/dev/null; then
- echo "ERROR: Package '$pkg' is not installed." >&2
- exit 1
- fi
- # Get dependencies.
- local pkgs=$(pactree -luo "$pkg" 2>/dev/null || true)
- # Include the main package.
- pkgs="$pkgs"$'\n'"$pkg"
- # Sanitize the pactree output.
- # Strip any version constraints (>=, =, <, > etc.), e.g. convert "glibc>=2.27" to "glibc".
- # Remove empty lines.
- pkgs=$(echo "$pkgs" | sed -E 's/[<>=].*$//' | sed '/^$/d')
- # Build a grep -E compatible regex for whole package name matching.
- # It only matches if the package name is surrounded by characters that are NOT alphanumeric, '_', '.', or '-'.
- # NOTE: I tried [echo "\b($(echo "$pkgs" | paste -sd'|' -))\b"] but it was too permissive.
- # '-' was treated as divider so a package like 'libxkbcommon' would also match 'lib32-libxkbcommon'.
- echo "(^|[^[:alnum:]_.-])($(echo "$pkgs" | paste -sd'|' -))($|[^[:alnum:]_.-])"
- }
- # ----- Log display -----
- print_header() {
- local text="$1"
- if [[ "$no_color" == true ]]; then
- echo ":: $text"
- else
- echo -e "${BRIGHT_BLUE}::${RESET} $text"
- fi
- }
- display_pacman_log() {
- local log_date="$1"
- local mode="${2:-exact}"
- if [[ ! -s "$PACMAN_LOG" ]]; then
- echo "ERROR: Log is empty or unreadable: $PACMAN_LOG" >&2
- exit 1
- fi
- local dependency_regex=$(create_dependency_regex "$package_name")
- local related_suffix=""
- [[ -n "$package_name" ]] && related_suffix=" (related to $package_name)"
- # Since-date mode.
- if [[ "$mode" == "since" ]]; then
- local current_date="$log_date"
- local today=$(date +%Y-%m-%d)
- local end_date=$(date -d "$today +1 day" +%Y-%m-%d)
- while [[ "$current_date" < "$end_date" ]]; do
- # Get all entries for the date.
- local matches
- matches=$(grep -F -- "$current_date" "$PACMAN_LOG" 2>/dev/null || true)
- # Apply package filter first if requested.
- if [[ -n "$dependency_regex" && -n "$matches" ]]; then
- matches=$(echo "$matches" | grep -E "$dependency_regex")
- fi
- # Only print header if matches exist.
- if [[ -n "$matches" ]]; then
- local date_label=$(create_date_label "$current_date")
- print_header "Viewing $PACMAN_LOG entries for ${date_label}${current_date}${related_suffix}"
- echo "$matches"
- echo
- fi
- current_date=$(date -d "$current_date +1 day" +%Y-%m-%d)
- done | while IFS= read -r line; do
- colorize_line "$line"
- done | less -R
- # Exact-date mode.
- else
- local matches=$(grep -F -- "$log_date" "$PACMAN_LOG" 2>/dev/null || true)
- if [[ -n "$dependency_regex" && -n "$matches" ]]; then
- matches=$(echo "$matches" | grep -E "$dependency_regex")
- fi
- if [[ -z "$matches" ]]; then
- local label=$(create_date_label "$log_date")
- print_header "No entries found for ${label}${log_date}${related_suffix} in $PACMAN_LOG"
- exit 0
- fi
- local label=$(create_date_label "$log_date")
- {
- print_header "Viewing $PACMAN_LOG entries for ${label}${log_date}${related_suffix}"
- echo "$matches"
- } | while IFS= read -r line; do
- colorize_line "$line"
- done | less -R
- fi
- }
- # ----- Argument parsing -----
- for arg in "$@"; do
- case "$arg" in
- -h|--help)
- display_help
- exit 0 ;;
- --related=*)
- package_name="${arg#--related=}" ;;
- --all)
- show_all=true ;;
- --no-color)
- no_color=true ;;
- --today)
- mode="exact"
- date_value="$(date +%Y-%m-%d)" ;;
- --yesterday)
- mode="exact"
- date_value="$(date -d yesterday +%Y-%m-%d)" ;;
- --date=*)
- mode="exact"
- date_value="${arg#--date=}" ;;
- --since=*)
- mode="since"
- date_value="$(resolve_date_arg "${arg#--since=}")" ;;
- --day=*)
- mode="exact"
- date_value="$(resolve_date_arg "${arg#--day=}")" ;;
- *)
- echo "ERROR: Unknown option '$arg'" >&2
- exit 1 ;;
- esac
- done
- # Check for conflicting options
- if [[ "$show_all" == true && -n "$date_value" ]]; then
- echo "ERROR: Cannot use --all with specific date options." >&2
- exit 1
- fi
- # Defaults
- if [[ -z "$date_value" && "$show_all" != true ]]; then
- mode="exact"
- date_value="$(date +%Y-%m-%d)"
- fi
- # Dispatch
- if [[ "$show_all" == true ]]; then
- first_date=$(grep -o '^\[[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}' "$PACMAN_LOG" | head -n1 | tr -d '[')
- if ! norm=$(normalize_date "$first_date"); then
- echo "ERROR: Could not determine start date of $PACMAN_LOG" >&2
- exit 1
- fi
- display_pacman_log "$norm" since
- else
- if ! norm=$(normalize_date "$date_value"); then
- echo "ERROR: Invalid date format: '$date_value'" >&2
- exit 1
- fi
- display_pacman_log "$norm" "$mode"
- fi
- File: /usr/local/bin/yup
- USAGE: $ yup <-- Updates my vim/nvim plugins.
- #!/bin/bash
- # Installation:
- # $ sudo cp ./yup /usr/local/bin/
- # $ sudo chmod +x /usr/local/bin/yup
- # ----- Colors -----
- BLACK="\033[30m"
- RED="\033[31m"
- GREEN="\033[32m"
- YELLOW="\033[33m"
- BLUE="\033[34m"
- MAGENTA="\033[35m"
- CYAN="\033[36m"
- WHITE="\033[37m"
- BRIGHT_BLACK="\033[90m"
- BRIGHT_RED="\033[91m"
- BRIGHT_GREEN="\033[92m"
- BRIGHT_YELLOW="\033[93m"
- BRIGHT_BLUE="\033[94m"
- BRIGHT_MAGENTA="\033[95m"
- BRIGHT_CYAN="\033[96m"
- BRIGHT_WHITE="\033[97m"
- # ----- Styles -----
- RESET="\033[0m" # Reset all styles.
- BOLD="\033[1m"
- HALF_BRIGHT="\033[2m"
- ITALIC="\033[3m"
- UNDERLINE="\033[4m"
- REVERSED="\033[7m" # Reverse foreground/background.
- # Prompt for sudo at the start.
- if ! sudo -v; then
- echo -e "${BRIGHT_BLUE}::${RESET} This script requires sudo privileges. Exiting."
- exit 1
- fi
- update_vim_plugins() {
- # ----- Vim -----
- if command -v vim &>/dev/null && [ -f ~/.vim/autoload/plug.vim ]; then
- echo -e "${BRIGHT_BLUE}::${RESET} Updating user vim plugins . . ."
- # Update ~/.vim/ plugins.
- vim -es +'silent! PlugUpgrade' +'silent! PlugUpdate' +qall
- echo
- if sudo -n true 2>/dev/null; then
- echo -e "${BRIGHT_BLUE}::${RESET} Updating root vim plugins . . ."
- # Update /root/.vim/ plugins.
- # -E is needed so the environment variables are available to the sudo instance of vim.
- # -H is needed so $HOME=/root instead of ~/home/wolf (a side effect of using the -E option).
- sudo -EH vim -es +'silent! PlugUpgrade' +'silent! PlugUpdate' +qall
- else
- echo -e "${BRIGHT_BLUE}::${RESET} Skipping root vim plugins (no sudo)."
- fi
- fi
- # ----- Neovim -----
- if command -v nvim &>/dev/null && [ -f ~/.local/share/nvim/site/autoload/plug.vim ]; then
- echo
- echo -e "${BRIGHT_BLUE}::${RESET} Updating user nvim plugins . . ."
- nvim --headless +'silent! PlugUpgrade' +'silent! PlugUpdate' +qall
- echo
- if sudo -n true 2>/dev/null; then
- echo -e "${BRIGHT_BLUE}::${RESET} Updating root nvim plugins . . ."
- sudo -EH nvim --headless +'silent! PlugUpgrade' +'silent! PlugUpdate' +qall
- else
- echo -e "${BRIGHT_BLUE}::${RESET} Skipping root nvim plugins (no sudo)."
- fi
- fi
- }
- update_vim_plugins
Advertisement
Add Comment
Please, Sign In to add comment