<!--
  canonical-url: https://www.avonture.be/fr/blog/zsh-docker-functions
  published:     2026-03-02
  This is a static plain-Markdown mirror generated at build time.
  Visit the canonical URL above for the fully rendered page, with images and interactive components.
-->

# Fonctions ZSH - Personnaliser votre shell pour gérer Docker

> Une collection de fonctions ZSH pour améliorer votre expérience du terminal, avec la gestion interactive des containers Docker grâce à fzf. Ouvrez de nouvelles sessions, arrêtez des containers et bien plus, sans effort.

---
![Fonctions ZSH - Personnaliser votre shell pour gérer Docker](/img/v2/zsh.webp)

**TL;DR**

Améliorez votre workflow dans le terminal avec des fonctions ZSH personnalisées pour gérer Docker de façon interactive. Cet article présente des outils comme `dex`, `dstop` et `dnuke` qui s'appuient sur `fzf` pour accéder rapidement à un shell, arrêter des containers, consulter les logs et nettoyer les ressources, sans rien mémoriser.

Au quotidien, j'utilise quelques fonctions ZSH que je trouve bien pratiques. Ces fonctions peuvent être ajoutées à votre fichier `~/.zshrc` (ou, mieux, dans leur propre fichier — voir [Beyond the Monolith - Organizing Your ZSH Workflow Like a Pro](/blog/modular-zsh-workflow)) pour améliorer votre expérience du terminal quand vous travaillez avec Docker. Elles offrent des moyens rapides et interactifs de gérer vos containers Docker **sans devoir retenir des commandes précises ou des ID de containers** (et soyons honnêtes, on les oublie vite !).

Dans cet article, je partage quelques-unes des fonctions ZSH que j'utilise pour gérer Docker : ouvrir une nouvelle session de terminal dans un container en cours d'exécution, arrêter des containers, accéder aux logs (même si le container est arrêté) et nettoyer les ressources inutilisées. Ces fonctions s'appuient sur `fzf` pour une sélection interactive, ce qui rend la gestion de votre environnement Docker bien plus simple, directement depuis votre terminal.

**Quick Jump:** [Ouvrir une nouvelle session de terminal dans un container Docker en cours d'exécution](#start-a-new-terminal-session-in-a-running-docker-container) | [Comment l'installer](#how-to-install)

Comme ces fonctions reposent sur `fzf`, si vous ne l'avez pas installé, faites-le via votre gestionnaire de packages (par exemple `sudo apt-get install fzf` sur les systèmes basés sur Debian). Vous découvrez l'outil ? Commencez par lire [Introduction to fzf - Fuzzy Finder](/blog/linux-fzf-introduction).

## Ouvrir une nouvelle session de terminal dans un container Docker en cours d'exécution {#start-a-new-terminal-session-in-a-running-docker-container}

Imaginez que vous voulez accéder rapidement à un container Docker en cours d'exécution sans devoir retenir son nom ou son ID. La fonction `dex` fait exactement cela. Elle liste tous les containers en cours d'exécution et vous laisse en choisir un pour y ouvrir une nouvelle session de terminal.

Il suffit donc de lancer `dex` dans votre terminal pour accéder à n'importe lequel de vos containers Docker actifs. C'est particulièrement utile quand plusieurs containers tournent en même temps et que vous voulez sauter dans l'un d'eux sans chercher son nom ou son ID.

![Utiliser dex pour ouvrir une nouvelle session de terminal dans un container Docker en cours d'exécution](./images/dex.webp)

> **Modifier la commande avant de l'exécuter:**
>
> Appuyez sur Entrée pour sélectionner le container et démarrer la session, ou sur Ctrl+E pour récupérer la commande dans votre prompt et la modifier — pratique si vous voulez l'ajuster avant de la lancer, par exemple pour désactiver l'entrypoint ou monter un volume.

> **Accès root:**
>
> Appuyez sur `Ctrl`+`R` pendant la sélection d'un container avec `dex` pour ouvrir la session en root. Utile quand vous devez effectuer des tâches d'administration dans le container qui exigent des privilèges élevés.

> **Filtrage:**
>
> Lancez `dex any_pattern` pour ne garder dans la liste que les containers correspondant à ce motif.

## Arrêter un ou plusieurs containers Docker en cours d'exécution {#stop-one-or-more-running-docker-containers}

`dstop` est une autre fonction bien utile : elle permet d'arrêter un ou plusieurs containers Docker actifs. Comme `dex`, elle utilise `fzf` pour offrir une interface interactive de sélection des containers à arrêter. Vous pouvez en sélectionner plusieurs grâce à la multi-sélection de `fzf`, c'est-à-dire en appuyant sur `Tab` pour cocher plusieurs containers avant d'appuyer sur `Enter` pour les arrêter.

Appuyez sur `CTRL`+`A` pour sélectionner tous les containers d'un coup.

Dans le panneau de droite, vous voyez les informations du ou des containers sélectionnés : nom, image, statut et ports. Cela vous aide à décider en connaissance de cause lesquels arrêter. Vous voyez aussi la consommation de ressources en direct (CPU, mémoire, I/O réseau, I/O disque), de quoi repérer les containers les plus gourmands avant de les arrêter.

![Utiliser dstop pour arrêter un ou plusieurs containers Docker en cours d'exécution](./images/dstop.webp)

## Accéder aux logs d'un container Docker en cours d'exécution {#access-logs-of-a-running-docker-container}

La fonction `dlogs` permet d'accéder aux logs d'un container Docker actif. Elle utilise également `fzf` pour proposer une interface interactive de sélection du container dont vous voulez voir les logs.

Dans le panneau de droite, vous voyez les logs du container sélectionné en temps réel. Très pratique pour le debug : vous suivez les logs au fur et à mesure. La molette de votre souris permet de les faire défiler.

![La fonction dlogs pour accéder aux logs d'un container Docker en cours d'exécution](./images/dlogs.webp)

## Nettoyer les containers, images, volumes et réseaux Docker inutilisés {#clean-up-unused-docker-containers-images-volumes-and-networks}

La fonction dnuke joue le rôle d'un **assistant de nettoyage Docker** intelligent et interactif, conçu pour récupérer de l'espace disque sans risquer de supprimer des ressources à l'aveugle. Contrairement à un simple `docker system prune`, `dnuke` commence par analyser votre environnement et vous présente un **plan d'exécution** dynamique, qui montre exactement ce qui peut être nettoyé (containers arrêtés, images dangling, volumes inutilisés et cache de build).

Le nettoyage se fait par étapes fines, ce qui vous permet d'ignorer certaines catégories si besoin. Par défaut, la fonction reste en mode « safe » et ne cible que les images dangling (sans tag).

`dnuke` accepte des flags CLI pour adapter son comportement :

**Les flags de dnuke**

* **Nettoyage en profondeur** — `dnuke -a` (ou `--all`) supprime **toutes** les images inutilisées, pas seulement les dangling (comme `docker image prune -a`).

* **Non interactif** — `dnuke -y` (ou `--force`) court-circuite l'assistant et confirme automatiquement toutes les étapes.

* **Aide** — `dnuke --help` affiche la liste complète des options et des exemples d'utilisation.

Si l'environnement est déjà propre, l'assistant le détecte immédiatement et s'arrête sans poser de questions inutiles.

![Afficher l'aide de dnuke pour voir les options disponibles](./images/dnuke_help.webp)

![Lancer dnuke pour nettoyer les ressources Docker inutilisées](./images/dnuke.webp)

## Créer un nouveau container {#creating-a-new-container}

La dernière fonction est `drun` : elle affiche la liste des images existantes et crée un nouveau container à partir de celle que vous choisissez.

> **Filtrage:**
>
> Lancez `drun any_pattern` pour ne garder dans la liste que les images correspondant à ce motif.

## Une seule pour les gouverner toutes {#one-to-rule-them-all}

La fonction `dops` est un wrapper autour des autres fonctions. Elle vous donne accès rapidement à toutes les autres avec une seule commande. Lancez simplement `dops` dans votre terminal : la liste des fonctions disponibles s'affiche. Vous choisissez celle qui vous intéresse et elle exécute la commande correspondante.

Éditez maintenant votre fichier `~/.zshrc` et ajoutez la ligne suivante pour sourcer le fichier `docker.zsh` : `[[ -f ~/.zsh/docker.zsh ]] && source ~/.zsh/docker.zsh`. Les fonctions seront alors disponibles dans votre terminal.

![Utiliser dops pour accéder au menu interactif des fonctions de gestion Docker](./images/dops.webp)

> **Quand vous ne savez pas encore quel container:**
>
> `dex`, `dstop` et `dlogs` supposent que vous savez déjà quel container vous visez. Pour les moments où ce n'est pas le cas — cinq containers viennent de démarrer et il vous faut voir CPU, mémoire et logs côte à côte pour identifier le fautif — un dashboard vaut mieux qu'un menu. C'est le rôle de [lazydocker, lancé dans un container](/blog/lazydocker).

## Comment l'installer {#how-to-install}

Éditez votre fichier `~/.zshrc` et ajoutez les lignes suivantes à la fin :

```zsh
fpath=(~/.zsh/docker-fns $fpath)

autoload -Uz _d_check_env _d_print_cmd _d_has_running_containers
autoload -Uz dex dlogs dnuke dops drun dstop

# Setup alias for the interactive menu
alias d='dops'

# Print quick reference menu only in interactive sessions to avoid breaking scripts
if [[ -o interactive ]]; then
    echo -e "\033[1;30mDocker Utils loaded:\033[0m"
    echo -e "  \033[0;36mdex\033[0m   (exec/root) \033[0;36mdlogs\033[0m (logs/json)    \033[0;36mdstop\033[0m (stop/kill)"
    echo -e "  \033[0;36mdnuke\033[0m (clean)     \033[0;36mdops\033[0m  (menu/alias \033[1;32md\033[0m) \033[0;36mdrun\033[0m  (create new container)"
fi
```

Créez ensuite le répertoire `~/.zsh/docker-fns` et déplacez-y les fichiers de chaque fonction (`dex`, `dlogs`, `dnuke`, `dops`, `drun`, `dstop`). Vos fonctions restent ainsi organisées et faciles à maintenir — le même pattern `fpath`/`autoload` est détaillé dans [Beyond the Monolith - Organizing Your ZSH Workflow Like a Pro](/blog/modular-zsh-workflow).

**Project: \~/.zsh/docker-fns**

> **Note:**
>
> Éditez maintenant votre fichier `~/.zshrc` et ajoutez les lignes mentionnées plus haut dans l'article pour sourcer le fichier `docker.zsh` et rendre les fonctions disponibles dans votre terminal. Vous pourrez alors utiliser `dex`, `dlogs`, `dnuke`, `dops`, `drun` et `dstop` pour gérer vos containers Docker directement depuis le terminal.

**\_d\_check\_env**

```
#!/usr/bin/env zsh

# Check for binary dependencies and Docker daemon status
local dep
for dep in "$@"; do
    if ! command -v "$dep" >/dev/null 2>&1; then
        echo >&2 -e "\033[0;31mError:\033[0m '$dep' is not installed."
        return 1
    fi
done

# Fast daemon check to ensure Docker is responsive
if ! docker info >/dev/null 2>&1; then
    echo >&2 -e "\033[0;31mError:\033[0m Docker daemon is not running or accessible."
    return 1
fi
```

**\_d\_print\_cmd**

```
#!/usr/bin/env zsh

# Print executed commands with optional color formatting
local cmd_str="$1"
local color="${2:-\033[1;32m}" # Default to green
local reset_color="\033[0m"

# Truncate output if it exceeds 200 characters for readability
if [[ ${#cmd_str} -gt 200 ]]; then
    cmd_str="${cmd_str:0:197}..."
fi

echo -e "Command: ${color}${cmd_str}${reset_color}"
```

**\_d\_has\_running\_containers**

```
#!/usr/bin/env zsh

# Verify if at least one container is currently running
# Utilizing 'read' is faster than piping to 'grep'
if [[ -z "$(docker ps -q -n 1)" ]]; then
    echo >&2 -e "\033[0;33mInfo:\033[0m No running containers found."
    return 1
fi
return 0
```

**dex**

```
#!/usr/bin/env zsh

# Interactive Docker Exec utilizing fzf
_d_check_env fzf docker || return 1
_d_has_running_containers || return 0

local fzf_out key selection container_id cmd_exec user_flag query

# Multi-line preview command using Docker Go templates
# We use literal newlines here to ensure proper formatting inside fzf's subshell.
# It extracts standard info, health state, ports, and the Docker Compose working directory.
local preview_cmd='docker inspect --format "Container: {{.Name}}
----------------------------------------
Status:      {{.State.Status}}
Health:      {{if .State.Health}}{{.State.Health.Status}}{{else}}No Healthcheck{{end}}
Image:       {{.Config.Image}}
IP Address:  {{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}
Ports:       {{range \$p, \$conf := .NetworkSettings.Ports}}{{\$p}} {{end}}
Compose Dir: {{index .Config.Labels \"com.docker.compose.project.working_dir\"}}" {1}'

# Removed the --height parameter to allow a full-screen fzf experience
fzf_out=$(docker ps --format "{{.ID}}\t{{.Names}}\t{{.Image}}" | \
    fzf --layout=reverse --border --query="$query" \
        --header "Enter: Run | Ctrl-E: Edit | Ctrl-R: Run as Root" \
        --preview "$preview_cmd" --preview-window=right:50%:wrap \
        --expect=ctrl-e,ctrl-r)

# Exit if user canceled
[[ -z "$fzf_out" ]] && return 0

key=$(head -n 1 <<< "$fzf_out")
selection=$(tail -n +2 <<< "$fzf_out")
container_id=$(awk '{print $1}' <<< "$selection")

[[ -z "$container_id" ]] && return 0

# Toggle root user execution based on keypress
user_flag=""
if [[ "$key" == "ctrl-r" ]]; then
    user_flag="--user root"
fi

# Smart shell execution: attempts zsh, then bash, fallbacks to sh
local smart_shell="sh -c 'exec \$(command -v zsh || command -v bash || echo sh)'"
cmd_exec=$(echo "docker exec -it ${user_flag} ${container_id} ${smart_shell}" | tr -s ' ')

# Place command in buffer for editing or execute immediately
if [[ "$key" == "ctrl-e" ]]; then
    print -z "$cmd_exec"
else
    _d_print_cmd "$cmd_exec" "\033[1;36m"
    eval "$cmd_exec"
fi
```

**dlogs**

```
#!/usr/bin/env zsh

# Interactive Docker Logs Viewer
_d_check_env fzf docker || return 1

local fzf_out key selection container_id cmd_logs bat_cmd

# Determine the best available syntax highlighter (batcat for Debian/Ubuntu)
if command -v batcat >/dev/null 2>&1; then
    bat_cmd="batcat"
elif command -v bat >/dev/null 2>&1; then
    bat_cmd="bat"
fi

# Define the base preview command for container metadata
local preview_cmd='docker inspect --format "Container: {{.Name}}
Status:      {{.State.Status}}
Compose Dir: {{index .Config.Labels \"com.docker.compose.project.working_dir\"}}" {1}
echo "------------------------------------------------------------"
'

# Append the log fetching command to the preview with color support
if [[ -n "$bat_cmd" ]]; then
    # Use bat for rich, out-of-the-box log syntax highlighting
    preview_cmd+="docker logs --tail 50 {1} 2>&1 | $bat_cmd --language=log --color=always --style=plain"
else
    # Fallback to awk to manually colorize ERROR/FATAL (red) and WARN/WARNING (yellow)
    local awk_colors='{gsub(/ERROR|FATAL|ERR/,"\033[1;31m&\033[0m"); gsub(/WARNING|WARN/,"\033[1;33m&\033[0m"); print}'
    preview_cmd+="docker logs --tail 50 {1} 2>&1 | awk '$awk_colors'"
fi

# Full-screen fzf experience with an expanded right panel.
# CRITICAL: --ansi is required to render color codes inside the preview window.
fzf_out=$(docker ps -a --format "{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Image}}" | \
    fzf --ansi --layout=reverse --border \
        --header "Enter: View Logs | Ctrl-E: Edit Command" \
        --preview "$preview_cmd" --preview-window=right:60%:wrap \
        --expect=ctrl-e)

# Exit if user canceled
[[ -z "$fzf_out" ]] && return 0

key=$(head -n 1 <<< "$fzf_out")
selection=$(tail -n +2 <<< "$fzf_out")
container_id=$(awk '{print $1}' <<< "$selection")

[[ -z "$container_id" ]] && return 0

# Construct log command based on available tools (Priority: lnav > bat > standard)
if command -v lnav >/dev/null 2>&1; then
    cmd_logs="docker logs -f --tail 500 ${container_id} 2>&1 | lnav"
elif [[ -n "$bat_cmd" ]]; then
    cmd_logs="docker logs -f --tail 500 ${container_id} 2>&1 | ${bat_cmd} --language=log --style=plain --pager=\"less +F -R\""
else
    cmd_logs="docker logs -f --tail 500 ${container_id}"
fi

# Place command in buffer for editing or execute immediately
if [[ "$key" == "ctrl-e" ]]; then
    print -z "$cmd_logs"
else
    _d_print_cmd "$cmd_logs" "\033[1;36m"
    eval "$cmd_logs"
fi
```

**dnuke**

```
#!/usr/bin/env zsh

# Docker Cleanup Wizard
_d_check_env docker || return 1

local force=0
local prune_all=0

# Function to display usage instructions
local _show_help() {
    echo -e "\033[1;34mUsage: dnuke [OPTIONS]\033[0m"
    echo -e "Interactive Docker cleanup wizard to remove unused resources."
    echo -e ""
    echo -e "\033[1;33mOptions:\033[0m"
    echo -e "  \033[1;32m-a, --all\033[0m    Deep clean: Remove ALL unused images (not just dangling)."
    echo -e "  \033[1;32m-y, --force\033[0m  Non-interactive mode: Auto-confirm all steps."
    echo -e "  \033[1;32m-h, --help\033[0m   Show this help message."
    echo -e ""
    echo -e "\033[1;33mExamples:\033[0m"
    echo -e "  dnuke          # Standard cleanup (dangling images only)"
    echo -e "  dnuke -a       # Deep cleanup (all unused images)"
    echo -e "  dnuke -y       # Instant cleanup (no questions asked)"
}

# Parse arguments
while [[ $# -gt 0 ]]; do
    case $1 in
        -y|--force) force=1; shift ;;
        -a|--all) prune_all=1; shift ;;
        -h|--help) _show_help; return 0 ;;
        *) echo -e "\033[0;31mUnknown option: $1\033[0m"; _show_help; return 1 ;;
    esac
done

echo -e "\033[1;34m=== Docker Cleanup Wizard ===\033[0m"
echo -n "Scanning Docker environment... "

# 1. Gather data silently
local c_out v_out cache_size
c_out=$(docker ps -a -f "status=exited" -q)
v_out=$(docker volume ls -qf dangling=true)
cache_size=$(docker system df | awk '/Build Cache/ {print $5}') # $5 usually holds the size

# Safely populate containers and volumes arrays
local -a c_ids v_ids i_ids
[[ -n "$c_out" ]] && c_ids=("${(@f)c_out}")
[[ -n "$v_out" ]] && v_ids=("${(@f)v_out}")

local s_cont="pending" s_img="pending" s_vol="pending" s_cache="pending"
[[ ${#c_ids[@]} -eq 0 ]] && s_cont="none"
[[ ${#v_ids[@]} -eq 0 ]] && s_vol="none"
[[ "$cache_size" == "0B" || "$cache_size" == "0" || -z "$cache_size" ]] && s_cache="none"

local img_desc=""
local img_cmd=""

# Handle image logic: Dangling vs All Unused
if [[ $prune_all -eq 1 ]]; then
    # Mode: --all
    # Logic: Get ALL image IDs, subtract IDs used by containers (running or stopped)
    local -a all_imgs=("${(@f)$(docker images -q)}")
    local -a used_imgs=()

    # Get images used by containers
    local c_all_imgs
    c_all_imgs=$(docker ps -a --format "{{.Image}}")
    if [[ -n "$c_all_imgs" ]]; then
        # We need to resolve image names to IDs to perform accurate subtraction
        # (This might be slow on huge systems, but accurate)
        used_imgs=("${(@f)$(docker inspect --format='{{.Id}}' ${(f)c_all_imgs} 2>/dev/null | sed 's/sha256://' | cut -c1-12)}")
    fi

    # Zsh Array Subtraction: i_ids = all_imgs - used_imgs
    i_ids=(${all_imgs:|used_imgs})

    [[ ${#i_ids[@]} -eq 0 ]] && s_img="none"
    img_desc="Remove ${#i_ids[@]} unused image(s) (Deep Clean)"
    img_cmd="docker image prune -a -f"
else
    # Mode: Standard (Dangling only)
    local i_out=$(docker images -f "dangling=true" -q)
    [[ -n "$i_out" ]] && i_ids=("${(@f)i_out}")

    [[ ${#i_ids[@]} -eq 0 ]] && s_img="none"
    img_desc="Remove ${#i_ids[@]} dangling image(s)"
    img_cmd="docker rmi ${i_ids[*]}"
fi

echo -e "Done.\n"

# Helper function to display the dynamic roadmap (Execution Plan)
_draw_plan() {
    local current_step=$1
    echo -e "\033[1;36m[ Execution Plan ]\033[0m"

    local _fmt() {
        local step_name=$1
        local state=$2
        local desc=$3

        local prefix="  "
        local color="\033[1;30m" # Dark Gray
        local step_status="[Pending]"

        if [[ "$step_name" == "$current_step" ]]; then
            prefix="➜ "
            color="\033[1;37m" # White (Current step)
            step_status="[Next]   "
        elif [[ "$state" == "done" ]]; then
            color="\033[1;32m" # Green (Finished)
            step_status="[Done]   "
        elif [[ "$state" == "skipped" ]]; then
            color="\033[0;33m" # Yellow (Skipped manually)
            step_status="[Skipped]"
        elif [[ "$state" == "none" ]]; then
            color="\033[1;30m" # Dark Gray (Nothing to do)
            step_status="[Empty]  "
        fi

        echo -e "${prefix}${color}${step_status} ${desc}\033[0m"
    }

    _fmt "cont" "$s_cont" "Remove ${#c_ids[@]} stopped container(s)"
    _fmt "img" "$s_img" "$img_desc"
    _fmt "vol" "$s_vol" "Remove ${#v_ids[@]} unused volume(s)"
    _fmt "cache" "$s_cache" "Prune build cache (Size: ${cache_size:-0B})"
    echo "----------------------------------------"
}

# Fast-exit if the environment is already clean
if [[ "$s_cont" == "none" && "$s_img" == "none" && "$s_vol" == "none" && "$s_cache" == "none" ]]; then
    _draw_plan "finish"
    echo -e "\033[1;32mEnvironment is already clean. Nothing to do!\033[0m"
    return 0
fi

# Helper function to handle user prompts consistently
local _ask() {
    if [[ $force -eq 1 ]]; then return 0; fi
    if read -q "?Execute this step? (y/N): "; then
        echo "\n"
        return 0
    else
        echo -e "\n\033[0;33mSkipped.\033[0m\n"
        return 1
    fi
}

# --- Step 1: Stopped Containers ---
if [[ "$s_cont" == "pending" ]]; then
    _draw_plan "cont"
    docker ps -a -f "status=exited" --format "table {{.Names}}\t{{.Status}}\t{{.Image}}"
    echo ""
    _d_print_cmd "docker rm ${c_ids[*]}" "\033[1;35m"
    if _ask; then
        docker rm "${c_ids[@]}" >/dev/null && echo -e "\033[1;32mContainers removed successfully.\033[0m\n"
        s_cont="done"
    else
        s_cont="skipped"
    fi
fi

# --- Step 2: Images (Dangling or All Unused) ---
if [[ "$s_img" == "pending" ]]; then
    _draw_plan "img"
    if [[ $prune_all -eq 1 ]]; then
        # Warning specific to --all mode
        echo -e "\n\033[0;33mWarning: This will remove ALL images not currently used by a container.\033[0m"
        _d_print_cmd "$img_cmd" "\033[1;35m"
        if _ask; then
            eval "$img_cmd" >/dev/null && echo -e "\033[1;32mImages removed successfully.\033[0m\n"
            s_img="done"
        else
            s_img="skipped"
        fi
    else
        # Standard mode
        docker images -f "dangling=true"
        echo ""
        _d_print_cmd "$img_cmd" "\033[1;35m"
        if _ask; then
            docker rmi "${i_ids[@]}" >/dev/null && echo -e "\033[1;32mImages removed successfully.\033[0m\n"
            s_img="done"
        else
            s_img="skipped"
        fi
    fi
fi

# --- Step 3: Dangling Volumes ---
if [[ "$s_vol" == "pending" ]]; then
    _draw_plan "vol"
    docker volume ls -f dangling=true
    echo ""
    _d_print_cmd "docker volume rm ${v_ids[*]}" "\033[1;35m"
    if _ask; then
        docker volume rm "${v_ids[@]}" >/dev/null && echo -e "\033[1;32mVolumes removed successfully.\033[0m\n"
        s_vol="done"
    else
        s_vol="skipped"
    fi
fi

# --- Step 4: Build Cache ---
if [[ "$s_cache" == "pending" ]]; then
    _draw_plan "cache"
    local prune_cmd="docker builder prune -f"
    if docker buildx version >/dev/null 2>&1; then
        prune_cmd="docker buildx prune -f"
    fi
    _d_print_cmd "$prune_cmd" "\033[1;35m"
    if _ask; then
        eval "$prune_cmd" >/dev/null && echo -e "\033[1;32mCache pruned successfully.\033[0m\n"
        s_cache="done"
    else
        s_cache="skipped"
    fi
fi

# Final recap
_draw_plan "finish"
echo -e "\033[1;32mCleanup Wizard completed.\033[0m"
```

**dops**

```
#!/usr/bin/env zsh

# Interactive Menu for Docker Operations
_d_check_env fzf || return 1

local -a options=(
    "dex: Exec into a container (smart shell & root toggle)"
    "dlogs: View logs (smart viewer & JSON support)"
    "dnuke: Cleanup wizard (BuildKit aware)"
    "drun: Run a new ephemeral container from an image"
    "dstop: Batch stop or kill containers"
)

local selected_op
selected_op=$(print -l "${options[@]}" | \
    fzf --height 30% --layout=reverse --header "Select Docker Operation" --border | \
    awk -F':' '{print $1}')

[[ -z "$selected_op" ]] && return 0

# Verify the selected function exists in the current Zsh session before executing
if type "$selected_op" >/dev/null 2>&1; then
    "$selected_op"
else
    echo >&2 "Error: Function '$selected_op' is not loaded or found."
fi
```

**drun**

```
#!/usr/bin/env zsh

# Interactive Docker Run (Ephemeral)
_d_check_env fzf docker || return 1

local fzf_out key selection image_id cmd_run user_flag query
query="$1"

# Preview showing Image layers, Env vars and Labels
local preview_cmd='docker inspect --format "Image: {{.RepoTags}}
----------------------------------------
ID:          {{.Id}}
Created:     {{.Created}}
Size:        {{.Size}}
Architecture: {{.Architecture}}
----------------------------------------
Labels:      {{range \$k, \$v := .Config.Labels}}{{\$k}}={{\$v}} {{end}}
" {2}'

# Full-screen fzf to select image
# Format: Repository:Tag | ID | Size | Created
fzf_out=$(docker images --format "{{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Size}}\t{{.CreatedAt}}" | \
    fzf --layout=reverse --border --query="$query" \
        --header "Enter: Run --rm | Ctrl-R: Run as Root" \
        --preview "$preview_cmd" --preview-window=right:50%:wrap \
        --expect=ctrl-r)

# Exit if user canceled
[[ -z "$fzf_out" ]] && return 0

key=$(head -n 1 <<< "$fzf_out")
selection=$(tail -n +2 <<< "$fzf_out")
image_id=$(awk '{print $2}' <<< "$selection")

[[ -z "$image_id" ]] && return 0

# Toggle root user
user_flag=""
[[ "$key" == "ctrl-r" ]] && user_flag="--user root"

# Smart shell execution
local smart_shell="sh -c 'exec \$(command -v zsh || command -v bash || echo sh)'"
cmd_run=$(echo "docker run --rm -it ${user_flag} ${image_id} ${smart_shell}" | tr -s ' ')

_d_print_cmd "$cmd_run" "\033[1;36m"
eval "$cmd_run"
```

**dstop**

```
#!/usr/bin/env zsh

# Batch Stop/Kill Docker Containers
_d_check_env fzf docker || return 1
_d_has_running_containers || return 0

local fzf_out key selection action_cmd color

# Multi-line preview command rearranged for better UX:
# 1. Core container metadata (IP, Ports, Compose).
# 2. Live Resource Usage placed HIGH up so it's always visible (preventing accidental stops of busy containers).
# 3. Mounts placed at the BOTTOM (since devcontainers can have massive lists).
# Note: You can scroll the preview window in fzf using Shift-Up/Shift-Down or mouse wheel.
local preview_cmd='docker inspect --format "Container: {{.Name}}
Status:      {{.State.Status}}
Compose Dir: {{index .Config.Labels \"com.docker.compose.project.working_dir\"}}
IP Address:  {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}
Ports:       {{range \$p, \$conf := .NetworkSettings.Ports}}{{if \$conf}}{{\$p}} {{end}}{{end}}" {1}
echo "------------------------------------------------------------"
echo -e "\033[1;33m>>> Live Resource Usage (Snapshot):\033[0m"
docker stats --no-stream --format "CPU: {{.CPUPerc}} | MEM: {{.MemUsage}} | NET: {{.NetIO}} | BLOCK I/O: {{.BlockIO}}" {1}
echo "------------------------------------------------------------"
echo -e "\033[1;34m>>> Mounts (Scroll down to see all):\033[0m"
docker inspect --format "{{range .Mounts}}• {{.Source}}
  -> {{.Destination}}
{{end}}" {1}'

# Full-screen fzf with multi-selection enabled (-m)
fzf_out=$(docker ps --format "{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}" | \
    fzf -m --ansi --layout=reverse --border \
        --header "TAB: Select | Ctrl-A: Select All | Enter: Stop | Ctrl-K: Kill (Scroll preview with Shift+Up/Down)" \
        --bind "ctrl-a:select-all" \
        --expect=ctrl-k \
        --preview "$preview_cmd" --preview-window=right:55%:wrap)

[[ -z "$fzf_out" ]] && return 0

# Extract the key pressed (Enter or ctrl-k)
key=$(head -n 1 <<< "$fzf_out")
# Extract only the container IDs (first column) from the selected rows
selection=$(tail -n +2 <<< "$fzf_out" | awk '{print $1}')

[[ -z "$selection" ]] && return 0

# Convert selection string into a proper Zsh array
local -a target_ids=("${(@f)selection}")

# Default action is graceful stop
action_cmd="docker stop"
color="\033[1;33m" # Yellow color for stop

# Switch to forceful kill if triggered by Ctrl-K
if [[ "$key" == "ctrl-k" ]]; then
    action_cmd="docker kill"
    color="\033[1;31m" # Red color for kill
fi

# Print and execute the command
_d_print_cmd "${action_cmd} ${target_ids[*]}" "$color"
eval "${action_cmd} ${target_ids[@]}"
```
