#!/usr/bin/env bash set -eo pipefail [[ $TRACE ]] && set -x && export TRACE=$TRACE version() { local VERSION="dev-master" if [[ -f /var/lib/aptfile/VERSION ]]; then VERSION=$(cat /var/lib/aptfile/VERSION) fi echo "aptfile $VERSION" } usage() { version echo "Usage: aptfile " } help() { usage echo echo " is the path to a bash file with apt instructions." echo echo " -h, --help Display this help message" echo " -v, --version Display the version number" echo echo " For more information, see https://github.com/seatgeek/bash-aptfile" echo } resolve_link() { $(type -p greadlink readlink | head -1) "$1" } abs_dirname() { local cwd local path="$1" cwd="$(pwd)" while [ -n "$path" ]; do cd "${path%/*}" local name="${path##*/}" path="$(resolve_link "$name" || true)" done pwd cd "$cwd" } expand_path() { { cd "$(dirname "$1")" 2>/dev/null local dirname="$PWD" cd "$OLDPWD" echo "$dirname/$(basename "$1")" } || echo "$1" } options=() arguments=() for arg in "$@"; do if [ "${arg:0:1}" = "-" ]; then if [ "${arg:1:1}" = "-" ]; then options[${#options[*]}]="${arg:2}" else index=1 while option="${arg:$index:1}"; do [ -n "$option" ] || break options[${#options[*]}]="$option" let index+=1 done fi else arguments[${#arguments[*]}]="$arg" fi done for option in "${options[@]}"; do case "$option" in "h" | "help") help exit 0 ;; "v" | "version") version exit 0 ;; *) usage >&2 exit 1 ;; esac done if [ "${#arguments[@]}" -eq 0 ]; then APTFILE_PATH=$(expand_path "aptfile") if [[ ! -f $APTFILE_PATH ]]; then usage >&2 exit 1 fi set -- "aptfile" "$@" fi export APTFILE_COLOR_OFF="\033[0m" # unsets color to term fg color export APTFILE_RED="\033[0;31m" # red export APTFILE_GREEN="\033[0;32m" # green export APTFILE_YELLOW="\033[0;33m" # yellow export APTFILE_MAGENTA="\033[0;35m" # magenta export APTFILE_CYAN="\033[0;36m" # cyan logfile=$(basename "$0") TMP_APTFILE_LOGFILE=$(mktemp "/tmp/${logfile}.XXXXXX") || { log_fail "${APTFILE_RED}WARNING: Cannot create temp file using mktemp in /tmp dir ${APTFILE_COLOR_OFF}\n" } export TMP_APTFILE_LOGFILE="$TMP_APTFILE_LOGFILE" trap 'rm -rf "$TMP_APTFILE_LOGFILE" > /dev/null' INT TERM EXIT log_fail() { [[ $TRACE ]] && set -x echo -e "${APTFILE_RED}$*${APTFILE_COLOR_OFF}" 1>&2 [[ -f "$TMP_APTFILE_LOGFILE" ]] && echo -e "verbose logs:\n" 1>&2 && sed -e 's/^/ /' "$TMP_APTFILE_LOGFILE" exit 1 } log_info() { [[ $TRACE ]] && set -x echo -e "$@" } update() { [[ $TRACE ]] && set -x log_info "Running update" apt-get update >"$TMP_APTFILE_LOGFILE" 2>&1 [[ $? -eq 0 ]] || log_fail "Failed to run update" } package() { [[ $TRACE ]] && set -x [[ -z $1 ]] && log_fail "Please specify a package to install" local pkg="$1" dpkg --force-confnew -s "$pkg" >"$TMP_APTFILE_LOGFILE" 2>&1 && log_info "${APTFILE_CYAN}[OK]${APTFILE_COLOR_OFF} package $pkg" && return 0 apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" -qq -y install "$pkg" [[ $? -eq 0 ]] || log_fail "${APTFILE_RED}[FAIL]${APTFILE_COLOR_OFF} package $pkg" log_info "${APTFILE_GREEN}[NEW]${APTFILE_COLOR_OFF} package $pkg" } package_from_url() { [[ $TRACE ]] && set -x [[ -z $2 ]] && log_fail "Please specify a name and a download url to install the package from" local name=$1 local url=$2 if type curl >/dev/null 2>&1; then local dl_cmd="curl" local dl_options="-so" elif type wget >/dev/null 2>&1; then local dl_cmd="wget" local dl_options="-qO" else log_fail "Neither curl nor wget found. Unable to download $url" fi dpkg --force-confnew -s "$name" >"$TMP_APTFILE_LOGFILE" 2>&1 && log_info "${APTFILE_CYAN}[OK]${APTFILE_COLOR_OFF} package $name" && return 0 tempdir=$(mktemp -d) $dl_cmd $dl_options $tempdir/${name}.deb $url \ && apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" -qq -y install "$tempdir/${name}.deb" if [[ $? -ne 0 ]]; then rm -r $tempdir log_fail "${APTFILE_RED}[FAIL]${APTFILE_COLOR_OFF} package $name" fi rm -r $tempdir log_info "${APTFILE_GREEN}[NEW]${APTFILE_COLOR_OFF} package $name" } packagelist() { [[ $TRACE ]] && set -x [[ -z $1 ]] && log_fail "Please specify at least one package to install" local input_packages=$@ local install_packages=() for pkg in $input_packages; do dpkg --force-confnew -s "$pkg" >"$TMP_APTFILE_LOGFILE" 2>&1 && log_info "${APTFILE_CYAN}[OK]${APTFILE_COLOR_OFF} package $pkg" && continue install_packages+=($pkg) done if [[ ${#install_packages[@]} -gt 0 ]]; then apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" -qq -y install ${install_packages[@]} [[ $? -eq 0 ]] || log_fail "${APTFILE_RED}[FAIL]${APTFILE_COLOR_OFF} packages ${install_packages[@]}" log_info "${APTFILE_GREEN}[NEW]${APTFILE_COLOR_OFF} packages ${install_packages[@]}" fi } ppa() { [[ $TRACE ]] && set -x [[ -z $1 ]] && log_fail "Please specify a repository to setup" local repo="$1" if [[ -d /etc/apt/sources.list.d/ ]]; then grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep -q "$repo" && log_info "${APTFILE_CYAN}[OK]${APTFILE_COLOR_OFF} ppa $repo" && return 0 fi repository "ppa:$1" } repository() { [[ $TRACE ]] && set -x [[ -z $1 ]] && log_fail "Please specify a repository to setup" local repo="$1" if [[ -d /etc/apt/sources.list.d/ ]]; then grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep -Fq "$repo" && log_info "${APTFILE_CYAN}[OK]${APTFILE_COLOR_OFF} repository $repo" && return 0 fi add-apt-repository -y "$repo" >"$TMP_APTFILE_LOGFILE" 2>&1 [[ $? -eq 0 ]] || log_fail "${APTFILE_RED}[FAIL]${APTFILE_COLOR_OFF} repository $pkg" update log_info "${APTFILE_GREEN}[NEW]${APTFILE_COLOR_OFF} repository $repo" } repository_file() { [[ $TRACE ]] && set -x [[ -z $2 ]] && log_fail "Please specify a filename and sourceline to setup" local repofile="$1" local repo="$2" # sourceline is not a complete repo configuration, needs modifying # i.e. not sourceline="deb http://domain.invalid/debian buster main extra" if [[ "$repo" != "deb "* ]]; then releasename=$(lsb_release -sc) if [[ "$repo" == *" "* ]]; then # Components given in sourceline, adding suite # i.e. sourceline="http://domain.invalid/debian main" repo="deb ${repo/ / $releasename }" else # only URL given, adding suite and component # i.e. sourceline="http://domain.invalid/debian" repo="deb ${repo} $releasename main" fi fi if [[ "$repofile" != *.list ]]; then # Adding extension to enable parsing file repofile=${repofile}.list fi # Adding path repofile="/etc/apt/sources.list.d/$repofile" [[ -d "/etc/apt/sources.list.d" ]] || mkdir -p /etc/apt/sources.list.d grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep -Fq "$repo" && log_info "${APTFILE_CYAN}[OK]${APTFILE_COLOR_OFF} repository $repo" && return 0 echo "Writing '$repo' to file '$repofile'" >"$TMP_APTFILE_LOGFILE" echo "$repo" >"$repofile" 2>>"$TMP_APTFILE_LOGFILE" [[ $? -eq 0 ]] || log_fail "${APTFILE_RED}[FAIL]${APTFILE_COLOR_OFF} repository $pkg" update log_info "${APTFILE_GREEN}[NEW]${APTFILE_COLOR_OFF} repository $repo" } debconf_selection() { [[ $TRACE ]] && set -x [[ -z $1 ]] && log_fail "Please specify a debconf line" echo "$1" | debconf-set-selections [[ $? -eq 0 ]] || log_fail "${APTFILE_RED}[FAIL]${APTFILE_COLOR_OFF} debconf: $1" log_info "${APTFILE_CYAN}[OK]${APTFILE_COLOR_OFF} set debconf line: $1" } if [ -z "$TMPDIR" ]; then APTFILE_TMPDIR="/tmp" else APTFILE_TMPDIR="${TMPDIR%/}" fi APTFILE_INPUT=$(expand_path "$1") APTFILE_TMPNAME="$APTFILE_TMPDIR/aptfile.$$" APTFILE_OUTPUT="${APTFILE_TMPNAME}.out" aptfile_preprocess_source() { tail -n +2 "$1" >"$APTFILE_OUTPUT" trap "aptfile_cleanup_preprocessed_source" err exit trap "aptfile_cleanup_preprocessed_source; exit 1" int } aptfile_cleanup_preprocessed_source() { rm -f "$APTFILE_TMPNAME" rm -f "$APTFILE_OUTPUT" } aptfile_preprocess_source "$APTFILE_INPUT" export -f update export -f package export -f package_from_url export -f packagelist export -f ppa export -f repository export -f repository_file export -f debconf_selection export -f log_fail export -f log_info chmod +x "$APTFILE_OUTPUT" exec "$APTFILE_OUTPUT"