#!/bin/bash # simple package installs typeset -r AUR='https://aur.archlinux.org' typeset -r BUILDDIR="$HOME/.cache/aur_builds" typeset -a PACPKGS=() typeset -a AURPKGS=() typeset -A DESC=([s]='search' [u]='update' [i]='install') typeset -A OPTS=([s]='RUN=search' [u]='RUN=update' [i]='RUN=installp' [n]='NOVIEW=--noview' [N]='NOCONF=--noconfirm' [a]='AURONLY=--auronly') use() { # show the standard help message.. if [[ $1 == '-v' ]]; then cat << EOF baph-VERSION EOF else cat << EOF baph - Simple helper to search, install, and update AUR packages usage: baph [options] [package(s)] operations: baph {-h --help} baph {-v --version} baph {-s --search} baph {-u --update} [options] [package(s)] baph {-i --install} [options] options: --noview, -n Skip viewing PKGBUILD files --auronly, -a Only operate on AUR packages --noconfirm, -N Skip confirmation dialogs examples: install 'google-chrome' and 'yay' from the AUR baph -i google-chrome yay search for AUR packages matching 'cmus' baph -s cmus update all AUR packages on the system, skipping view/confirm dialogs baph -uanN EOF fi exit 0 } msg() { # print colour $1 :: then message $2 in bold, usage: msg "color" "text" [[ $1 ]] && printf "%b::\e[0m \e[1m$2\e[0m\n" "$1" || printf "%s\n" "$2" } die() { # print string $1 and exit with error code $2, usage: die "text" exitcode printf "\e[1;31merror:\e[0m\t%s\n" "$1" && exit "${2:-1}" } get() { # install an AUR package.. usage: get "package" local pkg="$1" mkdir -p "$BUILDDIR" rm -rf "${BUILDDIR:?}/$pkg" cd "$BUILDDIR" || die "failed to change directory to build location" if hash git >/dev/null 2>&1; then msg '\e[34m' "Cloning \e[32m$pkg\e[0m\e[1m package repo..." git clone "$AUR/$pkg" || die "failed to clone package repo: $AUR/$pkg" else msg '\e[34m' "Retrieving package: $pkg" [[ -d "$BUILDDIR/$pkg" ]] && rm -rf "${BUILDDIR:?}/$pkg" [[ -e "$BUILDDIR/$pkg.tar.gz" ]] && rm -rf "$BUILDDIR/$pkg.tar.gz" if curl -LO -m 15 "$AUR/cgit/aur.git/snapshot/$pkg.tar.gz" && [[ -e "$BUILDDIR/$pkg.tar.gz" ]]; then tar -xvf "$pkg.tar.gz" || die "failed to extract package archive: $pkg.tar.gz" rm -rf "$BUILDDIR/$pkg.tar.gz" else die "failed to download requested package: $pkg" fi fi if [[ -r "$BUILDDIR/$pkg/PKGBUILD" ]] && cd "$BUILDDIR/$pkg"; then view "$BUILDDIR/$pkg/PKGBUILD" || yesno "Continue building $pkg" || { rm -rf "${BUILDDIR:?}/$pkg"; return 1;} buildp "$BUILDDIR/$pkg/PKGBUILD" || return 1 else die "$BUILDDIR/$pkg does not contain a PKGBUILD or it is not readable" fi return 0 } view() { # view PKGBUILD.. usage: view "/path/to/PKGBUILD" [[ -z $NOVIEW ]] && yesno "View/Edit the PKGBUILD for $pkg" 1 && { ${EDITOR:-vi} "$1"; return 1; } return 0 } keys() { # import PGP keys from package.. usage: keys ${KEYS[@]} for key; do if ! pacman-key --list-keys | grep -q "$key"; then msg '\e[33m' "Resolving missing pgp key for $pkg: $key" if ! gpg --receive-keys "$key" && sudo pacman-key -r "$key" && sudo pacman-key --lsign-key "$key"; then msg '\e[33m' "Failed to import pgp key, continuing anyway" fi fi done } deps() { # build package depends.. usage: deps ${DEPENDS[@]} for dep; do dep="$(sed 's/[=<>]=\?[0-9.\-]*.*//g' <<< "$dep")" if ! { pacman -Qsq "^$dep$" || pacman -Ssq "^$dep$"; } >/dev/null 2>&1; then msg '\e[33m' "Resolving \e[32m$pkg\e[0m\e[1m AUR dependency: $dep" get "$dep" || die "failed to build dependency $dep" fi done cd "$BUILDDIR/$pkg" || die "failed to cd $BUILDDIR/$pkg" } yesno() { # ask confirmation if NOCONF is not set, usage: yesno "question" [[ $NOCONF ]] && return 0 read -re -p $'\e[34m::\e[0m \e[1m'"$1"$'? [Y/n]\e[0m ' c [[ -z $c || $c == 'y' || $c == 'Y' ]] } query() { # return key value $1 from json/dict $2, usage: query "key" "input" # specifically for the response given when querying the AUR for a search awk -F'","' -v k="$1" '{ for (i=1; i <= NF; i++) { if ($i ~ k) print $i } }' <<< "$2" | sed 's/.*'"$1"'":"\?\(.*\),\?"\?.*/\1/g; s/[,"]$//g; s/[\\%]//g; s/null,".*$/null/' } buildp() { # build package.. usage: buildp "/path/to/PKGBUILD" # this function assumes that we're in the directory containing the PKGBUILD typeset -i in out # in or out of array typeset -a depends makedepends validpgpkeys # arrays typeset arrtext="" # read the PKGBUILD and grab the depends, makedepends, and validpgpkeys while read -r line; do [[ $line =~ ^[\ \ ]*# ]] && continue # skip comments # determine if were in and/or out an array (including single line arrays) case "$line" in depends=*|makedepends=*|validpgpkeys=*) in=1 [[ $line == *')'* ]] && out=1 # account for just a single line array ;; *')'*) (( in )) && out=1 ;; esac # if were in an array add/start the string (( in )) && { [[ $arrtext ]] && arrtext+=$'\n'"$line" || arrtext="$line"; } # if were now out of an array, reset both (( out )) && out=0 in=0 done < "$1" # better than evaluating the whole PKGBUILD but still sub-optimal, ideally # we get the 3 arrays filled with values we need to build the package eval "$arrtext" # keys (if any) (( ${#validpgpkeys[@]} > 0 )) && keys "${validpgpkeys[@]}" # dependencies (( ${#depends[@]} || ${#makedepends[@]} )) && deps "${depends[@]}" "${makedepends[@]}" # build and install it, upon success remove it makepkg -sicr && { rm -rf ./*.tar.xz >/dev/null 2>&1 || return 0; } } search() { # search query the AUR, usage: search "query" for q; do msg '\e[34m' "Searching the AUR for '$q'...\n" typeset res="$(curl -Lsm 10 "$AUR"'/rpc.php?type=search&arg='"$q")" if [[ -z $res || $res == *'"resultcount":0'* ]]; then printf "\e[1;31m:: \e[0mno results found\n" else typeset -i i=1 typeset -a pkgs=() while read -r key; do n=$(query "Name" "$key") v=$(query "Version" "$key") d=$(query "Description" "$key") (( ${#d} > ${COLUMNS:-$(tput cols)} )) && d=$(sed 's/\([\.,]\)/\1\\n /' <<< "$d") [[ $(query "OutOfDate" "$key") != null ]] && v+="\e[1;31m (Out of Date!)" printf "\e[1;33m%s\e[1;35m AUR/\e[1;37m%s \e[1;32m$v\n\e[0m $d\n" "$i" "$n" (( i++ )) pkgs+=("${n//[()]/}") done < <(sed 's/},{/\n/g' <<< "$res") if (( i > 1 )) && read -re -p $'\n\nEnter package number(s) to install: ' id && [[ $id =~ [0-9] ]]; then for num in $id; do case $num in ''|*[!0-9]*) : ;; *) AURPKGS+=("${pkgs[$((num - 1))]}") ;; esac done (( ! ${#AURPKGS[@]} )) || installp fi fi done } update() { # check updates for each package if (( ! ${#AURPKGS[@]} )); then mapfile -t AURPKGS < <(pacman -Qqm 2>/dev/null) [[ $AURONLY ]] || sudo pacman -Syyu $NOCONF fi if (( ${#AURPKGS[@]} )); then msg '\e[34m' "Synchronizing AUR package versions..." typeset -a needsupdate=() newv=() oldv=() latestver=() typeset installed="${AURPKGS[*]}" typeset -i i mapfile -t newv < <(curl -#L "$AUR/packages/{${installed// /,}}" | awk '/Details:/ {sub(/<\/h.?>/,""); print $4}') mapfile -t oldv < <(pacman -Q "${AURPKGS[@]}" | awk '{print $2}') for ((i=0; i < ${#AURPKGS[@]}; i++)); do if [[ ${newv[$i]} && ${oldv[$i]} && $(vercmp "${oldv[$i]}" "${newv[$i]}") -lt 0 ]]; then needsupdate+=("${AURPKGS[$i]}") latestver+=("${newv[$i]}") printf " %s \e[1m\e[31m%s \e[33m->\e[32m %s\e[0m\n" "${AURPKGS[$i]}" "${oldv[$i]}" "${newv[$i]}" fi done msg '\e[34m' "Starting AUR package upgrade..." if (( ${#needsupdate[@]} > 0 )); then printf "\n\e[1mPackages (%s)\e[0m %s\n\n" "${#needsupdate[@]}" "${needsupdate[*]}" for ((i=0; i < ${#needsupdate[@]}; i++)); do printf "%s" "${needsupdate[$i]}-${latestver[$i]}"; done yesno "Proceed with package upgrade" && for pkg in "${needsupdate[@]}"; do get "$pkg"; done else msg '' " there is nothing to do" fi else msg '\e[34m' "No AUR packages installed.." fi exit 0 } installp() { # loop over package array and install each if (( ${#AURPKGS[@]} || ${#PACPKGS[@]} )); then (( ! AURONLY && ${#PACPKGS[@]} )) && { sudo pacman -S "${PACPKGS[@]}" $NOCONF || exit 1; } for pkg in "${AURPKGS[@]}"; do if (( $(curl -sLI -m 10 "$AUR/packages/$pkg" | awk 'NR==1 {print $2}') == 200 )); then get "$pkg" || msg '\e[33m' "Exited $pkg build early" else die "$v response from $AUR/packages/$pkg"$'\n\ncheck the package name is spelled correctly' fi done else die "no targets specified" fi } trap 'echo; exit' SIGINT # catch ^C if (( ! UID )); then die "do not run baph as root" elif (( ! $# )); then die "no operation specified (use -h for help)" elif ! hash sudo curl >/dev/null 2>&1; then die "this requires to following packages: sudo, curl\n\n\toptional packages: git" else RUN='' ARGS='' for arg; do # shift long opts to short form case "$arg" in --version|--help|--search|--install|--update|--noview|--auronly|--noconfirm) [[ $arg == '--noconfirm' ]] && arg="${arg^^}" [[ $ARGS == *"${arg:2:1}"* ]] || ARGS+="${arg:1:2}" ;; --*) die "invalid option: '$arg'" ;; -*) [[ $ARGS == *"${arg:1:1}"* ]] || ARGS+="$arg " ;; *) [[ $ARGS == *"$arg"* ]] || ARGS+="$arg " ;; esac done eval set -- "$ARGS" while getopts ":hvuisanN" OPT; do case "$OPT" in h|v) use "-$OPT" ;; n|N|a|s|u|i) [[ $OPT =~ (s|u|i) && $RUN ]] && die "${DESC[$OPT]} and $RUN cannot be used together" eval "${OPTS[$OPT]}" ;; \?) die "invalid option: '$OPTARG'" ;; esac done shift $((OPTIND - 1)) if [[ $RUN == 'search' ]]; then (( $# > 0 )) || die "search requires a query" $RUN "$@" else for arg; do pacman -Ssq "^$arg$" >/dev/null 2>&1 && PACPKGS+=("$arg") || AURPKGS+=("$arg") done $RUN fi fi # vim:fdm=marker:fmr={,}