Files
docker-image-manager/docker-image-manager.v1.sh
2026-02-18 23:09:16 +01:00

481 lines
11 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# Script Name: docker-image-manager.v1.sh
# Beschreibung: Docker Image Manager - verwaltet und inspiziert Docker Images
# - Alle Images auflisten
# - Ungenutzte Images anzeigen
# - Zeigt zugeordnete Container pro Image
# - Images interaktiv loeschen
# Autor: Patrick Asmus
# Web: https://www.cleveradmin.de
# Git-Reposit.: https://git.techniverse.net/scriptos/docker-image-manager
# Version: 1.0.0
# Datum: 18.02.2026
# Modifikation: Initialer Release (v1.0.0)
#####################################################
LAST_IDS=()
LAST_LABELS=()
declare -A IMAGE_TO_CONTAINERS=()
SELECTED_IDS=()
SELECTED_LABELS=()
require_docker() {
if ! command -v docker >/dev/null 2>&1; then
echo "Docker CLI nicht gefunden. Bitte Docker installieren und PATH pruefen." >&2
exit 1
fi
if ! docker info >/dev/null 2>&1; then
echo "Docker Daemon nicht erreichbar. Bitte Docker starten." >&2
exit 1
fi
}
load_container_map() {
IMAGE_TO_CONTAINERS=()
local -a lines
mapfile -t lines < <(docker ps -a -q | xargs -r docker inspect --format '{{.Id}}|{{.Image}}|{{.Name}}')
if [[ ${#lines[@]} -eq 0 ]]; then
return 0
fi
local line image_id name
for line in "${lines[@]}"; do
IFS='|' read -r _ image_id name <<< "$line"
name=${name#/}
if [[ -n "${IMAGE_TO_CONTAINERS[$image_id]:-}" ]]; then
IMAGE_TO_CONTAINERS[$image_id]="${IMAGE_TO_CONTAINERS[$image_id]},${name}"
else
IMAGE_TO_CONTAINERS[$image_id]="$name"
fi
done
}
print_images_table() {
if [[ ${#LAST_IDS[@]} -eq 0 ]]; then
echo "Keine Images gefunden."
return 0
fi
load_container_map
printf "%-4s %-30s %-20s %-15s %-12s %-20s %-30s\n" "Nr." "REPOSITORY" "TAG" "IMAGE ID" "SIZE" "CREATED" "CONTAINERS"
printf "%-4s %-30s %-20s %-15s %-12s %-20s %-30s\n" "----" "------------------------------" "--------------------" "---------------" "------------" "--------------------" "------------------------------"
for i in "${!LAST_IDS[@]}"; do
IFS='|' read -r repo tag short_id size created <<< "${LAST_LABELS[$i]}"
local containers="${IMAGE_TO_CONTAINERS[${LAST_IDS[$i]}]:-}"
if [[ -z "$containers" ]]; then
containers="-"
fi
printf "%-4s %-30s %-20s %-15s %-12s %-20s %-30s\n" "$((i + 1))" "$repo" "$tag" "$short_id" "$size" "$created" "$containers"
done
}
load_images_from_lines() {
local -a lines=("$@")
LAST_IDS=()
LAST_LABELS=()
if [[ ${#lines[@]} -eq 0 ]]; then
return 0
fi
local line id repo tag created size short_id
for line in "${lines[@]}"; do
IFS='|' read -r id repo tag created size <<< "$line"
short_id=${id:0:12}
LAST_IDS+=("$id")
LAST_LABELS+=("$repo|$tag|$short_id|$size|$created")
done
}
list_all_images() {
local -a lines
mapfile -t lines < <(docker images --no-trunc --format '{{.ID}}|{{.Repository}}|{{.Tag}}|{{.CreatedSince}}|{{.Size}}')
load_images_from_lines "${lines[@]}"
print_images_table
}
list_unused_images() {
local used_file
used_file=$(mktemp)
if docker ps -a -q >/dev/null 2>&1; then
docker ps -a -q | xargs -r docker inspect --format '{{.Image}}' | sort -u > "$used_file"
fi
local -a lines
mapfile -t lines < <(docker images --no-trunc --format '{{.ID}}|{{.Repository}}|{{.Tag}}|{{.CreatedSince}}|{{.Size}}')
LAST_IDS=()
LAST_LABELS=()
local line id repo tag created size short_id
for line in "${lines[@]}"; do
IFS='|' read -r id repo tag created size <<< "$line"
if [[ ! -s "$used_file" ]] || ! grep -Fxq "$id" "$used_file"; then
short_id=${id:0:12}
LAST_IDS+=("$id")
LAST_LABELS+=("$repo|$tag|$short_id|$size|$created")
fi
done
rm -f "$used_file"
print_images_table
}
confirm_action() {
local prompt="$1"
local answer
read -r -e -p "$prompt [y/N]: " answer
case "$answer" in
y|Y|yes|YES) return 0 ;;
*) return 1 ;;
esac
}
select_images_by_numbers() {
local -a numbers=()
local input="${1:-}"
SELECTED_IDS=()
SELECTED_LABELS=()
if [[ -z "$input" ]]; then
read -r -e -p "Nummern der Images (z.B. 1,3,5) oder Enter fuer Abbruch: " input
fi
if [[ -z "$input" ]]; then
echo "Abgebrochen."
return 1
fi
input=${input//,/ }
for token in $input; do
if [[ "$token" =~ ^[0-9]+$ ]]; then
numbers+=("$token")
fi
done
if [[ ${#numbers[@]} -eq 0 ]]; then
echo "Keine gueltigen Nummern angegeben."
return 1
fi
local idx
for idx in "${numbers[@]}"; do
if (( idx < 1 || idx > ${#LAST_IDS[@]} )); then
echo "Nummer $idx ist ungueltig."
return 1
fi
SELECTED_IDS+=("${LAST_IDS[$((idx - 1))]}")
SELECTED_LABELS+=("${LAST_LABELS[$((idx - 1))]}")
done
return 0
}
print_selected_images() {
if [[ ${#SELECTED_IDS[@]} -eq 0 ]]; then
return 0
fi
printf "%-4s %-30s %-20s %-15s %-12s %-20s\n" "Nr." "REPOSITORY" "TAG" "IMAGE ID" "SIZE" "CREATED"
printf "%-4s %-30s %-20s %-15s %-12s %-20s\n" "----" "------------------------------" "--------------------" "---------------" "------------" "--------------------"
local i
for i in "${!SELECTED_IDS[@]}"; do
IFS='|' read -r repo tag short_id size created <<< "${SELECTED_LABELS[$i]}"
printf "%-4s %-30s %-20s %-15s %-12s %-20s\n" "$((i + 1))" "$repo" "$tag" "$short_id" "$size" "$created"
done
}
remove_image_by_id() {
local id="$1"
local -a used
mapfile -t used < <(docker ps -a --filter "ancestor=$id" --format '{{.ID}}|{{.Names}}')
if [[ ${#used[@]} -gt 0 ]]; then
echo "Ueberspringe Image (in Benutzung durch Container): $id"
local entry
for entry in "${used[@]}"; do
echo "- ${entry#*|} (${entry%%|*})"
done
return
fi
local -a tags=()
mapfile -t tags < <(docker image inspect "$id" --format '{{range .RepoTags}}{{println .}}{{end}}')
if [[ ${#tags[@]} -eq 0 ]]; then
echo "Loesche Image: $id"
docker rmi "$id"
return
fi
local tag
local -a tag_list=()
for tag in "${tags[@]}"; do
if [[ "$tag" == "<none>:<none>" || -z "$tag" ]]; then
continue
fi
tag_list+=("$tag")
done
if [[ ${#tag_list[@]} -eq 0 ]]; then
echo "Loesche Image: $id"
docker rmi "$id"
return
fi
for tag in "${tag_list[@]}"; do
echo "Loesche Tag: $tag"
docker rmi "$tag"
done
if docker image inspect "$id" >/dev/null 2>&1; then
echo "Loesche Image: $id"
docker rmi "$id"
fi
}
delete_selected_images() {
local -A seen=()
local id
for id in "${SELECTED_IDS[@]}"; do
if [[ -n "${seen[$id]:-}" ]]; then
continue
fi
seen[$id]=1
remove_image_by_id "$id"
done
}
inspect_image_by_number() {
local num="${1:-}"
if [[ -z "$num" ]]; then
read -r -e -p "Nummer des Images: " num
fi
if ! [[ "$num" =~ ^[0-9]+$ ]]; then
echo "Ungueltige Nummer."
return 1
fi
if (( num < 1 || num > ${#LAST_IDS[@]} )); then
echo "Nummer $num ist ungueltig."
return 1
fi
local id
id="${LAST_IDS[$((num - 1))]}"
docker image inspect "$id" --format \
$'ID: {{.Id}}\nRepoTags: {{.RepoTags}}\nCreated: {{.Created}}\nSize: {{.Size}}\nArchitecture: {{.Architecture}}\nOS: {{.Os}}\nLabels: {{json .Config.Labels}}'
echo
echo "Containers:"
local -a lines
mapfile -t lines < <(docker ps -a --filter "ancestor=$id" --format '{{.ID}}|{{.Names}}|{{.Status}}')
if [[ ${#lines[@]} -eq 0 ]]; then
echo "- keine"
return 0
fi
local line cid name status
for line in "${lines[@]}"; do
IFS='|' read -r cid name status <<< "$line"
echo "- $name ($cid) - $status"
done
}
purge_images() {
local mode="$1"
local force_yes="$2"
case "$mode" in
unused)
list_unused_images
if [[ ${#LAST_IDS[@]} -eq 0 ]]; then
return 0
fi
;;
*)
echo "Unbekannter Modus: $mode"
return 1
;;
esac
if [[ ${#LAST_IDS[@]} -eq 0 ]]; then
return 0
fi
if [[ "$force_yes" != "true" ]]; then
if ! confirm_action "Alle aufgelisteten Images loeschen?"; then
echo "Abgebrochen."
return 0
fi
fi
local -A seen=()
local id
for id in "${LAST_IDS[@]}"; do
if [[ -n "${seen[$id]:-}" ]]; then
continue
fi
seen[$id]=1
remove_image_by_id "$id"
done
}
print_help() {
echo "Docker Image Manager"
echo
echo "Verwendung:"
echo " bash ./docker-image-manager.v1.sh [BEFEHL] [OPTIONS]"
echo
echo "Befehle:"
echo " list - Alle Docker Images auflisten"
echo " unused-images - Images anzeigen, die von keinem Container genutzt werden"
echo " inspect - Details zu einem Image anzeigen (inkl. Containerliste)"
echo " delete - Einzelne Images nach Nummer loeschen"
echo " purge-unused - Alle ungenutzten Images loeschen"
echo " help, -h, --help - Diese Hilfe anzeigen"
echo
echo "Optionen:"
echo " --yes - Loeschen ohne Rueckfrage"
}
interactive_menu() {
while true; do
echo
echo "Docker Image Manager"
echo "1) Images anzeigen + Details"
echo "2) Ungenutzte Images anzeigen"
echo "3) Image loeschen (per Nummer)"
echo "4) Alle ungenutzten Images loeschen"
echo "0) Beenden"
read -r -e -p "Auswahl: " choice
case "$choice" in
1)
list_all_images
if [[ ${#LAST_IDS[@]} -eq 0 ]]; then
continue
fi
read -r -e -p "Nummer fuer Details (Enter fuer zurueck): " detail_num
if [[ -n "$detail_num" ]]; then
inspect_image_by_number "$detail_num"
fi
;;
2)
list_unused_images
;;
3)
list_all_images
if [[ ${#LAST_IDS[@]} -eq 0 ]]; then
continue
fi
if ! select_images_by_numbers ""; then
continue
fi
echo "Ausgewaehlte Images:"
print_selected_images
if confirm_action "Ausgewaehlte Images loeschen?"; then
delete_selected_images
else
echo "Abgebrochen."
fi
;;
4)
purge_images "unused" "false"
;;
0)
echo "Tschuess."
exit 0
;;
*)
echo "Ungueltige Auswahl."
;;
esac
done
}
main() {
require_docker
local cmd=""
local force_yes="false"
local -a extra_args=()
for arg in "$@"; do
case "$arg" in
--yes)
force_yes="true"
;;
-h|--help|help)
cmd="help"
;;
*)
if [[ -z "$cmd" ]]; then
cmd="$arg"
else
extra_args+=("$arg")
fi
;;
esac
done
if [[ -z "$cmd" ]]; then
interactive_menu
return 0
fi
case "$cmd" in
list)
list_all_images
;;
unused-images)
list_unused_images
;;
inspect)
list_all_images
if [[ ${#LAST_IDS[@]} -eq 0 ]]; then
return 0
fi
inspect_image_by_number "${extra_args[0]:-}"
;;
delete)
list_all_images
if [[ ${#LAST_IDS[@]} -eq 0 ]]; then
return 0
fi
if ! select_images_by_numbers "${extra_args[*]:-}"; then
return 1
fi
echo "Ausgewaehlte Images:"
print_selected_images
if [[ "$force_yes" != "true" ]]; then
if ! confirm_action "Ausgewaehlte Images loeschen?"; then
echo "Abgebrochen."
return 0
fi
fi
delete_selected_images
;;
purge-unused)
purge_images "unused" "$force_yes"
;;
help|*)
print_help
;;
esac
}
main "$@"