PYTHONPython

files

real world projects / cli tool / devtools / commands

PYTHON
files.py🐍
"""
File management commands.
"""

import os
from pathlib import Path
import click
from rich.console import Console
from rich.table import Table
from rich.tree import Tree

console = Console()


@click.group()
def files():
    """File management utilities."""
    pass


@files.command()
@click.argument('path', type=click.Path(exists=True), default='.')
@click.option('--pattern', '-p', default='*', help='Glob pattern to match')
def count(path: str, pattern: str):
    """Count files matching a pattern."""
    path = Path(path)
    
    if path.is_file():
        console.print(f"[yellow]1 file[/yellow]")
        return
    
    files = list(path.rglob(pattern))
    dirs = [f for f in files if f.is_dir()]
    regular_files = [f for f in files if f.is_file()]
    
    console.print(f"[green]{len(regular_files)}[/green] files, "
                  f"[blue]{len(dirs)}[/blue] directories "
                  f"matching [yellow]{pattern}[/yellow]")


@files.command()
@click.argument('path', type=click.Path(exists=True), default='.')
@click.option('--pattern', '-p', default='*.py', help='Glob pattern to find')
@click.option('--limit', '-l', default=20, help='Maximum results')
def find(path: str, pattern: str, limit: int):
    """Find files matching a pattern."""
    path = Path(path)
    files = list(path.rglob(pattern))[:limit]
    
    if not files:
        console.print(f"[yellow]No files matching {pattern}[/yellow]")
        return
    
    table = Table(title=f"Files matching {pattern}")
    table.add_column("File", style="cyan")
    table.add_column("Size", style="green", justify="right")
    
    for f in files:
        if f.is_file():
            size = f.stat().st_size
            size_str = _format_size(size)
            table.add_row(str(f), size_str)
    
    console.print(table)


@files.command()
@click.argument('path', type=click.Path(exists=True), default='.')
def size(path: str):
    """Calculate total size of a directory."""
    path = Path(path)
    
    if path.is_file():
        console.print(f"Size: [green]{_format_size(path.stat().st_size)}[/green]")
        return
    
    total = sum(f.stat().st_size for f in path.rglob('*') if f.is_file())
    file_count = len([f for f in path.rglob('*') if f.is_file()])
    
    console.print(f"Total: [green]{_format_size(total)}[/green] "
                  f"across [cyan]{file_count}[/cyan] files")


@files.command()
@click.argument('path', type=click.Path(exists=True), default='.')
@click.option('--depth', '-d', default=2, help='Max depth to display')
def tree(path: str, depth: int):
    """Display directory tree structure."""
    path = Path(path)
    
    tree = Tree(f"[bold blue]{path}[/bold blue]")
    _add_to_tree(tree, path, depth)
    
    console.print(tree)


def _add_to_tree(tree: Tree, path: Path, depth: int, current_depth: int = 0):
    """Recursively add directories to tree."""
    if current_depth >= depth:
        return
    
    try:
        items = sorted(path.iterdir(), key=lambda x: (x.is_file(), x.name.lower()))
        
        for item in items:
            if item.name.startswith('.'):
                continue
                
            if item.is_dir():
                branch = tree.add(f"[bold blue]{item.name}/[/bold blue]")
                _add_to_tree(branch, item, depth, current_depth + 1)
            else:
                tree.add(f"[green]{item.name}[/green]")
    except PermissionError:
        tree.add("[red]Permission denied[/red]")


def _format_size(size: int) -> str:
    """Format file size to human readable."""
    for unit in ['B', 'KB', 'MB', 'GB']:
        if size < 1024:
            return f"{size:.1f} {unit}"
        size /= 1024
    return f"{size:.1f} TB"
PreviousNext