#!/usr/bin/env bash # shellcheck disable=SC2076 # We often use =~ to match strings rather than a regex. # shellcheck disable=SC2155 # The exit codes aren't being used in these instances anyway. LC_ALL=C PACKAGE_INSTALLATION_TRIES=0 PACKAGE_INSTALLATION_COUNT=0 unset QUIET readonly VERSION="0.4.7" # set a github auth token (e.g a PAT ) in DEBGET_TOKEN to get a bigger rate limit if [ -n "${DEBGET_TOKEN}" ]; then export HEADERAUTH="Authorization: token ${DEBGET_TOKEN}" export HEADERPARAM="--header" else unset HEADERAUTH unset HEADERPARAM fi function usage() { cat < | install | reinstall | remove [--remove-repo] | purge [--remove-repo] | search [--include-unsupported] | cache | clean | list [--include-unsupported] [--raw|--installed|--not-installed] | prettylist [] | csvlist [] | fix-installed [--old-apps] | help | version} deb-get provides a high-level commandline interface for the package management system to easily install and update packages published in 3rd party apt repositories or via direct download. update update is used to resynchronize the package index files from their sources. When --repos-only is provided, only initialize and update deb-get's external repositories, without updating apt or looking for updates of installed packages. When --quiet is provided the fetching of deb-get repository updates is done without progress feedback. upgrade upgrade is used to install the newest versions of all packages currently installed on the system. When --dg-only is provided, only the packages which have been installed by deb-get will be upgraded. install install is followed by one package (or a space-separated list of packages) desired for installation or upgrading. reinstall reinstall is followed by one package (or a space-separated list of packages) desired for reinstallation. remove remove is identical to install except that packages are removed instead of installed. When --remove-repo is provided, also remove the apt repository of apt/ppa packages. purge purge is identical to remove except that packages are removed and purged (any configuration files are deleted too). When --remove-repo is provided, also remove the apt repository of apt/ppa packages. clean clean clears out the local repository (/var/cache/deb-get) of retrieved package files. search search for the given regex(7) term(s) from the list of available packages supported by deb-get and display matches. When --include-unsupported is provided, include packages with unsupported architecture or upstream codename and include PPAs for Debian-derived distributions. show show information about the given package (or a space-separated list of packages) including their install source and update mechanism. list list the packages available via deb-get. When no option is provided, list all supported packages and tell which ones are installed (slower). When --include-unsupported is provided, include packages with unsupported architecture or upstream codename and include PPAs for Debian-derived distributions (faster). When --raw is provided, list all packages and do not tell which ones are installed (faster). When --installed is provided, only list the packages installed (faster). When --not-installed is provided, only list the packages not installed (faster). prettylist markdown formatted list the packages available in repo. repo defaults to 01-main. If repo is 00-builtin or 01-main the packages from 00-builtin are included. Use this to update README.md. csvlist csv formatted list the packages available in repo. repo defaults to 01-main. If repo is 00-builtin or 01-main the packages from 00-builtin are included. Use this with 3rd party wrappers. cache list the contents of the deb-get cache (/var/cache/deb-get). fix-installed fix installed packages whose definitions were changed. When --old-apps is provided, transition packages to new format. This command is only intended for internal use. help show this help. version show deb-get version. HELP } function package_is_installed() { [[ " ${INSTALLED_APPS[*]} " =~ " ${1} " ]] } function elevate_privs() { if [ "$(id -ru)" -eq 0 ]; then # Already in the root context ELEVATE="" elif command -v doas 1>/dev/null && doas true 2>/dev/null; then ELEVATE="doas" elif command -v sudo 1>/dev/null && sudo true 2>/dev/null; then ELEVATE="sudo" else fancy_message fatal "$(basename "${0}") requires sudo or doas to elevate permissions, neither were found." fi } function create_cache_dir() { if [ -d /var/cache/get-deb ]; then ${ELEVATE} mv /var/cache/get-deb "${CACHE_DIR}" fi ${ELEVATE} mkdir -p "${CACHE_DIR}" 2>/dev/null ${ELEVATE} chmod 755 "${CACHE_DIR}" 2>/dev/null } function create_etc_dir() { ${ELEVATE} mkdir -p "${ETC_DIR}" 2>/dev/null ${ELEVATE} chmod 755 "${ETC_DIR}" 2>/dev/null } function unroll_url() { # Sourceforge started adding parameters local TRIM_URL="$(curl -w "%{url_effective}" -I -L -s -S "${1}" -o /dev/null)" #we will only check for the meta refresh if the initial unroll didn't accomplish anything if [[ "${1%/}" == "${TRIM_URL%/}" ]]; then local META_REFRESH="$(curl -s "${1}" | grep -E -i -o -m 1 "]*refresh[^\>]*>" | grep -i -E "content=.*url=")" if [[ -n "${META_REFRESH}" ]]; then META_REFRESH="$(sed -E 's|.*url=['\''"]?([^'\''">]*).*|\1|i' <<< "${META_REFRESH}")" if [[ ${META_REFRESH} =~ ^https?://.* ]]; then TRIM_URL="${META_REFRESH}" else TRIM_URL="$(sed -E 's|(https?://[^/]*).*|\1|' <<< ${1})/${META_REFRESH#\/}" fi fi fi printf '%s' "${TRIM_URL/\.deb*/.deb}" } function follow_url() { local TRIM_URL="$(curl -w "%{url_effective}" -I -L -s -S "${1}" -o /dev/null)" printf '%s' "${TRIM_URL}" } function get_github_releases() { METHOD="github" CACHE_FILE="${CACHE_DIR}/${APP}.json_extract" # Cache github releases json for 1 hour to try and prevent API rate limits # https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting # {"message":"API rate limit exceeded for 62.31.16.154. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)","documentation_url":"https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting"} # curl -I https://api.github.com/users/flexiondotorg # Do not process github releases while generating a pretty list or upgrading if [[ ' install fix-installed ' =~ " ${ACTION} " ]] || { [[ ' update ' =~ " ${ACTION} " ]] && package_is_installed "${APP}"; }; then if [ ! -e "${CACHE_FILE}" ] || [ -n "$(find "${CACHE_FILE}" -mmin +"${DEBGET_CACHE_RTN:-60}")" ]; then if [ -z ${QUIET} ]; then fancy_message info "Updating ${CACHE_FILE}" fi local URL="https://api.github.com/repos/${1}/releases${2:+/$2}" wgetcmdarray=(wget "${HEADERPARAM}" "${HEADERAUTH}" -q --no-use-server-timestamps "${URL}" -O- ) "${wgetcmdarray[@]}" | jq . | ${ELEVATE} tee "${CACHE_FILE}" > /dev/null || ( fancy_message warn "Updating ${CACHE_FILE} failed." ) if [ -f "${CACHE_FILE}" ] && grep "API rate limit exceeded" "${CACHE_FILE}"; then fancy_message warn "Updating ${CACHE_FILE} exceeded GitHub API limits. Deleting it." ${ELEVATE} rm "${CACHE_FILE}" 2>/dev/null fi if [[ " $* " != *' --no-filter '* ]]; then ${ELEVATE} sed -i "/browser_download/!d;/\.deb/!d" ${CACHE_FILE} fi fi fi } function get_gitlab_releases() { METHOD="gitlab" CACHE_FILE="${CACHE_DIR}/${APP}.json_extract" # Cache gitlab releases json for 1 hour to try and prevent API rate limits # # Do not process gitlab releases while generating a pretty list or upgrading # if $1 is org/app or user/repo it must be urlencoded # the gitlab api can take a release or use permalink/latest to get the latest release # So for gitlab sourced apps that use this release model users can use 99-local to pin a version # if [[ ' install fix-installed ' =~ " ${ACTION} " ]] || { [[ ' update ' =~ " ${ACTION} " ]] && package_is_installed "${APP}"; }; then if [ ! -e "${CACHE_FILE}" ] || [ -n "$(find "${CACHE_FILE}" -mmin +"${DEBGET_CACHE_RTN:-60}")" ]; then if [ -z ${QUIET} ]; then fancy_message info "Updating ${CACHE_FILE}" fi RELEASE=${2/%latest/permalink/latest} if [[ ${1} =~ ^https?://.* ]]; then local GITLAB_HOST="$(sed -E 's|(https?://[^/]*).*|\1|' <<< ${1})" local GITLAB_PROJECT="$(sed -E 's|https?://[^/]*/||' <<< ${1})" else local GITLAB_HOST="https://gitlab.com" local GITLAB_PROJECT="${1}" fi if [[ -n "${2}" ]]; then local URL="${GITLAB_HOST}/api/v4/projects/${GITLAB_PROJECT//\//%2F}/releases/${RELEASE}/assets/links" else local URL="${GITLAB_HOST}/api/v4/projects/${GITLAB_PROJECT//\//%2F}/releases" fi wgetcmdarray=(wget -q --no-use-server-timestamps "${URL}" -O- ) # Only grab the .deb files URLs, but first add newlines, because the JSON is all on one line. # This will make it easier for packages to use sed or grep, instead of needing to use jq. "${wgetcmdarray[@]}" | sed -E 's/(\"direct_asset_url\")/\n\1/g; s/(\",)/\1\n/g' | sed -E '/direct_asset_url/!d;/\.deb/!d; s/.*(http.*\.deb).*/\1/' | ${ELEVATE} tee "${CACHE_FILE}" > /dev/null || ( fancy_message warn "Updating ${CACHE_FILE} failed." ) fi fi } function get_website() { METHOD="website" CACHE_FILE="${CACHE_DIR}/${APP}.html" if [[ ' install fix-installed ' =~ " ${ACTION} " ]] || { [[ ' update ' =~ " ${ACTION} " ]] && package_is_installed "${APP}"; }; then if [ ! -e "${CACHE_FILE}" ] || [ -n "$(find "${CACHE_FILE}" -mmin +"${DEBGET_CACHE_RTN:-60}")" ]; then if [ -z ${QUIET} ]; then fancy_message info "Updating ${CACHE_FILE}" fi # shellcheck disable=SC2086 if ! ${ELEVATE} wget ${WGET_VERBOSITY} --no-use-server-timestamps ${WGET_TIMEOUT} "$@" -O "${CACHE_FILE}"; then fancy_message warn "Updating ${CACHE_FILE} failed. Deleting it." ${ELEVATE} rm -f "${CACHE_FILE}" fi fi fi } function fancy_message() { if [ -z "${1}" ] || [ -z "${2}" ]; then return fi local RED="\e[31m" GREEN="\e[32m" YELLOW="\e[33m" MAGENTA="\e[35m" RESET="\e[0m" MESSAGE_TYPE="${1}" MESSAGE="${2}" case "${MESSAGE_TYPE}" in (info) printf " [${GREEN}+${RESET}] %s\n" "${MESSAGE}";; (progress) printf " [${GREEN}+${RESET}] %s" "${MESSAGE}";; (recommend) printf " [${MAGENTA}!${RESET}] %s\n" "${MESSAGE}";; (warn) printf " [${YELLOW}*${RESET}] WARNING! %s\n" "${MESSAGE}";; (error) printf " [${RED}!${RESET}] ERROR! %s\n" "${MESSAGE}" >&2;; (fatal) printf " [${RED}!${RESET}] ERROR! %s\n" "${MESSAGE}" >&2 exit 1;; (*) printf " [?] UNKNOWN: %s\n" "${MESSAGE}";; esac } function download_deb() { local URL="${1}" local FILE="${2}" # shellcheck disable=SC2086 if ! ${ELEVATE} wget ${WGET_VERBOSITY} --continue ${WGET_TIMEOUT} --show-progress --progress=bar:force:noscroll "${URL}" -O "${CACHE_DIR}/${FILE}"; then fancy_message error "Failed to download ${URL}. Deleting ${CACHE_DIR}/${FILE}..." ${ELEVATE} rm "${CACHE_DIR}/${FILE}" 2>/dev/null return 1 fi if declare -F post_download >/dev/null; then post_download return $? fi } function eula() { if [ -n "${EULA}" ] && [ "${DEBIAN_FRONTEND}" != noninteractive ]; then echo -e "${EULA}\n\n" printf '%s\n' "Do you agree to the ${APP} EULA?" select yn in "Yes" "No"; do case "$yn" in Yes) return 0;; No) return 1;; esac done fi } function update_apt() { if ! [ "$DISABLE_APT" == "y" ]; then ${ELEVATE} apt-get -q${QUIET} -o Dpkg::Progress-Fancy="1" -y update fi } function upgrade_apt() { if ! [ "$DISABLE_APT" == "y" ]; then ${ELEVATE} apt-get -q -o Dpkg::Progress-Fancy="1" -y upgrade fi } function upgrade_only_dg() { mapfile -t INSTALLED_APT_PPA < <(grep -P -o "\S+(?=\s\d+\s(apt\s*$|ppa\s*)$)" "${ETC_DIR}/installed") printf '%s\0' "${INSTALLED_APT_PPA[@]}" | xargs -0 ${ELEVATE} apt-get -q -o Dpkg::Progress-Fancy="1" -y install --only-upgrade } # Update only the added repo (during install action) function update_only_repo() { fancy_message info "Updating: /etc/apt/sources.list.d/${APT_LIST_NAME}.list" ${ELEVATE} apt-get update -o Dir::Etc::sourcelist="sources.list.d/${APT_LIST_NAME}.list" \ -o Dir::Etc::sourceparts="-" -o APT::Get::List-Cleanup="0" } function install_apt() { ((PACKAGE_INSTALLATION_TRIES++)) add_apt_repo if ! update_only_repo; then remove_repo --remove-repo --quiet return fi if ! package_is_installed "${APP}"; then if ! eula; then remove_repo --remove-repo --quiet return fi if ! ${ELEVATE} apt-get -q=2 -o Dpkg::Progress-Fancy="1" -y install "${APP}"; then remove_repo --remove-repo --quiet return fi add_installed maint_supported_cache else if [ "${ACTION}" == "reinstall" ]; then if ! ${ELEVATE} apt-get -q=2 -o Dpkg::Progress-Fancy="1" -y --reinstall --allow-downgrades install "${APP}"; then return fi else fancy_message info "${APP} is up to date." fi fi ((PACKAGE_INSTALLATION_COUNT++)) } function install_ppa() { ppa_to_apt install_apt } function install_deb() { local URL="${1}" local FILE="${FILE:-${URL##*/}}" local STATUS="" ((PACKAGE_INSTALLATION_TRIES++)) if ! package_is_installed "${APP}"; then if ! eula; then return fi if ! download_deb "${URL}" "${FILE}"; then return fi if ! ${ELEVATE} apt-get -q=2 -o Dpkg::Progress-Fancy="1" -y install "${CACHE_DIR}/${FILE}"; then return fi add_installed maint_supported_cache else if [ "${ACTION}" == "reinstall" ]; then if ! download_deb "${URL}" "${FILE}"; then return fi if ! ${ELEVATE} apt-get -q=2 -o Dpkg::Progress-Fancy="1" -y --reinstall --allow-downgrades install "${CACHE_DIR}/${FILE}"; then return fi elif dpkg --compare-versions "${VERSION_PUBLISHED}" gt "${VERSION_INSTALLED}"; then if ! download_deb "${URL}" "${FILE}"; then return fi if ! ${ELEVATE} apt-get -q=2 -o Dpkg::Progress-Fancy="1" -y install "${CACHE_DIR}/${FILE}"; then return fi elif [ -z "${FILE}" ]; then fancy_message warn "${APP} update check failed, moving on to next package." else fancy_message info "${FILE} is up to date." fi fi ((PACKAGE_INSTALLATION_COUNT++)) if [ -f "${CACHE_DIR}/${FILE}" ]; then ${ELEVATE} rm "${CACHE_DIR}/${FILE}" 2>/dev/null fi } function remove_deb() { local APP="${1}" local REMOVE="${2:-remove}" local FILE="${FILE:-${URL##*/}}" local STATUS="" if package_is_installed "${APP}" || [[ " ${DEPRECATED_INSTALLED[*]} " =~ " ${APP} " ]]; then STATUS=$(dpkg-query -Wf '${Status}' "${APP}") if [ "${STATUS}" == "deinstall ok config-files" ]; then REMOVE="purge" fi ${ELEVATE} apt-get -q -y --autoremove "${REMOVE}" "${APP}" remove_installed "${APP}" maint_supported_cache else fancy_message info "${APP} is not installed." fi # Remove repos/PPA/key even if the app is not installed. case "${METHOD}" in direct|github|gitlab|website) if [ -f "${CACHE_DIR}/${FILE}" ]; then fancy_message info "Removing ${CACHE_DIR}/${FILE}" ${ELEVATE} rm "${CACHE_DIR}/${FILE}" 2>/dev/null fi ;; apt|ppa) remove_repo "${3}";; esac } function version_deb() { if package_is_installed "${APP}"; then dpkg-query -Wf '${Version}' "${APP}" 2> /dev/null # else empty output fi } function info_deb() { local INSTALLED="${VERSION_INSTALLED:-No}" case "${METHOD}" in (direct|github|gitlab|website) printf '%s\n' "${PRETTY_NAME}" printf ' %s:\t%s\n' Package "${APP}" Repository "${APP_SRC}" Updater deb-get Installed "${INSTALLED}" Published "${VERSION_PUBLISHED}" Architecture "${ARCHS_SUPPORTED}" Download "${URL}" Website "${WEBSITE}" Summary "${SUMMARY}";; (apt) printf '%s\n' "${PRETTY_NAME}" printf ' %s:\t%s\n' Package "${APP}" Repository "${APP_SRC}" Updater apt Installed "${INSTALLED}" Architecture "${ARCHS_SUPPORTED}" Repository "${APT_REPO_URL}" Website "${WEBSITE}" Summary "${SUMMARY}";; (ppa) printf '%s\n' "${PRETTY_NAME}" printf ' %s:\t%s\n' Package "${APP}" Repository "${APP_SRC}" Updater apt Installed "${INSTALLED}" Architecture "${ARCHS_SUPPORTED}" Launchpad "${PPA}" Website "${WEBSITE}" Summary "${SUMMARY}";; esac } function validate_deb() { local FULL_APP="${1}" export APP_SRC=${FULL_APP%/*} APP=${FULL_APP##*/} export DEFVER="" export ASC_KEY_URL="" GPG_KEY_URL="" GPG_KEY_ID="" export APT_LIST_NAME="${APP}" APT_REPO_URL="" APT_REPO_OPTIONS="" PPA="" export ARCHS_SUPPORTED="amd64" CODENAMES_SUPPORTED="" export METHOD="direct" export EULA="" export VERSION_INSTALLED="" VERSION_PUBLISHED="" export PRETTY_NAME="" SUMMARY="" WEBSITE="" export URL="" export CACHE_FILE="" FILE="" unset -f post_download 2>/dev/null # Source the variables if [ "${APP_SRC}" == "00-builtin" ]; then deb_"${APP}" 2>/dev/null else # shellcheck source=/dev/null . "${ETC_DIR}/${APP_SRC}.d/${APP}" 2>/dev/null fi if [[ " ${ARCHS_SUPPORTED} " =~ (" ${HOST_ARCH} "|" all ") ]] && { [ -z "${CODENAMES_SUPPORTED}" ] || [[ " ${CODENAMES_SUPPORTED} " =~ " ${UPSTREAM_CODENAME} " ]]; } && { [ "${METHOD}" != ppa ] || [ "${UPSTREAM_ID}" == ubuntu ]; }; then if [ -z "${DEFVER}" ] || [ -z "${PRETTY_NAME}" ] || [ -z "${SUMMARY}" ] || [ -z "${WEBSITE}" ]; then fancy_message error "Missing required information of package ${APP}:" printf >&2 '%s\n' "DEFVER=${DEFVER}" "PRETTY_NAME=${PRETTY_NAME}" "SUMMARY=${SUMMARY}" "WEBSITE=${WEBSITE}" return 1 fi VERSION_INSTALLED=$(version_deb) if [ -n "${APT_REPO_URL}" ]; then METHOD="apt" if [ "${ACTION}" != "prettylist" ]; then if [ -z "${ASC_KEY_URL}" ] && [ -z "${GPG_KEY_URL}" ] && [ -z "${GPG_KEY_ID}" ]; then fancy_message error "Missing required information of apt package ${APP}:" printf >&2 '%s\n' "ASC_KEY_URL=${ASC_KEY_URL}" "GPG_KEY_URL=${GPG_KEY_URL}" "GPG_KEY_ID=${GPG_KEY_ID}" return 1 fi if [ -n "${ASC_KEY_URL}" ] && [ -n "${GPG_KEY_URL}" ]; then fancy_message error "Conflicting repository key types for apt package ${APP}:" printf >&2 '%s\n' "ASC_KEY_URL=${ASC_KEY_URL}" "GPG_KEY_URL=${GPG_KEY_URL}" "GPG_KEY_ID=${GPG_KEY_ID}" return 1 fi if [ -n "${GPG_KEY_URL}" ] && [ -n "${GPG_KEY_ID}" ]; then fancy_message error "Conflicting repository key types for apt package ${APP}:" printf >&2 '%s\n' "ASC_KEY_URL=${ASC_KEY_URL}" "GPG_KEY_URL=${GPG_KEY_URL}" "GPG_KEY_ID=${GPG_KEY_ID}" return 1 fi if [ -n "${ASC_KEY_URL}" ] && [ -n "${GPG_KEY_ID}" ]; then fancy_message error "Conflicting repository key types for apt package ${APP}:" printf >&2 '%s\n' "ASC_KEY_URL=${ASC_KEY_URL}" "GPG_KEY_URL=${GPG_KEY_URL}" "GPG_KEY_ID=${GPG_KEY_ID}" return 1 fi fi elif [ -n "${PPA}" ]; then METHOD="ppa" else # If the method is github and the cache file is empty, ignore the package # The GitHub API is rate limit has likely been reached if [ "${METHOD}" == github ] && [ ! -s "${CACHE_FILE}" ]; then if ! [[ ' prettylist remove purge ' =~ " ${ACTION} " ]]; then fancy_message warn "Cached file ${CACHE_FILE} is empty or missing." ${ELEVATE} rm "${CACHE_FILE}" 2>/dev/null fi fi if { { [[ ' github website ' =~ " ${METHOD} " ]] && [ -s "${CACHE_FILE}" ]; } || [ "${METHOD}" == direct ]; } && ! [[ ' prettylist remove purge ' =~ " ${ACTION} " ]] && { [ -z "${URL}" ] || [ -z "${VERSION_PUBLISHED}" ]; }; then fancy_message error "Missing required information of ${METHOD} package ${APP}:" printf >&2 '%s\n' "URL=${URL}" "VERSION_PUBLISHED=${VERSION_PUBLISHED}" return 1 fi fi elif [ -n "${PPA}" ]; then METHOD="ppa" else # If the method is github and the cache file is empty, ignore the package # The GitHub API is rate limit has likely been reached if [ "${METHOD}" == github ] && [ ! -s "${CACHE_FILE}" ]; then if ! [[ ' prettylist remove purge ' =~ " ${ACTION} " ]]; then fancy_message warn "Cached file ${CACHE_FILE} is empty or missing." ${ELEVATE} rm "${CACHE_FILE}" 2>/dev/null fi fi if { { [[ ' github website gitlab ' =~ " ${METHOD} " ]] && [ -s "${CACHE_FILE}" ]; } || [ "${METHOD}" == direct ]; } && ! [[ ' prettylist remove purge ' =~ " ${ACTION} " ]] && { [ -z "${URL}" ] || [ -z "${VERSION_PUBLISHED}" ]; } && { [ -z "${ARCHS_SUPPORTED}" ] || [[ " ${ARCHS_SUPPORTED} " =~ (" ${HOST_ARCH} "|" all ") ]]; } && { [ -z "${CODENAMES_SUPPORTED}" ] || [[ " ${CODENAMES_SUPPORTED} " =~ " ${UPSTREAM_CODENAME} " ]]; }; then fancy_message error "Missing required information of ${METHOD} package ${APP}:" printf >&2 '%s\n' "URL=${URL}" "VERSION_PUBLISHED=${VERSION_PUBLISHED}" return 1 fi fi return 0 } function list_debs() { if [ "${1}" == --include-unsupported ]; then case "${2}" in (--raw) printf '%s\n' "${APPS[@]##*/}" ;; (--installed) printf '%s\n' "${INSTALLED_APPS[@]}" ;; (--not-installed) printf '%s\n' "${APPS[@]##*/}" | comm -23 - <(printf '%s\n' "${INSTALLED_APPS[@]}") ;; (*) local PAD=' ' for FULL_APP in "${APPS[@]}"; do local APP=${FULL_APP##*/} if package_is_installed "${APP}"; then printf '%s %s [ installed ]\n' "${APP}" "${PAD:${#APP}}" else printf '%s\n' "${APP}" fi done esac else if [ -f "${CACHE_DIR}"/supported.list ] ; then case "${2}" in (--raw) list_debs --include-unsupported --raw | comm --nocheck-order -12 ${CACHE_DIR}/supported_apps.list - ;; (--installed) # these don't have the [installed] tag so need a similar file to join list_debs --include-unsupported --installed | comm --nocheck-order -12 ${CACHE_DIR}/supported_apps.list - ;; (--not-installed) list_debs --include-unsupported --not-installed | comm --nocheck-order -12 ${CACHE_DIR}/supported_apps.list - ;; (--only-unsupported) list_debs --include-unsupported --raw | comm --nocheck-order -13 ${CACHE_DIR}/supported_apps.list - ;; (*) # this has [ installed ] tags list_debs --include-unsupported | comm --nocheck-order -12 ${CACHE_DIR}/supported.list - ;; esac else #elevate_privs # because we need to update the cache files this one slow time for FULL_APP in "${APPS[@]}"; do if validate_deb "${FULL_APP}"; then if [[ " ${ARCHS_SUPPORTED} " =~ (" ${HOST_ARCH} "|" all ") ]] && { [ -z "${CODENAMES_SUPPORTED}" ] || [[ " ${CODENAMES_SUPPORTED} " =~ " ${UPSTREAM_CODENAME} " ]]; } && { [ "${METHOD}" != ppa ] || [ "${UPSTREAM_ID}" == ubuntu ]; }; then case "${2}" in (--raw) printf '%s\n' "${APP}" ;; (--installed) if package_is_installed "${APP}"; then printf '%s\n' "${APP}" fi ;; (--not-installed) if ! package_is_installed "${APP}"; then printf '%s\n' "${APP}" fi ;; (*) if package_is_installed "${APP}"; then local PAD=' ' printf "%s %s [ installed ]\n" "${APP}" "${PAD:${#APP}}" else printf '%s\n' "${APP}" fi ;; esac fi fi done fi fi } function prettylist_debs() { local REPO="${1:-01-main}" local ICON="" cat <<"EOMSG" | Source | Package Name | Description | | :------: | :------------- | :------------ | EOMSG for FULL_APP in "${APPS[@]}"; do validate_deb "${FULL_APP}" if [ "${APP_SRC}" == "${REPO}" ] || { [ "${REPO}" == "01-main" ] && [ "${APP_SRC}" == "00-builtin" ]; }; then case "${METHOD}" in apt) ICON="debian.png";; github) ICON="github.png";; gitlab) ICON="gitlab.png";; ppa) ICON="launchpad.png";; *) ICON="direct.png";; esac echo "| [](${WEBSITE}) | "'`'"${APP}"'`'" | ${SUMMARY} |" fi done } function csvlist_debs() { local REPO="${1:-01-main}" for FULL_APP in "${APPS[@]}"; do if validate_deb "${FULL_APP}"; then if [ "${APP_SRC}" == "${REPO}" ] || { [ "${REPO}" == "01-main" ] && [ "${APP_SRC}" == "00-builtin" ]; }; then echo "\"${APP}\",\"${PRETTY_NAME}\",\"${VERSION_INSTALLED}\",\"${ARCHS_SUPPORTED}\",\"${METHOD}\",\"${SUMMARY}\"" fi fi done } function update_debs() { local STATUS="" update_apt for APP in "${INSTALLED_APPS[@]}"; do validate_deb "${APPNAME2FULL[$APP]}" if [ "${METHOD}" == "direct" ] || [ "${METHOD}" == "github" ] || [ "${METHOD}" == "gitlab" ] || [ "${METHOD}" == "website" ]; then STATUS=$(dpkg-query -Wf '${Status}' "${APP}") if [ "${STATUS}" == "install ok installed" ] && dpkg --compare-versions "${VERSION_PUBLISHED}" gt "${VERSION_INSTALLED}"; then fancy_message info "${APP} (${VERSION_INSTALLED}) has an update pending. ${VERSION_PUBLISHED} is available." fi fi done } function upgrade_debs() { local STATUS="" if [[ " $* " != *' --dg-only '* ]] ; then upgrade_apt else upgrade_only_dg fi for APP in "${INSTALLED_APPS[@]}"; do validate_deb "${APPNAME2FULL[$APP]}" if [ "${METHOD}" == "direct" ] || [ "${METHOD}" == "github" ]|| [ "${METHOD}" == "gitlab" ] || [ "${METHOD}" == "website" ]; then STATUS=$(dpkg-query -Wf '${Status}' "${APP}") if [ "${STATUS}" == "install ok installed" ]; then install_deb "${URL}" fi fi done } function init_repos() { if [ ! -e "${ETC_DIR}/01-main.repo" ]; then ${ELEVATE} tee "${ETC_DIR}/01-main.repo" <<<"${MAIN_REPO_URL}" >/dev/null fi for REPO in $(find "${ETC_DIR}" -maxdepth 1 -name "*.repo" ! -name 00-builtin.repo ! -name 99-local.repo -type f -printf "%f\n" | sed "s/.repo$//"); do if [ ! -e "${ETC_DIR}/${REPO}.d" ]; then ${ELEVATE} mkdir "${ETC_DIR}/${REPO}.d" 2>/dev/null ${ELEVATE} chmod 755 "${ETC_DIR}/${REPO}.d" 2>/dev/null fi done } function refresh_supported_cache_lists() { # WARN: this function must run in a subshell local lockfile=${CACHE_DIR}/updating_supported.lock if [ -f "$lockfile" ]; then pgrep -f "$DEBGET_BIN update" if [ $? ]; then # pgrep returned 1 (command not found), delete lockfile and continue fancy_message warn "Lock file found, but job is not running. Deleting $lockfile, cache update continues." ${ELEVATE} rm $lockfile else # pgrep returned 0 (command found), do nothing and return fancy_message warn "Cannot update cache of supported apps: $lockfile found (job still running?)" return 0 fi fi trap 'trap - EXIT; ${ELEVATE} rm -f "$lockfile"' EXIT ${ELEVATE} touch "$lockfile" ${ELEVATE} rm -f "${CACHE_DIR}/supported.list" "${CACHE_DIR}/supported_apps.list" if [ -z ${QUIET} ]; then fancy_message info "Updating cache of supported apps in the background" fi list_debs | grep -v -e '^[[:space:]][[:space:]]*\[' | ${ELEVATE} tee "${CACHE_DIR}/supported.list.tmp" >/dev/null ${ELEVATE} mv "${CACHE_DIR}/supported.list.tmp" "${CACHE_DIR}/supported.list" # # belt and braces no longer needed #${ELEVATE} sed -i '/[+]/d' ${CACHE_DIR}/supported.list.tmp cut -d" " -f 1 "${CACHE_DIR}/supported.list" | sort -u | ${ELEVATE} tee "${CACHE_DIR}/supported_apps.list" >/dev/null } function update_repos() { local REPO_URL="" # preserve current behaviour for now but allow modification via env local CURL_OPTS="--disable --progress-bar" local WGET_VERBOSITY="--quiet" if [[ " $* " == *' --quiet'* ]] ; then # shellcheck disable=SC2034 CURL_OPTS="--disable --show-error --silent" fi for REPO in $(find "${ETC_DIR}" -maxdepth 1 -name "*.repo" ! -name 00-builtin.repo ! -name 99-local.repo -type f -printf "%f\n" | sed "s/.repo$//"); do # export REPO ETC_DIR ELEVATE # no longer needed, `| bash -` replaced with `eval` if [ -z ${QUIET} ]; then fancy_message info "Updating ${ETC_DIR}/${REPO}" fi REPO_URL="$(head -n 1 "${ETC_DIR}/${REPO}.repo")" # shellcheck disable=SC2086 ${ELEVATE} wget ${WGET_VERBOSITY} ${WGET_TIMEOUT} "${REPO_URL}/manifest" -O "${ETC_DIR}/${REPO}.repo.tmp" && ${ELEVATE} mv "${ETC_DIR}/${REPO}.repo.tmp" "${ETC_DIR}/${REPO}.repo" # ${ELEVATE} rm "${ETC_DIR}/${REPO}.d/* # we currently leave old litter : either <- this or maybe rm older ones # although so long as manifest is good we are OK # Faster by some margin if we are hitting github # Otherwise revert to old-style for a bespoke hosted repo if pushd "${ETC_DIR}/${REPO}.d" >/dev/null; then (eval "$( awk -F/ '/github/ {print "# fetching github repo"; print "GITREPO="$4"/"$5;\ print "BRANCH="$6;\ print "curl ${CURL_OPTS} -L https://api.github.com/repos/${GITREPO}/tarball/${BRANCH} | ${ELEVATE} tar zx --wildcards \"*/${REPO}*/packages/*\" --strip-components=3"} ! /github/ {print "# fetching non-github repo"; print "tail -n +2 \"${ETC_DIR}/${REPO}.repo\" | sed \"s/^#//\" | ${ELEVATE} sort -u -o \"${ETC_DIR}/${REPO}.repo.tmp\"";\ print "${ELEVATE} wget ${WGET_VERBOSITY} ${WGET_TIMEOUT} -N -B \"${REPO_URL}/packages/\" -i \"${ETC_DIR}/${REPO}.repo.tmp\" -P \"${ETC_DIR}/${REPO}.d\""; print "${ELEVATE} rm \"${ETC_DIR}/${REPO}.repo.tmp\"" } '\ <<<"${REPO_URL}" )") popd >/dev/null || true fi done refresh_supported_cache_lists & } function list_repo_apps() { if [ -d "${ETC_DIR}" ]; then find "${ETC_DIR}" -maxdepth 1 -name '*.repo' ! -name 00-builtin.repo ! -name 99-local.repo -type f -printf '%f\n' | sort -r | while IFS= read -r REPO; do # WARN: repos can't contain '/' or '\', which the rest of the code assumes anyway sed -n -e "1d; /^#/d; s/^/${REPO%.repo}\//p" <"${ETC_DIR}/${REPO}" | sort -u done fi } function populate_deprecated_apps() { declare -ga DEPRECATED_APPS=() DEPRECATED_INSTALLED=() if [ -d "${ETC_DIR}" ]; then mapfile -t DEPRECATED_APPS < <( find "${ETC_DIR}" -maxdepth 1 -name "*.repo" ! -name 00-builtin.repo ! -name 99-local.repo -type f -printf "%f\n" | sort -r | while IFS= read -r REPO; do # sort -t ... -u may fail sed -n -e "1d; s/^#/${REPO%.repo}\//p" <"${ETC_DIR}/${REPO}" | sort -t / -k 2 | uniq done ) if [ "${#DEPRECATED_APPS[@]}" -gt 0 ]; then mapfile -t DEPRECATED_INSTALLED < <(dpkg-query 2>/dev/null -Wf '${db:Status-abbrev}${Package}\n' "${DEPRECATED_APPS[@]##*/}" | sed -n -e 's/^ii //p') fi fi readonly DEPRECATED_INSTALLED } function list_local_apps() { if [ -d "${ETC_DIR}/99-local.d" ]; then find "${ETC_DIR}/99-local.d" -maxdepth 1 -type f -printf '99-local/%f\n' fi } function print_etc_overrides() { if [ "${#LOCAL_APPS[@]}" -gt 0 ] || [ "${#APP_CONFLICTS[@]}" -gt 0 ]; then local DEB_GET_SCRIPT_FILE="${0}" local NUM_OLDER_CONFLICTS=0 for APP in "${APP_CONFLICTS[@]}"; do local FULL_APP=${APPNAME2FULL[$APP]} fancy_message warn "Conflict detected, duplicate declaration of package ${APP}, using declaration from ${FULL_APP%/*}" if [[ " ${LOCAL_APPS[*]} " =~ " ${FULL_APP} " ]] && [ "${DEB_GET_SCRIPT_FILE}" -nt "${ETC_DIR}/99-local.d/${APP}" ]; then ((NUM_OLDER_CONFLICTS++)) fi done if [ "${NUM_OLDER_CONFLICTS}" -gt 0 ]; then fancy_message recommend "Duplicate entr(ies) already merged upstream (if no longer needed), must be manually removed from your ${ETC_DIR}/99-local.d folder." fi for FULL_APP in "${LOCAL_APPS[@]}"; do fancy_message info "Including local package ${FULL_APP##*/}" done if [ "${#LOCAL_APPS[@]}" -gt 0 ]; then fancy_message recommend "Please consider contributing back new entries, an issue (or raise a PR) directly at https://github.com/wimpysworld/deb-get/pulls" fi fi } function print_deprecated() { for APP in "${DEPRECATED_INSTALLED[@]}"; do fancy_message warn "Deprecated package ${APP} detected. It will no longer receive updates, and keeping it installed is considered unsafe." fancy_message recommend "Please remove it with: deb-get purge ${APP}" done } function fix_old_apps() { local OLD_METHOD="" local OLD_APT_LIST_NAME="" local OLD_PPA="" case "${APP}" in 1password) OLD_METHOD="apt" OLD_APT_LIST_NAME="1password" ;; anydesk) OLD_METHOD="apt" OLD_APT_LIST_NAME="anydesk-stable" ;; appimagelauncher) OLD_METHOD="ppa" OLD_PPA="ppa:appimagelauncher-team/stable" ;; atom) OLD_METHOD="apt" OLD_APT_LIST_NAME="atom" ;; audio-recorder) OLD_METHOD="ppa" OLD_PPA="ppa:audio-recorder/ppa" ;; azure-cli) OLD_METHOD="apt" OLD_APT_LIST_NAME="azure-cli" ;; blanket) OLD_METHOD="ppa" OLD_PPA="ppa:apandada1/blanket" ;; brave-browser) OLD_METHOD="apt" OLD_APT_LIST_NAME="brave-browser-release" ;; cawbird) OLD_METHOD="apt" OLD_APT_LIST_NAME="home:IBBoard:cawbird" ;; chronograf) OLD_METHOD="apt" OLD_APT_LIST_NAME="influxdata" ;; code) OLD_METHOD="apt" OLD_APT_LIST_NAME="vscode" ;; copyq) OLD_METHOD="ppa" OLD_PPA="ppa:hluk/copyq" ;; cryptomator) OLD_METHOD="ppa" OLD_PPA="ppa:sebastian-stenzel/cryptomator" ;; docker-ce) OLD_METHOD="apt" OLD_APT_LIST_NAME="docker" ;; enpass) OLD_METHOD="apt" OLD_APT_LIST_NAME="enpass" ;; firefox-esr) OLD_METHOD="ppa" OLD_PPA="ppa:mozillateam/ppa" ;; foliate) OLD_METHOD="ppa" OLD_PPA="ppa:apandada1/foliate" ;; fsearch) OLD_METHOD="ppa" OLD_PPA="ppa:christian-boxdoerfer/fsearch-stable" ;; google-chrome-stable) OLD_METHOD="apt" OLD_APT_LIST_NAME="google-chrome" ;; google-earth-pro-stable) OLD_METHOD="apt" OLD_APT_LIST_NAME="google-earth-pro" ;; gpu-viewer) OLD_METHOD="ppa" OLD_PPA="ppa:arunsivaraman/gpuviewer" ;; influxdb) OLD_METHOD="apt" OLD_APT_LIST_NAME="influxdata" ;; influxdb2) OLD_METHOD="apt" OLD_APT_LIST_NAME="influxdata" ;; influxdb2-cli) OLD_METHOD="apt" OLD_APT_LIST_NAME="influxdata" ;; insync) OLD_METHOD="apt" OLD_APT_LIST_NAME="insync" ;; jellyfin) OLD_METHOD="apt" OLD_APT_LIST_NAME="jellyfin" ;; kapacitor) OLD_METHOD="apt" OLD_APT_LIST_NAME="influxdata" ;; kdiskmark) OLD_METHOD="ppa" OLD_PPA="ppa:jonmagon/kdiskmark" ;; keepassxc) OLD_METHOD="ppa" OLD_PPA="ppa:phoerious/keepassxc" ;; keybase) OLD_METHOD="apt" OLD_APT_LIST_NAME="keybase" ;; kopia-ui) OLD_METHOD="apt" OLD_APT_LIST_NAME="kopia" ;; lutris) OLD_METHOD="ppa" OLD_PPA="ppa:lutris-team/lutris" ;; microsoft-edge-stable) OLD_METHOD="apt" OLD_APT_LIST_NAME="microsoft-edge" ;; neo4j) OLD_METHOD="apt" OLD_APT_LIST_NAME="neo4j" ;; nextcloud-desktop) OLD_METHOD="ppa" OLD_PPA="ppa:nextcloud-devs/client" ;; nomad) OLD_METHOD="apt" OLD_APT_LIST_NAME="nomad" ;; obs-studio) OLD_METHOD="ppa" OLD_PPA="ppa:flexiondotorg/obs-fully-loaded" ;; openrazer-meta) OLD_METHOD="ppa" OLD_PPA="ppa:openrazer/stable" ;; opera-stable) OLD_METHOD="apt" OLD_APT_LIST_NAME="opera-stable" ;; papirus-icon-theme) OLD_METHOD="ppa" OLD_PPA="ppa:papirus/papirus" ;; plexmediaserver) OLD_METHOD="apt" OLD_APT_LIST_NAME="plexmediaserver" ;; polychromatic) OLD_METHOD="ppa" OLD_PPA="ppa:polychromatic/stable" ;; protonvpn) OLD_METHOD="apt" OLD_APT_LIST_NAME="protonvpn-stable" ;; qownnotes) OLD_METHOD="ppa" OLD_PPA="ppa:pbek/qownnotes" ;; quickemu) OLD_METHOD="ppa" OLD_PPA="ppa:flexiondotorg/quickemu" ;; quickgui) OLD_METHOD="ppa" OLD_PPA="ppa:yannick-mauray/quickgui" ;; resilio-sync) OLD_METHOD="apt" OLD_APT_LIST_NAME="resilio-sync" ;; retroarch) OLD_METHOD="ppa" OLD_PPA="ppa:libretro/stable" ;; signal-desktop) OLD_METHOD="apt" OLD_APT_LIST_NAME="signal-xenial.list" ;; skypeforlinux) OLD_METHOD="apt" OLD_APT_LIST_NAME="skype-stable" ;; slack-desktop) OLD_METHOD="apt" OLD_APT_LIST_NAME="slack" ;; softmaker-office-2021) OLD_METHOD="apt" OLD_APT_LIST_NAME="softmaker" ;; strawberry) OLD_METHOD="ppa" OLD_PPA="ppa:jonaski/strawberry" ;; sublime-merge) OLD_METHOD="apt" OLD_APT_LIST_NAME="sublime-text" ;; sublime-text) OLD_METHOD="apt" OLD_APT_LIST_NAME="sublime-text" ;; syncthing) OLD_METHOD="apt" OLD_APT_LIST_NAME="syncthing" ;; teams) OLD_METHOD="apt" OLD_APT_LIST_NAME="teams" ;; telegraf) OLD_METHOD="apt" OLD_APT_LIST_NAME="influxdata" ;; terraform) OLD_METHOD="apt" OLD_APT_LIST_NAME="terraform" ;; texworks) OLD_METHOD="ppa" OLD_PPA="ppa:texworks/stable" ;; typora) OLD_METHOD="apt" OLD_APT_LIST_NAME="typora" ;; ubuntu-make) OLD_METHOD="ppa" OLD_PPA="ppa:lyzardking/ubuntu-make" ;; ulauncher) OLD_METHOD="ppa" OLD_PPA="ppa:agornostal/ulauncher" ;; virtualbox-6.1) OLD_METHOD="apt" OLD_APT_LIST_NAME="virtualbox-6.1" ;; vivaldi-stable) OLD_METHOD="apt" OLD_APT_LIST_NAME="vivaldi" ;; wavebox) OLD_METHOD="apt" OLD_APT_LIST_NAME="wavebox-stable" ;; weechat) OLD_METHOD="apt" OLD_APT_LIST_NAME="weechat" ;; wire-desktop) OLD_METHOD="apt" OLD_APT_LIST_NAME="wire-desktop" ;; xemu) OLD_METHOD="ppa" OLD_PPA="ppa:mborgerson/xemu" ;; yq) OLD_METHOD="ppa" OLD_PPA="ppa:rmescandon/yq" ;; esac if [ -n "${OLD_METHOD}" ]; then if [ "${OLD_METHOD}" = apt ]; then remove_old_apt_repo "${OLD_APT_LIST_NAME}" else # ppa remove_old_ppa_repo "${OLD_PPA}" fi fi if [ "${METHOD}" = apt ]; then add_apt_repo elif [ "${METHOD}" = ppa ]; then ppa_to_apt add_apt_repo fi add_installed if [ "${DEFVER}" != 1 ]; then fancy_message warn "${APP} must be manually reinstalled with \"deb-get reinstall ${APP}\", otherwise it will not be updated properly" fi } function fix_installed() { local line="$(grep -m 1 "^${APP} " "${ETC_DIR}/installed")" local OLD_DEFVER="$(cut -d " " -f 2 <<<"$line")" local OLD_METHOD="$(cut -d " " -f 3 <<<"$line")" if [ "${DEFVER}" != "${OLD_DEFVER}" ]; then remove_installed "${APP}" if [[ " apt ppa " =~ " ${OLD_METHOD} " ]]; then remove_repo --remove-repo fi if [ "${METHOD}" = apt ]; then add_apt_repo elif [ "${METHOD}" = ppa ]; then ppa_to_apt add_apt_repo fi add_installed fancy_message warn "${APP} must be manually reinstalled with \"deb-get reinstall ${APP}\", otherwise it will not be updated properly" fi } function remove_old_apt_repo() { fancy_message info "Removing /etc/apt/trusted.gpg.d/${1}.asc" ${ELEVATE} rm -f "/etc/apt/trusted.gpg.d/${1}.asc" fancy_message info "Removing /etc/apt/sources.list.d/${1}.list" ${ELEVATE} rm -f "/etc/apt/sources.list.d/${1}.list" } function remove_old_ppa_repo() { local -r PPA_ADDRESS=${1#*:} local -r PPA_PERSON=${PPA_ADDRESS%%/*} local -r PPA_ARCHIVE=${PPA_ADDRESS#*/} local -r APT_LIST_NAME="${PPA_PERSON}-ubuntu-${PPA_ARCHIVE}" fancy_message info "Removing /etc/apt/trusted.gpg.d/${APT_LIST_NAME}.gpg" ${ELEVATE} rm -f "/etc/apt/trusted.gpg.d/${APT_LIST_NAME}.gpg" ${ELEVATE} rm -f "/etc/apt/trusted.gpg.d/${APT_LIST_NAME}.gpg~" fancy_message info "Removing /etc/apt/sources.list.d/${APT_LIST_NAME}-${UPSTREAM_CODENAME}.list" ${ELEVATE} rm -f "/etc/apt/sources.list.d/${APT_LIST_NAME}-${UPSTREAM_CODENAME}.list" } function remove_repo() { if [[ -n "${PPA}" ]]; then local -r PPA_ADDRESS=${PPA#*:} local -r PPA_PERSON=${PPA_ADDRESS%%/*} local -r PPA_ARCHIVE=${PPA_ADDRESS#*/} APT_LIST_NAME="${PPA_PERSON}-ubuntu-${PPA_ARCHIVE}-${UPSTREAM_CODENAME}" fi local count="" if [ -e "${ETC_DIR}/aptrepos" ]; then count="$(grep -m 1 "^${APT_LIST_NAME} " "${ETC_DIR}/aptrepos" | cut -d " " -f 2)" fi if [ -z "${count}" ]; then count=0 fi if [ "${count}" -gt 0 ]; then ((count--)) ${ELEVATE} sed -i -E "/^${APT_LIST_NAME} [0-9]+/d" "${ETC_DIR}/aptrepos" ${ELEVATE} tee -a "${ETC_DIR}/aptrepos" <<<"${APT_LIST_NAME} ${count}" >/dev/null fi if [ "${1}" == --remove-repo ]; then if [ "${count}" -eq 0 ]; then if [ "${2}" != --quiet ]; then fancy_message info "Removing /usr/share/keyrings/${APT_LIST_NAME}-archive-keyring.gpg" fi ${ELEVATE} rm -f "/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring.gpg" if [ "${2}" != --quiet ]; then fancy_message info "Removing /etc/apt/sources.list.d/${APT_LIST_NAME}.list" fi ${ELEVATE} rm -f "/etc/apt/sources.list.d/${APT_LIST_NAME}.list" if [ -e "${ETC_DIR}/aptrepos" ]; then ${ELEVATE} sed -i -E "/^${APT_LIST_NAME} [0-9]+/d" "${ETC_DIR}/aptrepos" fi elif [ "${2}" != --quiet ]; then fancy_message warn "/etc/apt/sources.list.d/${APT_LIST_NAME}.list was not removed because other packages depend on it." fi fi } function add_apt_repo() { if [[ "${ACTION}" != "reinstall" ]]; then local count="" if [ -e "${ETC_DIR}/aptrepos" ]; then count="$(grep -m 1 "^${APT_LIST_NAME} " "${ETC_DIR}/aptrepos" | cut -d " " -f 2)" fi if [ -z "${count}" ]; then count=0 fi if [ "${count}" -eq 0 ] && [ -e "/etc/apt/sources.list.d/${APT_LIST_NAME}.list" ]; then ((count++)) fi ((count++)) if [ -e "${ETC_DIR}/aptrepos" ]; then ${ELEVATE} sed -i -E "/^${APT_LIST_NAME} [0-9]+/d" "${ETC_DIR}/aptrepos" fi ${ELEVATE} tee -a "${ETC_DIR}/aptrepos" <<<"${APT_LIST_NAME} ${count}" >/dev/null fi if [ ! -d /usr/share/keyrings ]; then ${ELEVATE} mkdir -p /usr/share/keyrings 2>/dev/null fi if [ ! -e "/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring.gpg" ]; then if [ -n "${ASC_KEY_URL}" ]; then # shellcheck disable=SC2086 ${ELEVATE} wget ${WGET_VERBOSITY} ${WGET_TIMEOUT} "${ASC_KEY_URL}" -O "/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring" ${ELEVATE} gpg --yes --dearmor "/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring" ${ELEVATE} rm "/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring" elif [ -n "${GPG_KEY_ID}" ]; then #Fetching from the keyserver will fail if root doesn't already have a .gnupg directory. #This will create it if it doesn't already exist. if ${ELEVATE} printenv GNUPGHOME > /dev/null && ${ELEVATE} [ ! -e "${GNUPGHOME}" ] || ${ELEVATE} [ ! -e "$(${ELEVATE} printenv HOME)/.gnupg" ] ; then ${ELEVATE} gpg --list-keys fi ${ELEVATE} gpg --no-default-keyring --keyring /usr/share/keyrings/"${APT_LIST_NAME}"-archive-keyring-temp.gpg --keyserver keyserver.ubuntu.com --recv "${GPG_KEY_ID}" ${ELEVATE} gpg --no-default-keyring --keyring /usr/share/keyrings/"${APT_LIST_NAME}"-archive-keyring-temp.gpg --yes --output "/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring.gpg" --export "${GPG_KEY_ID}" ${ELEVATE} rm /usr/share/keyrings/"${APT_LIST_NAME}"-archive-keyring-temp.gpg else #GPG_KEY_URL # shellcheck disable=SC2086 ${ELEVATE} wget ${WGET_VERBOSITY} ${WGET_TIMEOUT} "${GPG_KEY_URL}" -O "/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring.gpg" fi fi local APT_LIST_LINE="deb [signed-by=/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring.gpg" if [ -n "${APT_REPO_OPTIONS}" ]; then APT_LIST_LINE="${APT_LIST_LINE} ${APT_REPO_OPTIONS}" fi APT_LIST_LINE="${APT_LIST_LINE}] ${APT_REPO_URL}" ${ELEVATE} tee "/etc/apt/sources.list.d/${APT_LIST_NAME}.list" <<<"${APT_LIST_LINE}" >/dev/null } function ppa_to_apt() { local -r PPA_ADDRESS=${PPA#*:} local -r PPA_PERSON=${PPA_ADDRESS%%/*} local -r PPA_ARCHIVE=${PPA_ADDRESS#*/} export APT_REPO_URL="https://ppa.launchpadcontent.net/${PPA_ADDRESS}/ubuntu/ ${UPSTREAM_CODENAME} main" export ASC_KEY_URL="https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$(curl -s "https://api.launchpad.net/devel/~${PPA_PERSON}/+archive/ubuntu/${PPA_ARCHIVE}" | grep -o -E "\"signing_key_fingerprint\": \"[0-9A-F]+\"" | cut -d \" -f 4)" export APT_LIST_NAME="${PPA_PERSON}-ubuntu-${PPA_ARCHIVE}-${UPSTREAM_CODENAME}" } function maint_supported_cache() { # called by install and re-install when we've installed # so we should be supported if [ -f "${CACHE_DIR}/supported.list" ]; then case "${ACTION}" in remove|purge) ${ELEVATE} sed -i "/^${APP} /d" "${CACHE_DIR}/supported.list" cat "${CACHE_DIR}/supported.list" - <<<"${APP}" | ${ELEVATE} sort -t " " -k 1 -u -o "${CACHE_DIR}/supported.list" ;; reinstall|install) local PAD=' ' local cache_line=$(printf "%s %s [ installed ]\n" "${APP}" "${PAD:${#APP}}") # # First remove the bare entry ${ELEVATE} sed -i -e '/^${APP}$/d' "${CACHE_DIR}/supported.list" # Replace it with a flagged one cat "${CACHE_DIR}/supported.list" - <<<"${cache_line}" | ${ELEVATE} sort -t " " -k 1 -u -o "${CACHE_DIR}/supported.list" # should be there but safest to be sure grep -q -w "${APP}$" "${CACHE_DIR}"/supported_apps.list || \ cat ${CACHE_DIR}/supported_apps.list - <<<"${APP}" | ${ELEVATE} sort -t " " -k 1 -u -o ${CACHE_DIR}/supported_apps.list ;; esac fi } function add_installed() { local line="${APP} ${DEFVER} ${METHOD}" cat "${ETC_DIR}/installed" - <<< "${line}" | ${ELEVATE} sort -t " " -k 1 -u -o "${ETC_DIR}/installed" } function remove_installed() { ${ELEVATE} sed -i "/^${1} /d" "${ETC_DIR}/installed" } function list_apps() { list_local_apps; list_repo_apps declare -F | sed -n -e 's|declare -f deb_|00-builtin/|gp' } function populate_apps() { declare -ga APPS declare -gA APPNAME2FULL local APP mapfile -t APPS < <(list_apps | sort -t / -k 2 -u) # set, unless key already exists (only first match counts) for APP in "${APPS[@]}"; do : "${APPNAME2FULL["${APP##*/}"]:=${APP}}"; done } function deb_deb-get() { DEFVER=1 ARCHS_SUPPORTED="amd64 arm64 armhf i386" get_github_releases "wimpysworld/deb-get" if [ "${ACTION}" != "prettylist" ]; then URL="$(grep "browser_download_url.*\.deb\"" "${CACHE_FILE}" | head -n1 | cut -d'"' -f4)" VERSION_PUBLISHED="$(echo "${URL}" | cut -d'_' -f2)" fi PRETTY_NAME="deb-get" WEBSITE="https://github.com/wimpysworld/deb-get" SUMMARY="'apt-get' functionality for .debs published in 3rd party repositories or via direct download package." } function parse_machine() { export HOST_CPU="$(uname -m)" case "${HOST_CPU}" in aarch64|armv7l|x86_64) export HOST_ARCH="$(dpkg --print-architecture)";; *) fancy_message fatal "${HOST_CPU} is not supported. Quitting.";; esac OS_ID=$(lsb_release --id --short) case "${OS_ID}" in Debian) OS_ID_PRETTY="Debian";; Devuan) OS_ID_PRETTY="Devuan";; elementary|Elementary) OS_ID_PRETTY="elementary OS";; Linuxmint) OS_ID_PRETTY="Linux Mint";; Neon) OS_ID_PRETTY="KDE Neon";; Pop) OS_ID_PRETTY="Pop!_OS";; Ubuntu) OS_ID_PRETTY="Ubuntu";; Zorin) OS_ID_PRETTY="Zorin OS";; *) OS_ID_PRETTY="${OS_ID}" fancy_message warn "${OS_ID} is not supported." ;; esac OS_CODENAME=$(lsb_release --codename --short) if [ -e /etc/os-release ]; then OS_RELEASE=/etc/os-release elif [ -e /usr/lib/os-release ]; then OS_RELEASE=/usr/lib/os-release else fancy_message fatal "os-release not found. Quitting" fi UPSTREAM_ID="$(sed -n -e 's/^ID=//p' "${OS_RELEASE}")" # Fallback to ID_LIKE if ID was not 'ubuntu' or 'debian' if ! [[ ' ubuntu debian ' =~ " ${UPSTREAM_ID} " ]]; then UPSTREAM_ID_LIKE="$(sed -n -e 's/^ID_LIKE=//p' "${OS_RELEASE}" | cut -d \" -f 2)" if [[ " ${UPSTREAM_ID_LIKE} " =~ " ubuntu " ]]; then UPSTREAM_ID=ubuntu elif [[ " ${UPSTREAM_ID_LIKE} " =~ " debian " ]]; then UPSTREAM_ID=debian else fancy_message fatal "${OS_ID_PRETTY} ${OS_CODENAME^} is not supported because it is not derived from a supported Debian or Ubuntu release." fi fi # Debian Sid & Testing don't always have their own unique os-release file. It is often the same as the previous Debian stable. # So we first check /etc/debian_version to make sure we're not running Stable. # If not, we will then use apt-cache policy to distinguish between Debian Sid and Debian Testing. local ETC_DEB_VER="$(< /etc/debian_version)" if [[ "${UPSTREAM_ID}" == "debian" ]] && grep -q "sid" <<< "${ETC_DEB_VER}"; then if apt-cache policy | grep -q -i -e "o=debian.*n=${ETC_DEB_VER%%/*}" -e "o=debian.*n=testing"; then UPSTREAM_CODENAME="${ETC_DEB_VER%%/*}" OS_CODENAME="${UPSTREAM_CODENAME}" # shellcheck disable=SC2034 # this variable is only used within package definition files DEBIAN_TESTING="${UPSTREAM_CODENAME}" else UPSTREAM_CODENAME="sid" OS_CODENAME="sid" fi else local codename for codename in UBUNTU_CODENAME DEBIAN_CODENAME VERSION_CODENAME; do UPSTREAM_CODENAME=$(sed -n -e "/^$codename=/ {s/^$codename=//p;q}" "${OS_RELEASE}") [ -z "${UPSTREAM_CODENAME}" ] || break done fi case "${UPSTREAM_CODENAME}" in sid) UPSTREAM_RELEASE="unstable";; *) UPSTREAM_RELEASE="$(grep -m1 -o -P "^[^,\s]+(?=\s*[A-Z]*,[^,]+,${UPSTREAM_CODENAME},)" "${DISTRO_INFO_DIR}/${UPSTREAM_ID}.csv")" #Devuan if [ -z ${UPSTREAM_RELEASE} ] && [[ "${UPSTREAM_ID}" == "debian" ]]; then UPSTREAM_RELEASE="$(grep -E -o -m 1 "^[0-9]+" "/etc/debian_version")" UPSTREAM_CODENAME="$(grep -m1 -o -P "^${UPSTREAM_RELEASE},[^,]+,\K[a-z]+(?=,)" "${DISTRO_INFO_DIR}/${UPSTREAM_ID}.csv")" fi ;; esac if [ -z ${UPSTREAM_CODENAME} ] || [ -z ${UPSTREAM_RELEASE} ]; then fancy_message fatal "${OS_ID_PRETTY} ${OS_CODENAME^} is not supported because it is not derived from a supported Debian or Ubuntu release." fi } function dg_action_cache() { ls -lh "${CACHE_DIR}/" } function dg_action_clean() { elevate_privs ${ELEVATE} rm -fv "${CACHE_DIR}"/*.deb ${ELEVATE} rm -fv "${CACHE_DIR}"/*.json* ${ELEVATE} rm -fv "${CACHE_DIR}"/*.html ${ELEVATE} rm -fv "${CACHE_DIR}"/*.txt } function dg_action_show() { for APP in "${@,,}"; do FULL_APP=${APPNAME2FULL[$APP]} if [ -z "${FULL_APP}" ]; then fancy_message error "${APP} is not a supported application." ACTION="list" list_debs "" --raw >&2 exit 1 fi if validate_deb "${FULL_APP}"; then info_deb fi done } function dg_action_reinstall() { dg_action_install "$@"; } function dg_action_install() { elevate_privs create_cache_dir create_etc_dir for APP in "${@,,}"; do FULL_APP=${APPNAME2FULL[$APP]} if [ -z "${FULL_APP}" ]; then fancy_message error "${APP} is not a supported application." ACTION="list" list_debs "" --raw >&2 exit 1 fi if validate_deb "${FULL_APP}"; then if [[ " ${ARCHS_SUPPORTED} " != *" ${HOST_ARCH} "* && " ${ARCHS_SUPPORTED} " != *" all "* ]]; then fancy_message fatal "${APP} is not supported on ${HOST_ARCH}." fi if [ -n "${CODENAMES_SUPPORTED}" ] && ! [[ " ${CODENAMES_SUPPORTED[*]} " =~ " ${UPSTREAM_CODENAME} " ]]; then fancy_message fatal "${APP} is not supported on ${OS_ID_PRETTY} ${UPSTREAM_CODENAME^}." fi if [ "${METHOD}" == "ppa" ] && [ "${UPSTREAM_ID}" != "ubuntu" ]; then fancy_message fatal "${APP} cannot be installed as PPAs are not supported on distros that are not derived from Ubuntu." fi case "${METHOD}" in direct|github|gitlab|website) install_deb "${URL}";; apt) install_apt;; ppa) install_ppa;; esac fi done } function dg_action_list() { list_opt_1="" list_opt_2="" while [ -n "${1}" ]; do if [ "${1}" == --include-unsupported ]; then list_opt_1=--include-unsupported elif [[ " --raw --installed --not-installed --only-unsupported " =~ " ${1} " ]]; then list_opt_2="${1}" else fancy_message fatal "Unknown option supplied: ${1}" fi shift done list_debs "${list_opt_1}" "${list_opt_2}" } function dg_action_prettylist() { ACTION="prettylist" prettylist_debs "${1}" } function dg_action_csvlist() { ACTION="prettylist" csvlist_debs "${1}" } function dg_action_purge() { elevate_privs opt_remove_repo="" if [ "${1}" == --remove-repo ]; then opt_remove_repo=--remove-repo shift fi for APP in "${@,,}"; do FULL_APP=${APPNAME2FULL[$APP]} if [ -z "${FULL_APP}" ]; then FULL_APP="$(printf '%s\n' "${DEPRECATED_APPS[@]}" | grep -m 1 "/${APP}$")" fi if [ -z "${FULL_APP}" ]; then fancy_message error "${APP} is not a supported application." ACTION="list" list_debs "" --raw >&2 exit 1 fi if validate_deb "${FULL_APP}"; then remove_deb "${APP}" purge "${opt_remove_repo}" fi done } function dg_action_remove() { elevate_privs opt_remove_repo="" if [ "${1}" == --remove-repo ]; then opt_remove_repo=--remove-repo shift fi for APP in "${@,,}"; do FULL_APP=${APPNAME2FULL[$APP]} if [ -z "${FULL_APP}" ]; then FULL_APP="$(printf '%s\n' "${DEPRECATED_APPS[@]}" | grep -m 1 "/${APP}$")" fi if [ -z "${FULL_APP}" ]; then fancy_message error "${APP} is not a supported application." ACTION="list" list_debs "" --raw >&2 exit 1 fi if validate_deb "${FULL_APP}"; then remove_deb "${APP}" "" "${opt_remove_repo}" fi done } function dg_action_search() { local list_opt_1='' if [ "${1}" == --include-unsupported ]; then list_opt_1=$1 shift fi if [ -z "${1}" ]; then fancy_message error "You must specify a pattern." usage >&2 exit 1 fi list_debs "$list_opt_1" --raw | grep -e "${1}" } function dg_action_update() { if [ -n "${1}" ] && ! [[ ' --repos-only --quiet ' =~ "${1}" ]]; then fancy_message fatal "Unknown option supplied: ${1}" elif [ -n "${2}" ] && ! [[ ' --repos-only --quiet ' =~ "${2}" ]]; then fancy_message fatal "Unknown option supplied: ${2}" fi if [ -n "${3}" ] ; then fancy_message error "Ignoring extra options from : ${3}" fi if [[ " $* " == *' --quiet '* ]] ; then QUIET="q" fi elevate_privs create_cache_dir create_etc_dir init_repos update_repos "$@" if [[ " $* " != *' --repos-only '* ]] ; then populate_apps for APP in "${INSTALLED_APPS[@]}"; do FULL_APP=${APPNAME2FULL[$APP]} if [ -n "${FULL_APP}" ]; then if validate_deb "${FULL_APP}"; then fix_installed fi else remove_installed "${APP}" fi done mapfile -t INSTALLED_APPS <"${ETC_DIR}/installed"; INSTALLED_APPS=("${INSTALLED_APPS[@]%% *}") update_debs fi } function dg_action_upgrade() { elevate_privs create_cache_dir upgrade_debs "$@" } function dg_action_fix-installed() { if [ -n "${1}" ] && [ "${1}" != --old-apps ]; then fancy_message fatal "Unknown option supplied: ${1}" fi elevate_privs if [ "${1}" = --old-apps ]; then for APP in $(dpkg-query 2>/dev/null -Wf '${db:Status-abbrev}${Package}\n' "${APPS[*]##*/}" | sed -n -e 's/^ii //p'); do if validate_deb "$(printf '%s\n' "${APPS[@]}" | grep -m 1 "/${APP}$")"; then fix_old_apps fi done else for APP in "${INSTALLED_APPS[@]}"; do FULL_APP="$(printf '%s\n' "${APPS[@]}" | grep -m 1 "/${APP}$")" if [ -n "${FULL_APP}" ]; then if validate_deb "${FULL_APP}"; then fix_installed fi else remove_installed "${APP}" fi done fi } function dg_action_version() { echo "${VERSION}"; } function dg_action_help() { usage; } if ((BASH_VERSINFO[0] < 4)); then fancy_message fatal "Sorry, you need bash 4.0 or newer to run $(basename "${0}")." fi if ! command -v lsb_release 1>/dev/null; then fancy_message fatal "lsb_release not detected. Quitting." fi export CACHE_DIR="/var/cache/deb-get" readonly ETC_DIR="/etc/deb-get" readonly MAIN_REPO_URL="https://raw.githubusercontent.com/wimpysworld/deb-get/main/01-main" readonly DISTRO_INFO_DIR="/usr/share/distro-info" # shellcheck disable=SC2034 readonly USER_AGENT="Mozilla/5.0 (X11; Linux ${HOST_CPU}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36" readonly DEBGET_BIN=$(basename $0) parse_machine if [ -n "${1}" ]; then ACTION="${1,,}" shift else fancy_message error "You must specify an action." usage >&2 exit 1 fi case "${ACTION}" in (pretty_list|csv_list) ACTION=${ACTION/_/} ;; (csv) ACTION=csvlist ;; esac case "${ACTION}" in (update|upgrade|show|reinstall|install|remove|purge|search|list|prettylist|csvlist|fix-installed) populate_apps mapfile -t APP_CONFLICTS < <(printf '%s\n' "${APPS[@]##*/}" | uniq --repeated) mapfile -t LOCAL_APPS < <(printf '%s\n' "${APPS[@]}" | grep "^99-local/") if [ -e "${ETC_DIR}/installed" ]; then mapfile -t INSTALLED_APPS <"${ETC_DIR}/installed"; INSTALLED_APPS=("${INSTALLED_APPS[@]%% *}") else INSTALLED_APPS=() fi readonly APP_CONFLICTS LOCAL_APPS ;; esac case "${ACTION}" in (install|reinstall|remove|purge|show) if [ -z "${1}" ]; then fancy_message error "You must specify an app:\n" ACTION="list" list_debs "" --raw >&2 exit 1 fi print_etc_overrides populate_deprecated_apps print_deprecated;; esac export ELEVATE="" if declare -F "dg_action_$ACTION" >/dev/null; then "dg_action_$ACTION" "$@" else fancy_message fatal "Unknown action supplied: ${ACTION}" fi if [[ ${PACKAGE_INSTALLATION_COUNT} -lt ${PACKAGE_INSTALLATION_TRIES} ]]; then exit 1 fi