Click: CLIs modernas e composíveis

1. Introdução ao Click e seus diferenciais

Se você já precisou criar uma interface de linha de comando (CLI) em Python, provavelmente conhece o argparse da biblioteca padrão. Embora funcional, o argparse frequentemente resulta em código verboso, difícil de manter e com pouca flexibilidade para composição. É aí que entra o Click.

Click (Command Line Interface Creation Kit) é um framework criado por Armin Ronacher (mesmo criador do Flask) que revoluciona a construção de CLIs. Sua filosofia é baseada em três pilares: composição (comandos podem ser combinados livremente), decoração (uso intensivo de decoradores Python) e simplicidade (menos boilerplate).

Para instalar:

pip install click

Um exemplo mínimo:

import click

@click.command()
def hello():
    """Comando simples que cumprimenta o usuário."""
    click.echo("Olá, mundo!")

if __name__ == "__main__":
    hello()

2. Definindo parâmetros de linha de comando

Click oferece duas formas principais de receber dados do usuário: opções e argumentos.

Opções com @click.option()

@click.command()
@click.option("--nome", default="Mundo", help="Nome para cumprimentar")
@click.option("--idade", type=int, prompt="Sua idade")
@click.option("--ativo/--inativo", default=True, help="Status do usuário")
def saudacao(nome, idade, ativo):
    click.echo(f"Olá {nome}, você tem {idade} anos")
    if ativo:
        click.echo("Status: ativo")

Argumentos com @click.argument()

@click.command()
@click.argument("arquivo", type=click.Path(exists=True))
@click.argument("linhas", type=int, default=10)
@click.argument("palavras", nargs=-1)  # Múltiplos valores
def ler_arquivo(arquivo, linhas, palavras):
    """Lê um arquivo e busca palavras específicas."""
    with open(arquivo) as f:
        conteudo = f.readlines()[:linhas]
    for palavra in palavras:
        click.echo(f"Buscando: {palavra}")

3. Agrupamento e composição de comandos

Um dos recursos mais poderosos do Click é a capacidade de criar grupos de comandos.

@click.group()
def cli():
    """Ferramenta de gerenciamento de projetos."""
    pass

@cli.command()
@click.argument("nome")
def init(nome):
    """Inicializa um novo projeto."""
    click.echo(f"Projeto {nome} criado!")

@cli.command()
@click.argument("nome")
def build(nome):
    """Compila o projeto."""
    click.echo(f"Compilando {nome}...")

@cli.command()
@click.argument("nome")
@click.option("--force", is_flag=True)
def deploy(nome, force):
    """Faz deploy do projeto."""
    if force:
        click.echo("Forçando deploy...")
    click.echo(f"Deploy de {nome} realizado!")

if __name__ == "__main__":
    cli()

Para usar:

python app.py init meu-projeto
python app.py build meu-projeto
python app.py deploy meu-projeto --force

4. Validação e conversão de tipos

Click oferece tipos nativos e personalizados para validação robusta.

def validar_email(ctx, param, value):
    """Callback de validação personalizado."""
    if "@" not in value:
        raise click.BadParameter("Email inválido: deve conter @")
    return value.lower()

@click.command()
@click.option("--tamanho", type=click.IntRange(1, 100))
@click.option("--modo", type=click.Choice(["dev", "prod", "test"]))
@click.option("--caminho", type=click.Path(file_okay=True, dir_okay=False))
@click.option("--email", callback=validar_email)
def configurar(tamanho, modo, caminho, email):
    click.echo(f"Configuração: {tamanho}, {modo}, {caminho}, {email}")

5. Contexto e estado compartilhado

O objeto Context permite compartilhar dados entre comandos em um grupo.

@click.group()
@click.option("--verbose", is_flag=True)
@click.pass_context
def cli(ctx, verbose):
    """CLI principal com contexto compartilhado."""
    ctx.ensure_object(dict)
    ctx.obj["verbose"] = verbose
    ctx.obj["config"] = {"timeout": 30, "retry": 3}

@cli.command()
@click.argument("servidor")
@click.pass_context
def ping(ctx, servidor):
    """Testa conectividade com um servidor."""
    config = ctx.obj["config"]
    verbose = ctx.obj["verbose"]

    if verbose:
        click.echo(f"Timeout: {config['timeout']}s")
        click.echo(f"Retentativas: {config['retry']}")

    click.echo(f"Pingando {servidor}...")

6. Saída formatada e interatividade

Click facilita a criação de interfaces ricas e interativas.

@click.command()
@click.option("--nome", prompt="Digite seu nome")
@click.password_option()
def login(nome, password):
    """Sistema de login interativo."""

    # Saída colorida
    click.secho("Bem-vindo!", fg="green", bold=True)
    click.echo(click.style(f"Usuário: {nome}", fg="cyan"))

    # Confirmação
    if click.confirm("Deseja continuar?"):
        # Barra de progresso
        with click.progressbar(range(10), label="Carregando") as bar:
            for item in bar:
                import time
                time.sleep(0.1)

        click.echo("Operação concluída!")
    else:
        click.echo("Operação cancelada.")

7. Tratamento de erros e testes

Click fornece exceções específicas e um runner para testes.

import click
from click.testing import CliRunner

@click.command()
@click.argument("numero", type=int)
def dividir(numero):
    """Divide 10 pelo número fornecido."""
    if numero == 0:
        raise click.ClickException("Divisão por zero não permitida!")

    resultado = 10 / numero
    click.echo(f"Resultado: {resultado}")

# Teste unitário
def test_dividir():
    runner = CliRunner()

    # Teste bem-sucedido
    result = runner.invoke(dividir, ["5"])
    assert result.exit_code == 0
    assert "Resultado: 2.0" in result.output

    # Teste de erro
    result = runner.invoke(dividir, ["0"])
    assert result.exit_code != 0
    assert "Divisão por zero" in result.output

    # Teste de tipo inválido
    result = runner.invoke(dividir, ["abc"])
    assert result.exit_code != 0

8. Boas práticas e composição avançada

Para aplicações mais sofisticadas, considere estas práticas:

# Decorador reutilizável para opções comuns
def opcoes_comuns(func):
    @click.option("--verbose", is_flag=True, help="Modo verboso")
    @click.option("--config", type=click.Path(), help="Arquivo de configuração")
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# Plugin com descoberta automática
def descobrir_plugins():
    """Descobre comandos em pacotes instalados."""
    import pkg_resources
    plugins = {}
    for entry_point in pkg_resources.iter_entry_points("meuapp.commands"):
        plugins[entry_point.name] = entry_point.load()
    return plugins

@click.group()
def cli():
    """CLI principal com plugins."""
    pass

# Registro dinâmico de comandos
for nome, cmd in descobrir_plugins().items():
    cli.add_command(cmd, nome)

Para publicar sua CLI no PyPI, estruture seu setup.py ou pyproject.toml com entry points:

# setup.py
setup(
    name="minha-cli",
    entry_points={
        "console_scripts": [
            "minha-cli=meupacote.cli:cli",
        ],
    },
)

Click se destaca por transformar a criação de CLIs em Python em uma experiência agradável e produtiva. Sua filosofia de composição, combinada com decoradores elegantes e suporte nativo a testes, faz dele a escolha ideal para projetos de qualquer escala — desde scripts simples até ferramentas corporativas complexas.

Referências