Skip to content

Instantly share code, notes, and snippets.

@Pymmdrza
Created May 3, 2026 12:02
Show Gist options
  • Select an option

  • Save Pymmdrza/4caf59a02900d14f952c1108a8a6321d to your computer and use it in GitHub Desktop.

Select an option

Save Pymmdrza/4caf59a02900d14f952c1108a8a6321d to your computer and use it in GitHub Desktop.
Ubuntu App Uninstaller Shell
#!/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