#!/bin/sh # SPDX-License-Identifier: GPL-3.0-only # # This file is part of the distrobox project: # https://github.com/89luca89/distrobox # # Copyright (C) 2021 distrobox contributors # # distrobox is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3 # as published by the Free Software Foundation. # # distrobox is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with distrobox; if not, see . # POSIX # Optional env variables: # DBX_CONTAINER_MANAGER # DBX_CONTAINER_NAME # DBX_CONTAINER_RM_CUSTOM_HOME # DBX_NON_INTERACTIVE # DBX_VERBOSE # DBX_SUDO_PROGRAM # Despite of running this script via SUDO/DOAS being not supported (the # script itself will call the appropriate tool when necessary), we still want # to allow people to run it as root, logged in in a shell, and create rootful # containers. # # SUDO_USER is a variable set by SUDO and can be used to check whether the script was called by it. Same thing for DOAS_USER, set by DOAS. if { [ -n "${SUDO_USER}" ] || [ -n "${DOAS_USER}" ] } && [ "$(id -ru)" -eq 0 ]; then printf >&2 "Running %s via SUDO/DOAS is not supported. Instead, please try running:\n" "$(basename "${0}")" printf >&2 " %s --root %s\n" "$(basename "${0}")" "$*" exit 1 fi # Ensure we have our env variables correctly set [ -z "${USER}" ] && USER="$(id -run)" [ -z "${HOME}" ] && HOME="$(getent passwd "${USER}" | cut -d':' -f6)" [ -z "${SHELL}" ] && SHELL="$(getent passwd "${USER}" | cut -d':' -f7)" # Defaults all=0 container_manager="autodetect" distrobox_flags="" distrobox_path="$(dirname "$(realpath "${0}")")" force=0 force_flag="" non_interactive=0 # If the user runs this script as root in a login shell, set rootful=1. # There's no need for them to pass the --root flag option in such cases. [ "$(id -ru)" -eq 0 ] && rootful=1 || rootful=0 verbose=0 rm_home=0 response_rm_home="N" version="1.8.2.2" # Source configuration files, this is done in an hierarchy so local files have # priority over system defaults # leave priority to environment variables. # # On NixOS, for the distrobox derivation to pick up a static config file shipped # by the package maintainer the path must be relative to the script itself. self_dir="$(dirname "$(realpath "$0")")" nix_config_file="${self_dir}/../share/distrobox/distrobox.conf" config_files=" ${nix_config_file} /usr/share/distrobox/distrobox.conf /usr/share/defaults/distrobox/distrobox.conf /usr/etc/distrobox/distrobox.conf /usr/local/share/distrobox/distrobox.conf /etc/distrobox/distrobox.conf ${XDG_CONFIG_HOME:-"${HOME}/.config"}/distrobox/distrobox.conf ${HOME}/.distroboxrc " for config_file in ${config_files}; do # Shellcheck will give error for sourcing a variable file as it cannot follow # it. We don't care so let's disable this linting for now. # shellcheck disable=SC1090 [ -e "${config_file}" ] && . "$(realpath "${config_file}")" done [ -n "${DBX_CONTAINER_MANAGER}" ] && container_manager="${DBX_CONTAINER_MANAGER}" [ -n "${DBX_CONTAINER_RM_CUSTOM_HOME}" ] && rm_home="${DBX_CONTAINER_RM_CUSTOM_HOME}" [ -n "${DBX_NON_INTERACTIVE}" ] && non_interactive="${DBX_NON_INTERACTIVE}" [ -n "${DBX_VERBOSE}" ] && verbose="${DBX_VERBOSE}" # Fixup variable=[true|false], in case we find it in the config file(s) [ "${non_interactive}" = "true" ] && non_interactive=1 [ "${non_interactive}" = "false" ] && non_interactive=0 [ "${verbose}" = "true" ] && verbose=1 [ "${verbose}" = "false" ] && verbose=0 # If we're running this script as root - as in logged in in the shell as root # user, and not via SUDO/DOAS -, we don't need to set distrobox_sudo_program # as it's meaningless for this use case. if [ "$(id -ru)" -ne 0 ]; then # If the DBX_SUDO_PROGRAM/distrobox_sudo_program variable was set by the # user, use its value instead of "sudo". But only if not running the script # as root (UID 0). distrobox_sudo_program=${DBX_SUDO_PROGRAM:-${distrobox_sudo_program:-"sudo"}} fi # Declare it AFTER config sourcing because we do not want a default name set for rm. container_name_default="my-distrobox" container_name_list="" # show_help will print usage to stdout. # Arguments: # None # Expected global variables: # version: distrobox version # Expected env variables: # None # Outputs: # print usage with examples. show_help() { cat << EOF distrobox version: ${version} Usage: distrobox-rm [-f/--force] container-name [container-name1 container-name2 ...] Options: --all/-a: delete all distroboxes --force/-f: force deletion --rm-home: remove the mounted home if it differs from the host user's one --root/-r: launch podman/docker/lilipod with root privileges. Note that if you need root this is the preferred way over "sudo distrobox" (note: if using a program other than 'sudo' for root privileges is necessary, specify it through the DBX_SUDO_PROGRAM env variable, or 'distrobox_sudo_program' config variable) --help/-h: show this message --verbose/-v: show more verbosity --version/-V: show version EOF } # Parse arguments while :; do case $1 in -h | --help) # Call a "show_help" function to display a synopsis, then exit. show_help exit 0 ;; -a | --all) shift all=1 ;; -r | --root) shift rootful=1 ;; --rm-home) shift rm_home=1 ;; -v | --verbose) verbose=1 shift ;; -V | --version) printf "distrobox: %s\n" "${version}" exit 0 ;; -f | --force) force=1 non_interactive=1 shift ;; -Y | --yes) non_interactive=1 shift ;; --) # End of all options. shift break ;; -*) # Invalid options. printf >&2 "ERROR: Invalid flag '%s'\n\n" "$1" show_help exit 1 ;; *) # Default case: If no more options then break out of the loop. # If we have a flagless option and container_name is not specified # then let's accept argument as container_name if [ -n "$1" ]; then container_name_list="${container_name_list} $1" shift else break fi ;; esac done set -o errexit set -o nounset # set verbosity if [ "${verbose}" -ne 0 ]; then set -o xtrace fi # We depend on a container manager let's be sure we have it # First we use podman, else docker, else lilipod case "${container_manager}" in autodetect) if command -v podman > /dev/null; then container_manager="podman" elif command -v podman-launcher > /dev/null; then container_manager="podman-launcher" elif command -v docker > /dev/null; then container_manager="docker" elif command -v lilipod > /dev/null; then container_manager="lilipod" fi ;; podman) container_manager="podman" ;; podman-launcher) container_manager="podman-launcher" ;; lilipod) container_manager="lilipod" ;; docker) container_manager="docker" ;; *) printf >&2 "Invalid input %s.\n" "${container_manager}" printf >&2 "The available choices are: 'autodetect', 'podman', 'docker', 'lilipod'\n" ;; esac # Be sure we have a container manager to work with. if ! command -v "${container_manager}" > /dev/null; then # Error: we need at least one between docker, podman or lilipod. printf >&2 "Missing dependency: we need a container manager.\n" printf >&2 "Please install one of podman, docker or lilipod.\n" printf >&2 "You can follow the documentation on:\n" printf >&2 "\tman distrobox-compatibility\n" printf >&2 "or:\n" printf >&2 "\thttps://github.com/89luca89/distrobox/blob/main/docs/compatibility.md\n" exit 127 fi # add verbose if -v is specified if [ "${verbose}" -ne 0 ]; then container_manager="${container_manager} --log-level debug" fi # add -f if force is specified if [ "${force}" -ne 0 ]; then force_flag="--force" fi # prepend sudo (or the specified sudo program) if we want our container manager to be rootful if [ "${rootful}" -ne 0 ]; then container_manager="${distrobox_sudo_program-} ${container_manager}" distrobox_flags="--root" fi # If all, just set container_name to the list of names in distrobox-list if [ "${all}" -ne 0 ]; then # prepend sudo (or the specified sudo program) if we want our container manager to be rootful # shellcheck disable=SC2086,2248 container_name_list="$("${distrobox_path}"/distrobox-list ${distrobox_flags} --no-color | tail -n +2 | cut -d'|' -f2 | tr -d ' ' | tr '\n' ' ')" fi if [ -z "${container_name_list}" ] && [ "${all}" -ne 0 ]; then printf >&2 "No containers found.\n" exit 0 fi # check if we have containers to delete if [ -z "${container_name_list}" ]; then container_name_list="${container_name_default}" fi # cleanup_exports will remove exported apps and bins for container to delete. # Arguments: # container_name: string container name # Expected global variables: # distrobox_flags: string additional distrobox flags to use # Expected env variables: # None # Outputs: # None cleanup_exports() { container_name="$1" IFS='¤' printf "Removing exported binaries...\n" binary_files="$(grep -rl "# distrobox_binary" "${HOME}/.local/bin" 2> /dev/null | sed 's/./\\&/g' | xargs -I{} grep -le "# name: ${container_name}$" "{}" | sed 's/./\\&/g' | xargs -I{} printf "%s¤" "{}" 2> /dev/null || :)" for file in ${binary_files}; do printf "Removing exported binary %s...\n" "${file}" rm -f "${file}" done # Remove exported gui apps from this container in default path # shellcheck disable=SC2086,SC2038 desktop_files="$(find "${HOME}/.local/share/applications/${container_name}"* -type f -o -type l 2> /dev/null | sed 's/./\\&/g' | xargs -I{} grep -le "Exec=.*${container_name} " "{}" | sed 's/./\\&/g' | xargs -I{} printf "%s¤" "{}" 2> /dev/null || :)" for file in ${desktop_files}; do if [ -e "${file}" ]; then app="$(grep -Eo "Name=.*" "${file}" | head -n 1 | cut -d'=' -f2)" icon="$(grep -Eo "Icon=.*" "${file}" | head -n 1 | cut -d'=' -f2)" printf "Removing exported app %s...\n" "${app}" rm -f "${file}" find "${HOME}/.local/share/icons" -name "${icon}.*" -delete fi done unset IFS } # delete_container will remove input container # Arguments: # container_name: string container name # Expected global variables: # container_manager: string container manager to use # distrobox_flags: string distrobox additional flags # non_interactive: bool non interactive mode # force_flag: bool force mode # rm_home: bool remove home # verbose: bool verbose # Expected env variables: # None # Outputs: # None delete_container() { container_name="$1" # Inspect the container we're working with. container_status="$(${container_manager} inspect --type container \ --format '{{.State.Status}}' "${container_name}" || :)" # Does the container exist? check if inspect reported errors if [ -z "${container_status}" ]; then # If not, prompt to create it first printf >&2 "Cannot find container %s.\n" "${container_name}" return fi # Retrieve container's HOME, and check if it's different from host's one. In # this case we prompt for deletion of the custom home. container_home=$(${container_manager} inspect --type container --format \ '{{range .Config.Env}}{{if and (ge (len .) 5) (eq (slice . 0 5) "HOME=")}}{{slice . 5}}{{end}}{{end}}' "${container_name}") # Prompt for confirmation if [ "${container_home}" != "${HOME}" ]; then if [ "${non_interactive}" -eq 0 ] && [ "${rm_home}" -eq 1 ]; then printf "Do you want to remove custom home of container %s (%s)? [y/N]: " "${container_name}" "${container_home}" read -r response_rm_home response_rm_home="${response_rm_home:-"N"}" fi fi # Validate home response # Accept only y,Y,Yes,yes,n,N,No,no. case "${response_rm_home}" in y | Y | Yes | yes | YES) rm_home_local=1 ;; n | N | No | no | NO) rm_home_local=0 ;; *) # Default case: If no more options then break out of the loop. printf >&2 "Invalid input.\n" printf >&2 "The available choices are: y,Y,Yes,yes,YES or n,N,No,no,NO.\nExiting.\n" exit 1 ;; esac # Remove the container printf "Removing container...\n" # shellcheck disable=SC2086,SC2248 ${container_manager} rm ${force_flag} --volumes "${container_name}" # Remove exported apps and bins cleanup_exports "${container_name}" # We're going to delete the box, let's also delete the entry verbose_arg="" if [ "${verbose}" -ne 0 ]; then verbose_arg="--verbose" fi "$(dirname "$(realpath "${0}")")/distrobox-generate-entry" "${container_name}" --delete "${verbose_arg}" # Remove custom home if [ "${rm_home_local}" -eq 1 ]; then rm -r "${container_home}" printf "Successfully removed %s\n" "${container_home}" fi } # Prompt for confirmation if [ "${non_interactive}" -eq 0 ] && [ "${force}" -eq 0 ]; then printf "Do you really want to delete containers:%s? [Y/n]: " "${container_name_list}" read -r response response="${response:-"Y"}" else response="yes" fi for container in ${container_name_list}; do if [ "$(${container_manager} inspect --type container --format '{{.State.Status}}' "${container}")" = "running" ]; then if [ "${non_interactive}" -eq 0 ] && [ "${force}" -eq 0 ]; then printf "Container %s running, do you want to force delete them? [Y/n]: " "${container_name_list}" read -r response_force response_force="${response_force:-"Y"}" else response_force="yes" fi fi # Accept only y,Y,Yes,yes,n,N,No,no. case "${response_force:-"N"}" in y | Y | Yes | yes | YES) force=1 force_flag="--force" break ;; n | N | No | no | NO) ;; *) # Default case: If no more options then break out of the loop. printf >&2 "Invalid input.\n" printf >&2 "The available choices are: y,Y,Yes,yes,YES or n,N,No,no,NO.\nExiting.\n" ;; esac done # Accept only y,Y,Yes,yes,n,N,No,no. case "${response}" in y | Y | Yes | yes | YES) for container in ${container_name_list}; do delete_container "${container}" done ;; n | N | No | no | NO) printf "Aborted.\n" exit 0 ;; *) # Default case: If no more options then break out of the loop. printf >&2 "Invalid input.\n" printf >&2 "The available choices are: y,Y,Yes,yes,YES or n,N,No,no,NO.\nExiting.\n" exit 1 ;; esac