From 42c79870acd36917b5fd2c682e2fff99a1384d18 Mon Sep 17 00:00:00 2001 From: scriptos Date: Wed, 18 Feb 2026 23:09:16 +0100 Subject: [PATCH] Initialer Release (v1.0.0) --- LICENSE | 2 +- README.md | 62 ++++- docker-image-manager.v1.sh | 480 +++++++++++++++++++++++++++++++++++++ 3 files changed, 540 insertions(+), 4 deletions(-) create mode 100644 docker-image-manager.v1.sh diff --git a/LICENSE b/LICENSE index 11c2c16..2220be8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 scriptos +Copyright (c) 2026 Patrick Asmus (scriptos) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index ffe6696..bce890f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,65 @@ -# template_repository +# Docker Image Manager +Ein interaktives Bash-Script zum Anzeigen, Filtern und Loeschen von Docker Images. Es listet alle Images inklusive zugeordneter Container, zeigt ungenutzte Images und erlaubt das gezielte Loeschen per Nummer oder den sicheren Massenvorgang per Parameter. +## Voraussetzungen -Wichtig: Link für Lizenz anpassen. +- Docker installiert und laeuft +- Bash (unter Windows z.B. via WSL oder Git Bash) +## Nutzung + +```bash +bash ./docker-image-manager.v1.sh +``` + +Ohne Parameter startet das interaktive Menu. Dort kannst du Images anzeigen und optional direkt Details zu einem Image abrufen. + +### Befehle + +- `list` - Alle Docker Images auflisten (inkl. zugeordneter Container) +- `unused-images` - Images anzeigen, die von keinem Container genutzt werden +- `inspect` - Details zu einem Image anzeigen (Auswahl per Nummer, inkl. Containerliste) +- `delete` - Einzelne Images nach Nummer loeschen +- `purge-unused` - Alle ungenutzten Images loeschen +- `help` / `-h` / `--help` - Hilfe anzeigen + +### Optionen + +- `--yes` - Loeschen ohne Rueckfrage + +## Beispiele + +Alle Images anzeigen: + +```bash +bash ./docker-image-manager.v1.sh list +``` + +Alle ungenutzten Images loeschen (mit Rueckfrage): + +```bash +bash ./docker-image-manager.v1.sh purge-unused +``` + +Ein Image nach Nummer loeschen: + +```bash +bash ./docker-image-manager.v1.sh delete 3 +``` + +Image Details inkl. zugeordneter Container anzeigen: + +```bash +bash ./docker-image-manager.v1.sh inspect 2 +``` + +## Hinweise + +- Ungenutzte Images werden ueber alle Container ermittelt (inkl. gestoppter Container). +- Ungetaggte (dangling) Images haben in der Liste die Tags `:` und tauchen bei "ungenutzt" auf, wenn kein Container sie nutzt. +- In der Liste zeigt die Spalte `CONTAINERS`, welche Container das Image verwenden. +- Das Script loescht keine Images, die noch von Containern genutzt werden (weder einzeln noch als Massenloeschung). In diesem Fall werden die Container angezeigt und das Image uebersprungen.

@@ -11,5 +67,5 @@ Wichtig: Link für Lizenz anpassen.

-License License | Matrix Matrix | Matrix Mastodon +License License | Matrix Matrix | Matrix Mastodon

\ No newline at end of file diff --git a/docker-image-manager.v1.sh b/docker-image-manager.v1.sh new file mode 100644 index 0000000..871e523 --- /dev/null +++ b/docker-image-manager.v1.sh @@ -0,0 +1,480 @@ +#!/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" == ":" || -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 "$@" -- 2.49.1