Created
May 3, 2026 12:02
-
-
Save Pymmdrza/4caf59a02900d14f952c1108a8a6321d to your computer and use it in GitHub Desktop.
Ubuntu App Uninstaller Shell
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| APP="" | |
| ASSUME_YES=0 | |
| DRY_RUN=0 | |
| REMOVE_DOCKER=0 | |
| usage() { | |
| cat <<'EOF' | |
| Usage: | |
| sudo bash remover.sh <program-name> [options] | |
| Options: | |
| --dry-run Show what would be done, but do not delete anything | |
| --yes Do not ask for confirmation | |
| --docker Also remove Docker containers, images, and volumes related to the program | |
| --help Show this help message | |
| Examples: | |
| sudo bash remover.sh n8n --dry-run | |
| sudo bash remover.sh n8n | |
| sudo bash remover.sh n8n --docker | |
| EOF | |
| } | |
| if [[ $# -lt 1 ]]; then | |
| usage | |
| exit 1 | |
| fi | |
| APP="$1" | |
| shift | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --dry-run) | |
| DRY_RUN=1 | |
| ;; | |
| --yes) | |
| ASSUME_YES=1 | |
| ;; | |
| --docker) | |
| REMOVE_DOCKER=1 | |
| ;; | |
| --help) | |
| usage | |
| exit 0 | |
| ;; | |
| *) | |
| echo "Error: Unknown option: $1" | |
| usage | |
| exit 1 | |
| ;; | |
| esac | |
| shift | |
| done | |
| if [[ ! "$APP" =~ ^[A-Za-z0-9][A-Za-z0-9._+-]*$ ]]; then | |
| echo "Error: Invalid program name." | |
| echo "Allowed characters: letters, numbers, dot, underscore, plus, and hyphen." | |
| exit 1 | |
| fi | |
| APP_LOWER="${APP,,}" | |
| PROTECTED_NAMES=( | |
| bash sh dash zsh fish sudo su apt apt-get dpkg systemd systemctl journalctl | |
| rm mv cp chmod chown chgrp mkdir rmdir ln find grep sed awk tar gzip xz | |
| python python3 pip pip3 perl ruby node npm coreutils libc6 linux-image | |
| ubuntu-minimal openssh-server ssh passwd login | |
| ) | |
| for protected in "${PROTECTED_NAMES[@]}"; do | |
| if [[ "$APP_LOWER" == "$protected" ]]; then | |
| echo "Error: Refusing to remove protected system component: $APP" | |
| exit 1 | |
| fi | |
| done | |
| if [[ "$(id -u)" -eq 0 ]]; then | |
| SUDO=() | |
| else | |
| if ! command -v sudo >/dev/null 2>&1; then | |
| echo "Error: sudo is required when not running as root." | |
| exit 1 | |
| fi | |
| SUDO=(sudo) | |
| fi | |
| TARGET_USER="${SUDO_USER:-$(id -un)}" | |
| TARGET_HOME="$(getent passwd "$TARGET_USER" | cut -d: -f6 || true)" | |
| if [[ -z "$TARGET_HOME" ]]; then | |
| TARGET_HOME="${HOME:-}" | |
| fi | |
| print_command() { | |
| printf 'Running:' | |
| printf ' %q' "$@" | |
| printf '\n' | |
| } | |
| run_or_warn() { | |
| print_command "$@" | |
| if [[ "$DRY_RUN" -eq 1 ]]; then | |
| echo "Dry run: command skipped." | |
| return 0 | |
| fi | |
| if ! "$@"; then | |
| echo "Warning: Command failed and the script will continue." | |
| fi | |
| return 0 | |
| } | |
| run_sudo() { | |
| run_or_warn "${SUDO[@]}" "$@" | |
| } | |
| shell_quote() { | |
| printf '%q' "$1" | |
| } | |
| run_as_target_user() { | |
| local cmd="$1" | |
| echo "Running as user '$TARGET_USER': $cmd" | |
| if [[ "$DRY_RUN" -eq 1 ]]; then | |
| echo "Dry run: command skipped." | |
| return 0 | |
| fi | |
| if [[ "$(id -u)" -eq 0 && "$TARGET_USER" != "root" ]]; then | |
| if ! sudo -H -u "$TARGET_USER" bash -lc "$cmd"; then | |
| echo "Warning: User-level command failed and the script will continue." | |
| fi | |
| else | |
| if ! bash -lc "$cmd"; then | |
| echo "Warning: User-level command failed and the script will continue." | |
| fi | |
| fi | |
| return 0 | |
| } | |
| confirm() { | |
| local question="$1" | |
| if [[ "$ASSUME_YES" -eq 1 ]]; then | |
| echo "Automatic confirmation enabled: $question" | |
| return 0 | |
| fi | |
| local answer | |
| read -r -p "$question [y/N]: " answer | |
| case "$answer" in | |
| y|Y|yes|YES|Yes) | |
| return 0 | |
| ;; | |
| *) | |
| return 1 | |
| ;; | |
| esac | |
| } | |
| section() { | |
| echo | |
| echo "============================================================" | |
| echo "$1" | |
| echo "============================================================" | |
| } | |
| unit_exists() { | |
| local unit="$1" | |
| systemctl list-unit-files --no-legend "$unit" 2>/dev/null | grep -q . && return 0 | |
| systemctl list-units --all --no-legend "$unit" 2>/dev/null | grep -q . && return 0 | |
| return 1 | |
| } | |
| stop_systemd_units() { | |
| section "Stage 1: Stopping and disabling related systemd units" | |
| if ! command -v systemctl >/dev/null 2>&1; then | |
| echo "systemctl was not found. Skipping systemd cleanup." | |
| return 0 | |
| fi | |
| local units=( | |
| "${APP}.service" | |
| "${APP}@.service" | |
| "${APP}.timer" | |
| "${APP}.socket" | |
| ) | |
| local found=0 | |
| for unit in "${units[@]}"; do | |
| if unit_exists "$unit"; then | |
| found=1 | |
| echo "Found systemd unit: $unit" | |
| run_sudo systemctl stop "$unit" | |
| run_sudo systemctl disable "$unit" | |
| fi | |
| done | |
| if [[ "$found" -eq 0 ]]; then | |
| echo "No matching systemd units were found." | |
| fi | |
| } | |
| remove_apt_packages() { | |
| section "Stage 2: Removing APT packages" | |
| if ! command -v dpkg-query >/dev/null 2>&1 || ! command -v apt-get >/dev/null 2>&1; then | |
| echo "APT/dpkg was not found. Skipping APT cleanup." | |
| return 0 | |
| fi | |
| mapfile -t apt_packages < <( | |
| dpkg-query -W -f='${binary:Package}\n' 2>/dev/null | | |
| awk -v app="$APP" ' | |
| { | |
| pkg=tolower($0) | |
| a=tolower(app) | |
| if (pkg == a || index(pkg, a "-") == 1 || index(pkg, a "_") == 1 || index(pkg, a ".") == 1) { | |
| print $0 | |
| } | |
| } | |
| ' | | |
| sort -u | |
| ) | |
| if [[ "${#apt_packages[@]}" -eq 0 ]]; then | |
| echo "No matching APT packages were found." | |
| return 0 | |
| fi | |
| echo "Matching APT packages:" | |
| printf ' %s\n' "${apt_packages[@]}" | |
| if confirm "Purge these APT packages and their configuration files?"; then | |
| run_sudo apt-get -y purge "${apt_packages[@]}" | |
| run_sudo apt-get -y autoremove --purge | |
| run_sudo apt-get -y autoclean | |
| else | |
| echo "APT package removal skipped by user." | |
| fi | |
| } | |
| remove_snap_packages() { | |
| section "Stage 3: Removing Snap packages" | |
| if ! command -v snap >/dev/null 2>&1; then | |
| echo "Snap was not found. Skipping Snap cleanup." | |
| return 0 | |
| fi | |
| mapfile -t snap_packages < <( | |
| snap list 2>/dev/null | | |
| awk -v app="$APP" ' | |
| NR > 1 { | |
| name=tolower($1) | |
| a=tolower(app) | |
| if (name == a || index(name, a) == 1) { | |
| print $1 | |
| } | |
| } | |
| ' | | |
| sort -u | |
| ) | |
| if [[ "${#snap_packages[@]}" -eq 0 ]]; then | |
| echo "No matching Snap packages were found." | |
| return 0 | |
| fi | |
| echo "Matching Snap packages:" | |
| printf ' %s\n' "${snap_packages[@]}" | |
| if confirm "Remove these Snap packages with purge?"; then | |
| for pkg in "${snap_packages[@]}"; do | |
| run_sudo snap remove --purge "$pkg" | |
| done | |
| else | |
| echo "Snap package removal skipped by user." | |
| fi | |
| } | |
| remove_flatpak_packages() { | |
| section "Stage 4: Removing Flatpak applications" | |
| if ! command -v flatpak >/dev/null 2>&1; then | |
| echo "Flatpak was not found. Skipping Flatpak cleanup." | |
| return 0 | |
| fi | |
| mapfile -t flatpak_system_apps < <( | |
| flatpak list --app --columns=application 2>/dev/null | | |
| awk -v app="$APP" ' | |
| { | |
| id=tolower($0) | |
| a=tolower(app) | |
| if (index(id, a) > 0) { | |
| print $0 | |
| } | |
| } | |
| ' | | |
| sort -u | |
| ) | |
| if [[ "${#flatpak_system_apps[@]}" -gt 0 ]]; then | |
| echo "Matching system Flatpak applications:" | |
| printf ' %s\n' "${flatpak_system_apps[@]}" | |
| if confirm "Uninstall these system Flatpak applications and delete their data?"; then | |
| for app_id in "${flatpak_system_apps[@]}"; do | |
| run_sudo flatpak uninstall -y --delete-data "$app_id" | |
| done | |
| else | |
| echo "System Flatpak removal skipped by user." | |
| fi | |
| else | |
| echo "No matching system Flatpak applications were found." | |
| fi | |
| local quoted_app | |
| quoted_app="$(shell_quote "$APP")" | |
| run_as_target_user "if command -v flatpak >/dev/null 2>&1; then flatpak list --user --app --columns=application 2>/dev/null | awk -v app=$quoted_app '{ id=tolower(\$0); a=tolower(app); if (index(id, a) > 0) print \$0 }' | while read -r id; do echo \"Removing user Flatpak: \$id\"; flatpak uninstall --user -y --delete-data \"\$id\"; done; fi" | |
| } | |
| remove_language_manager_installs() { | |
| section "Stage 5: Removing npm, pnpm, yarn, pipx, and uv tool installs" | |
| local quoted_app | |
| quoted_app="$(shell_quote "$APP")" | |
| if command -v npm >/dev/null 2>&1; then | |
| run_or_warn npm uninstall -g -- "$APP" | |
| else | |
| echo "npm was not found in the current environment." | |
| fi | |
| run_as_target_user "if command -v npm >/dev/null 2>&1; then npm uninstall -g -- $quoted_app; fi" | |
| if command -v pnpm >/dev/null 2>&1; then | |
| run_or_warn pnpm remove -g "$APP" | |
| else | |
| echo "pnpm was not found in the current environment." | |
| fi | |
| run_as_target_user "if command -v pnpm >/dev/null 2>&1; then pnpm remove -g $quoted_app; fi" | |
| if command -v yarn >/dev/null 2>&1; then | |
| run_or_warn yarn global remove "$APP" | |
| else | |
| echo "yarn was not found in the current environment." | |
| fi | |
| run_as_target_user "if command -v yarn >/dev/null 2>&1; then yarn global remove $quoted_app; fi" | |
| if command -v pipx >/dev/null 2>&1; then | |
| run_or_warn pipx uninstall "$APP" | |
| else | |
| echo "pipx was not found in the current environment." | |
| fi | |
| run_as_target_user "if command -v pipx >/dev/null 2>&1; then pipx uninstall $quoted_app; fi" | |
| if command -v uv >/dev/null 2>&1; then | |
| run_or_warn uv tool uninstall "$APP" | |
| else | |
| echo "uv was not found in the current environment." | |
| fi | |
| run_as_target_user "if command -v uv >/dev/null 2>&1; then uv tool uninstall $quoted_app; fi" | |
| } | |
| remove_docker_resources() { | |
| section "Stage 6: Removing Docker resources" | |
| if [[ "$REMOVE_DOCKER" -ne 1 ]]; then | |
| echo "Docker cleanup was not requested. Use --docker to enable it." | |
| return 0 | |
| fi | |
| if ! command -v docker >/dev/null 2>&1; then | |
| echo "Docker was not found. Skipping Docker cleanup." | |
| return 0 | |
| fi | |
| mapfile -t docker_containers < <( | |
| docker ps -a --format '{{.ID}} {{.Names}}' 2>/dev/null | | |
| awk -v app="$APP" ' | |
| { | |
| name=tolower($2) | |
| a=tolower(app) | |
| if (index(name, a) > 0) { | |
| print $1 | |
| } | |
| } | |
| ' | | |
| sort -u | |
| ) | |
| mapfile -t docker_images < <( | |
| docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}' 2>/dev/null | | |
| awk -v app="$APP" ' | |
| { | |
| repo=tolower($1) | |
| a=tolower(app) | |
| if (index(repo, a) > 0) { | |
| print $2 | |
| } | |
| } | |
| ' | | |
| sort -u | |
| ) | |
| mapfile -t docker_volumes < <( | |
| docker volume ls -q 2>/dev/null | | |
| awk -v app="$APP" ' | |
| { | |
| vol=tolower($0) | |
| a=tolower(app) | |
| if (index(vol, a) > 0) { | |
| print $0 | |
| } | |
| } | |
| ' | | |
| sort -u | |
| ) | |
| if [[ "${#docker_containers[@]}" -eq 0 && "${#docker_images[@]}" -eq 0 && "${#docker_volumes[@]}" -eq 0 ]]; then | |
| echo "No matching Docker resources were found." | |
| return 0 | |
| fi | |
| if [[ "${#docker_containers[@]}" -gt 0 ]]; then | |
| echo "Matching Docker containers:" | |
| printf ' %s\n' "${docker_containers[@]}" | |
| fi | |
| if [[ "${#docker_images[@]}" -gt 0 ]]; then | |
| echo "Matching Docker images:" | |
| printf ' %s\n' "${docker_images[@]}" | |
| fi | |
| if [[ "${#docker_volumes[@]}" -gt 0 ]]; then | |
| echo "Matching Docker volumes:" | |
| printf ' %s\n' "${docker_volumes[@]}" | |
| fi | |
| if confirm "Remove these Docker resources? This may delete application data stored in Docker volumes."; then | |
| if [[ "${#docker_containers[@]}" -gt 0 ]]; then | |
| run_or_warn docker rm -f "${docker_containers[@]}" | |
| fi | |
| if [[ "${#docker_images[@]}" -gt 0 ]]; then | |
| run_or_warn docker rmi -f "${docker_images[@]}" | |
| fi | |
| if [[ "${#docker_volumes[@]}" -gt 0 ]]; then | |
| run_or_warn docker volume rm "${docker_volumes[@]}" | |
| fi | |
| else | |
| echo "Docker cleanup skipped by user." | |
| fi | |
| } | |
| PATHS_TO_REMOVE=() | |
| add_path() { | |
| local path="$1" | |
| if [[ -z "$path" ]]; then | |
| return 0 | |
| fi | |
| if [[ ! -e "$path" && ! -L "$path" ]]; then | |
| return 0 | |
| fi | |
| for existing in "${PATHS_TO_REMOVE[@]}"; do | |
| if [[ "$existing" == "$path" ]]; then | |
| return 0 | |
| fi | |
| done | |
| PATHS_TO_REMOVE+=("$path") | |
| } | |
| collect_residual_paths() { | |
| section "Stage 7: Searching for residual files and directories" | |
| PATHS_TO_REMOVE=() | |
| add_path "/etc/$APP" | |
| add_path "/etc/$APP.d" | |
| add_path "/etc/default/$APP" | |
| add_path "/etc/init.d/$APP" | |
| add_path "/var/lib/$APP" | |
| add_path "/var/log/$APP" | |
| add_path "/var/cache/$APP" | |
| add_path "/var/run/$APP" | |
| add_path "/run/$APP" | |
| add_path "/opt/$APP" | |
| add_path "/srv/$APP" | |
| add_path "/usr/local/bin/$APP" | |
| add_path "/usr/bin/$APP" | |
| add_path "/bin/$APP" | |
| add_path "/usr/local/sbin/$APP" | |
| add_path "/usr/sbin/$APP" | |
| add_path "/sbin/$APP" | |
| add_path "/usr/local/lib/node_modules/$APP" | |
| add_path "/usr/lib/node_modules/$APP" | |
| add_path "/usr/local/share/$APP" | |
| add_path "/usr/share/$APP" | |
| add_path "/snap/bin/$APP" | |
| add_path "/etc/systemd/system/${APP}.service" | |
| add_path "/etc/systemd/system/${APP}@.service" | |
| add_path "/etc/systemd/system/${APP}.timer" | |
| add_path "/etc/systemd/system/${APP}.socket" | |
| add_path "/lib/systemd/system/${APP}.service" | |
| add_path "/lib/systemd/system/${APP}@.service" | |
| add_path "/lib/systemd/system/${APP}.timer" | |
| add_path "/lib/systemd/system/${APP}.socket" | |
| add_path "/usr/lib/systemd/system/${APP}.service" | |
| add_path "/usr/lib/systemd/system/${APP}@.service" | |
| add_path "/usr/lib/systemd/system/${APP}.timer" | |
| add_path "/usr/lib/systemd/system/${APP}.socket" | |
| if [[ -n "$TARGET_HOME" && -d "$TARGET_HOME" ]]; then | |
| add_path "$TARGET_HOME/.$APP" | |
| add_path "$TARGET_HOME/.config/$APP" | |
| add_path "$TARGET_HOME/.cache/$APP" | |
| add_path "$TARGET_HOME/.local/share/$APP" | |
| add_path "$TARGET_HOME/.local/state/$APP" | |
| fi | |
| if [[ -d "/root" ]]; then | |
| add_path "/root/.$APP" | |
| add_path "/root/.config/$APP" | |
| add_path "/root/.cache/$APP" | |
| add_path "/root/.local/share/$APP" | |
| add_path "/root/.local/state/$APP" | |
| fi | |
| local binary_path | |
| binary_path="$(type -P "$APP" 2>/dev/null || true)" | |
| case "$binary_path" in | |
| /usr/local/bin/*|/usr/bin/*|/bin/*|/usr/local/sbin/*|/usr/sbin/*|/sbin/*|/opt/*|/snap/bin/*) | |
| add_path "$binary_path" | |
| ;; | |
| esac | |
| local scan_roots=( | |
| /etc | |
| /var/lib | |
| /var/log | |
| /var/cache | |
| /opt | |
| /srv | |
| /usr/local/share | |
| ) | |
| for root in "${scan_roots[@]}"; do | |
| if [[ -d "$root" ]]; then | |
| while IFS= read -r -d '' found_path; do | |
| add_path "$found_path" | |
| done < <( | |
| find "$root" -xdev -maxdepth 3 \ | |
| \( -iname "$APP" \ | |
| -o -iname ".$APP" \ | |
| -o -iname "$APP.d" \ | |
| -o -iname "$APP.service" \ | |
| -o -iname "$APP.timer" \ | |
| -o -iname "$APP.socket" \) \ | |
| -print0 2>/dev/null | |
| ) | |
| fi | |
| done | |
| if [[ "${#PATHS_TO_REMOVE[@]}" -eq 0 ]]; then | |
| echo "No obvious residual paths were found." | |
| return 0 | |
| fi | |
| echo "Residual paths found:" | |
| printf ' %s\n' "${PATHS_TO_REMOVE[@]}" | |
| } | |
| is_safe_to_delete() { | |
| local path="$1" | |
| case "$path" in | |
| ""|"/"|"/bin"|"/sbin"|"/usr"|"/usr/bin"|"/usr/sbin"|"/usr/local"|"/etc"|"/var"|"/home"|"/root"|"/opt"|"/srv") | |
| return 1 | |
| ;; | |
| esac | |
| if [[ "$path" != /* ]]; then | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| delete_residual_paths() { | |
| if [[ "${#PATHS_TO_REMOVE[@]}" -eq 0 ]]; then | |
| return 0 | |
| fi | |
| if confirm "Delete the residual paths listed above?"; then | |
| for path in "${PATHS_TO_REMOVE[@]}"; do | |
| if is_safe_to_delete "$path"; then | |
| run_sudo rm -rf --one-file-system -- "$path" | |
| else | |
| echo "Skipping unsafe path: $path" | |
| fi | |
| done | |
| if command -v systemctl >/dev/null 2>&1; then | |
| run_sudo systemctl daemon-reload | |
| run_sudo systemctl reset-failed | |
| fi | |
| else | |
| echo "Residual file deletion skipped by user." | |
| fi | |
| } | |
| remove_system_user_and_group() { | |
| section "Stage 8: Checking for matching Linux user and group" | |
| if getent passwd "$APP" >/dev/null 2>&1; then | |
| echo "Found Linux user: $APP" | |
| if confirm "Remove Linux user '$APP' and its home directory?"; then | |
| if command -v deluser >/dev/null 2>&1; then | |
| run_sudo deluser --remove-home "$APP" | |
| else | |
| run_sudo userdel -r "$APP" | |
| fi | |
| else | |
| echo "Linux user removal skipped by user." | |
| fi | |
| else | |
| echo "No matching Linux user was found." | |
| fi | |
| if getent group "$APP" >/dev/null 2>&1; then | |
| echo "Found Linux group: $APP" | |
| if confirm "Remove Linux group '$APP'?"; then | |
| run_sudo groupdel "$APP" | |
| else | |
| echo "Linux group removal skipped by user." | |
| fi | |
| else | |
| echo "No matching Linux group was found." | |
| fi | |
| } | |
| final_verification() { | |
| section "Stage 9: Final verification" | |
| local remaining=0 | |
| if type -P "$APP" >/dev/null 2>&1; then | |
| echo "Remaining command found:" | |
| type -P "$APP" | |
| remaining=1 | |
| else | |
| echo "No command named '$APP' was found in PATH." | |
| fi | |
| if command -v dpkg-query >/dev/null 2>&1; then | |
| if dpkg-query -W -f='${binary:Package}\n' 2>/dev/null | awk -v app="$APP" ' | |
| { | |
| pkg=tolower($0) | |
| a=tolower(app) | |
| if (pkg == a || index(pkg, a "-") == 1 || index(pkg, a "_") == 1 || index(pkg, a ".") == 1) { | |
| found=1 | |
| } | |
| } | |
| END { exit found ? 0 : 1 } | |
| '; then | |
| echo "Some matching APT packages may still be installed." | |
| remaining=1 | |
| else | |
| echo "No matching APT packages were found." | |
| fi | |
| fi | |
| if [[ "$remaining" -eq 0 ]]; then | |
| echo "Cleanup completed. No obvious installation remains were found by this script." | |
| else | |
| echo "Cleanup completed, but some items may still remain. Review the messages above." | |
| fi | |
| } | |
| section "Program removal started" | |
| echo "Program name: $APP" | |
| echo "Target user: $TARGET_USER" | |
| echo "Target home: ${TARGET_HOME:-unknown}" | |
| echo "Dry run: $DRY_RUN" | |
| echo "Docker cleanup: $REMOVE_DOCKER" | |
| stop_systemd_units | |
| remove_apt_packages | |
| remove_snap_packages | |
| remove_flatpak_packages | |
| remove_language_manager_installs | |
| remove_docker_resources | |
| collect_residual_paths | |
| delete_residual_paths | |
| remove_system_user_and_group | |
| final_verification | |
| section "Program removal finished" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment