Created
December 22, 2025 09:25
-
-
Save cumulus13/ef3bcacd1fb436abf2cc9eae7df0f9a1 to your computer and use it in GitHub Desktop.
Prune Docker images with 'None' tag.
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 python3 | |
| #File: dprune.py | |
| ## Author: Hadi Cahyadi <cumulus13@gmail.com> | |
| # Date: 2025-12-22 | |
| # Description: Prune Docker images with 'None' tag. | |
| # License: MIT | |
| import subprocess | |
| from rich.console import Console | |
| from rich_argparse import RichHelpFormatter, _lazy_rich as rr | |
| from typing import ClassVar | |
| from rich.progress import Progress | |
| import argparse | |
| import sys | |
| import time | |
| import re | |
| console = Console() | |
| class CustomRichHelpFormatter(RichHelpFormatter): | |
| """A custom RichHelpFormatter with modified styles.""" | |
| styles: ClassVar[dict[str, rr.StyleType]] = { | |
| "argparse.args": "bold #FFFF00", # Changed from cyan | |
| "argparse.groups": "#AA55FF", # Changed from dark_orange | |
| "argparse.help": "bold #00FFFF", # Changed from default | |
| "argparse.metavar": "bold #FF00FF", # Changed from dark_cyan | |
| "argparse.syntax": "underline", # Changed from bold | |
| "argparse.text": "white", # Changed from default | |
| "argparse.prog": "bold #00AAFF italic", # Changed from grey50 | |
| "argparse.default": "bold", # Changed from italic | |
| } | |
| def colorize_docker_output(out): | |
| # Color <none> (first and second on each line) | |
| def replace_none(match): | |
| return "[#FF5500]<none>[/]" | |
| # Color image_id (3rd column, usually 12 hex characters) | |
| def replace_image_id(match): | |
| return f"[#00FFFF]{match.group(0)}[/]" | |
| # Color the time (eg: '47 hours ago', '2 weeks ago', etc.) | |
| def replace_time(match): | |
| return f"[#AA55FF]{match.group(0)}[/]" | |
| # Color the size (eg: '319MB', '551MB', etc) | |
| def replace_size(match): | |
| return f"[#5555FF]{match.group(0)}[/]" | |
| lines = out.splitlines() | |
| colored_lines = [] | |
| for line in lines: | |
| # Color <none> (max 2 times per line) | |
| line = re.sub(r"<none>", replace_none, line, count=2) | |
| # Color image_id (12+ hex, 3rd column) | |
| line = re.sub(r"\b[a-f0-9]{12,}\b", replace_image_id, line, count=1) | |
| # Color the time (eg: '47 hours ago', '2 weeks ago', etc.) | |
| line = re.sub(r"\b\d+\s+\w+\s+ago\b", replace_time, line) | |
| # Color the size (eg: '319MB', '551MB', etc) | |
| line = re.sub(r"\b\d+MB\b", replace_size, line) | |
| colored_lines.append(line) | |
| return "\n".join(colored_lines) | |
| def dprune(quiet=False): | |
| cmd = "docker image ls -a | grep -i None" | |
| try: | |
| result = subprocess.run(cmd, shell=True, check=True, text=True, capture_output=True) | |
| out, err = result.stdout, result.stderr | |
| if out: | |
| console.print("\n[bold #FFFF00]Docker images with[/] [white on #0000FF]'None'[/] [bold #FFFF00]tag:[/]") | |
| # console.print(out, "\n") | |
| console.print(colorize_docker_output(out), "\n") | |
| image_ids = [line.split()[2] for line in out.splitlines()] | |
| if not quiet: | |
| q = console.input("[bold #00FFFF]Do you want to remove these images? (y/n): [/]") | |
| print("\n") | |
| if q.lower() != 'y': | |
| console.print("[white on #550000]Exiting without removing images.[/]") | |
| return | |
| with Progress() as progress: | |
| emoji = "🐳" | |
| task = progress.add_task(f"{emoji} [bold #FFFF00]remove[/]: [bold #00FFFF]{image_ids[0]}[/]", total=len(image_ids)) | |
| for idx, image_id in enumerate(image_ids): | |
| remove_cmd = f"docker rmi {image_id}" | |
| try: | |
| subprocess.run(remove_cmd, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| progress.update(task, description=f"{emoji} [bold #FFFF00]remove[/]: [bold #00FFFF]{image_id}[/]") | |
| progress.advance(task) | |
| # time.sleep(0.1) | |
| except subprocess.CalledProcessError as e: | |
| progress.console.print(f"[white on #005500]Failed to remove image:[/] [bold #00FFFF]{image_id}[/] - [white on red]{e.stderr.decode().strip()}[/]") | |
| # hide progress bar | |
| progress.stop() | |
| print("\n") | |
| console.print(f"[white on #0000FF]Done removing images.[/]") | |
| else: | |
| console.print("[black on #FFFF00]No Docker images with 'None' tag found.[/]") | |
| except subprocess.CalledProcessError as e: | |
| console.print(f"[white on red]An error occurred:[/] [white on blue]{e.stderr}[/]") | |
| except Exception as e: | |
| console.print(f"[white on red]An unexpected error occurred:[/] [[white on blue]{str(e)}[/]") | |
| def usage(): | |
| parser = argparse.ArgumentParser( | |
| prog="dprune", | |
| description="Prune Docker images with 'None' tag.", | |
| formatter_class=CustomRichHelpFormatter, | |
| ) | |
| parser.add_argument( | |
| "-v", "--version", | |
| action="version", | |
| version="%(prog)s 1.0", | |
| help="Show the version of the program." | |
| ) | |
| parser.add_argument( | |
| "-q", "--quiet", | |
| action="store_true", | |
| help="Suppress confirmation prompt before removing images." | |
| ) | |
| parser.add_argument( | |
| "-s", "--setup", | |
| action="store_true", | |
| help="Setup dprune command in the system." | |
| ) | |
| if len(sys.argv) == 1: | |
| parser.print_help() | |
| dprune() | |
| else: | |
| args = parser.parse_args() | |
| if args.setup: | |
| p = subprocess.run("pip install rich rich-argparse", shell=True) | |
| out, err = p.stdout, p.stderr | |
| if p.returncode != 0: | |
| console.print(f"[white on red]Failed to install dependencies:[/] [white on blue]{err}[/]") | |
| sys.exit(1) | |
| console.print("[white on #005500]Setup completed. You can now use 'dprune' command.[/]") | |
| else: | |
| dprune(quiet=True) | |
| if __name__ == "__main__": | |
| usage() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment