Criando uma CLI com getopt e argparse

1. Introdução à Criação de CLIs em C

Interfaces de linha de comando (CLIs) são a porta de entrada para a maioria dos programas escritos em C. Elas permitem que usuários controlem o comportamento do programa sem modificar o código-fonte, passando parâmetros como --verbose, -o arquivo.txt ou --help. Essa flexibilidade é essencial para ferramentas que precisam se integrar a scripts, pipelines ou ambientes de automação.

Os padrões POSIX e GNU estabelecem convenções amplamente adotadas: opções curtas com um único traço (-v) e opções longas com dois traços (--verbose). Implementar manualmente o parsing desses argumentos é tedioso e propenso a erros. Felizmente, a linguagem C oferece bibliotecas robustas para essa tarefa. A biblioteca padrão getopt (e sua extensão getopt_long) está disponível em qualquer sistema POSIX. Já a biblioteca argparse, mais moderna e de cabeçalho único, oferece uma API mais expressiva e recursos como argumentos posicionais, tipos e subcomandos.

Este artigo explora ambas as abordagens, desde os fundamentos do getopt até a construção de uma CLI rica com argparse, com exemplos práticos de código.

2. Fundamentos do getopt Padrão

A função getopt() é a espinha dorsal do parsing de opções curtas em C. Ela é declarada em <unistd.h> e utiliza três variáveis globais:
- optarg: aponta para o argumento de uma opção que requer valor.
- optind: índice do próximo elemento a ser processado em argv.
- optopt: armazena a opção que causou erro.

A string de formato define as opções esperadas. Por exemplo, "a:b::c" significa:
- a: → opção -a com argumento obrigatório.
- b:: → opção -b com argumento opcional (dois pontos).
- c → opção -c sem argumento.

O loop básico é:

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    int opt;
    while ((opt = getopt(argc, argv, "a:b::c")) != -1) {
        switch (opt) {
            case 'a':
                printf("Opção -a com argumento: %s\n", optarg);
                break;
            case 'b':
                printf("Opção -b com argumento: %s\n", optarg ? optarg : "(nenhum)");
                break;
            case 'c':
                printf("Opção -c ativada\n");
                break;
            case '?':
                fprintf(stderr, "Opção desconhecida: -%c\n", optopt);
                return 1;
        }
    }
    // Argumentos posicionais restantes estão em argv[optind..argc-1]
    for (int i = optind; i < argc; i++)
        printf("Argumento posicional: %s\n", argv[i]);
    return 0;
}

A string de formato controla rigorosamente o que é aceito. Opções não listadas retornam '?' e optopt indica qual caractere causou o erro.

3. Opções Longas com getopt_long

Para suporte a opções longas como --verbose, usamos getopt_long(), disponível em <getopt.h> em sistemas GNU/Linux. A estrutura option é definida como:

struct option {
    const char *name;       // Nome longo (ex: "verbose")
    int         has_arg;    // no_argument, required_argument, optional_argument
    int        *flag;       // Se NULL, retorna val; senão, armazena val em *flag
    int         val;        // Valor retornado ou armazenado
};

Exemplo combinando opções curtas e longas:

#include <stdio.h>
#include <getopt.h>

int main(int argc, char *argv[]) {
    int verbose = 0;
    char *output = NULL;
    int help = 0;

    struct option long_options[] = {
        {"verbose", no_argument,       &verbose, 1},
        {"output",  required_argument, NULL,     'o'},
        {"help",    no_argument,       NULL,     'h'},
        {0, 0, 0, 0}  // Terminador
    };

    int opt;
    int option_index = 0;
    while ((opt = getopt_long(argc, argv, "vo:h", long_options, &option_index)) != -1) {
        switch (opt) {
            case 0:
                // flag foi setada automaticamente
                break;
            case 'v':
                verbose = 1;
                break;
            case 'o':
                output = optarg;
                break;
            case 'h':
                help = 1;
                break;
            case '?':
                return 1;
        }
    }

    if (verbose) printf("Modo verboso ativado\n");
    if (output)  printf("Arquivo de saída: %s\n", output);
    if (help)    printf("Ajuda solicitada\n");
    return 0;
}

Note que quando flag não é NULL, getopt_long retorna 0 e a variável apontada é atualizada. Isso simplifica o código para opções booleanas.

4. Implementando uma CLI Completa com getopt

Vamos construir uma ferramenta de log que aceita:
- -l ou --level: nível do log (obrigatório)
- -f ou --file: arquivo de saída (opcional)
- -v ou --verbose: modo verboso

#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>

void usage(const char *prog) {
    fprintf(stderr, "Uso: %s -l LEVEL [-f FILE] [-v]\n", prog);
    fprintf(stderr, "  -l, --level LEVEL  Nível do log (DEBUG, INFO, ERROR)\n");
    fprintf(stderr, "  -f, --file FILE    Arquivo de saída (padrão: stdout)\n");
    fprintf(stderr, "  -v, --verbose      Modo verboso\n");
}

int main(int argc, char *argv[]) {
    char *level = NULL;
    char *file = NULL;
    int verbose = 0;

    struct option long_opts[] = {
        {"level",   required_argument, NULL, 'l'},
        {"file",    required_argument, NULL, 'f'},
        {"verbose", no_argument,       NULL, 'v'},
        {"help",    no_argument,       NULL, 'h'},
        {0, 0, 0, 0}
    };

    int opt;
    while ((opt = getopt_long(argc, argv, "l:f:vh", long_opts, NULL)) != -1) {
        switch (opt) {
            case 'l':
                level = optarg;
                break;
            case 'f':
                file = optarg;
                break;
            case 'v':
                verbose = 1;
                break;
            case 'h':
                usage(argv[0]);
                return 0;
            case '?':
                usage(argv[0]);
                return 1;
        }
    }

    if (!level) {
        fprintf(stderr, "Erro: --level é obrigatório\n");
        usage(argv[0]);
        return 1;
    }

    if (verbose) printf("Nível: %s, Arquivo: %s\n", level, file ? file : "stdout");
    // Lógica principal aqui...
    return 0;
}

O tratamento de erros é explícito: opção desconhecida gera '?' e argumento faltante em opção obrigatória também retorna '?'. A função usage() centraliza as mensagens de ajuda.

5. Introdução ao argparse (Biblioteca Moderna)

A biblioteca argparse (disponível em github.com/cofyc/argparse) é um parser de argumentos de linha de comando em C, inspirado no módulo argparse do Python. É um único arquivo .h que você pode copiar para seu projeto.

Instalação: basta baixar argparse.h e incluí-lo no código. Compile com qualquer compilador C99+.

Conceitos fundamentais:
- Argumentos posicionais: valores obrigatórios na ordem especificada.
- Argumentos opcionais: começam com - ou --.
- Flags: opções booleanas que não recebem valor.
- Subcomandos: comandos aninhados (como git commit).

O fluxo básico é:

#include "argparse.h"

int main(int argc, char *argv[]) {
    const char *const usage[] = {
        "meu_programa [opções] ARQUIVO",
        NULL,
    };

    // Definição dos argumentos
    struct argparse_option options[] = {
        OPT_HELP(),
        OPT_BOOLEAN('v', "verbose", NULL, "modo verboso"),
        OPT_STRING('o', "output", NULL, "arquivo de saída"),
        OPT_END(),
    };

    // Estrutura do parser
    struct argparse argparse;
    argparse_init(&argparse, options, usage, 0);

    // Parsing
    argparse_parse(&argparse, argc, argv);
    // Após o parsing, os argumentos posicionais estão em argv[argc - n...]
    return 0;
}

OPT_HELP() adiciona automaticamente -h/--help. As macros OPT_BOOLEAN, OPT_STRING, OPT_INTEGER, OPT_FLOAT definem os tipos.

6. Construindo uma CLI Rica com argparse

Vamos criar uma ferramenta de conversão de arquivos com --input, --output e --format:

#include <stdio.h>
#include <stdlib.h>
#include "argparse.h"

int main(int argc, char *argv[]) {
    const char *const usage[] = {
        "converter [opções]",
        "Converte arquivos entre formatos.",
        NULL,
    };

    char *input = NULL;
    char *output = NULL;
    char *format = "txt";
    int verbose = 0;
    int count = 1;

    struct argparse_option options[] = {
        OPT_HELP(),
        OPT_STRING('i', "input", &input, "arquivo de entrada (obrigatório)"),
        OPT_STRING('o', "output", &output, "arquivo de saída"),
        OPT_STRING('f', "format", &format, "formato de saída (txt, csv, json)"),
        OPT_BOOLEAN('v', "verbose", &verbose, "exibir informações detalhadas"),
        OPT_INTEGER('c', "count", &count, "número de conversões"),
        OPT_END(),
    };

    struct argparse argparse;
    argparse_init(&argparse, options, usage, 0);
    argparse_describe(&argparse, "\nFerramenta de conversão de arquivos.", "\nExemplo: converter -i dados.csv -o dados.json -f json -v");

    int argc2 = argc;
    char **argv2 = argv;
    argparse_parse(&argparse, argc2, argv2);

    if (!input) {
        fprintf(stderr, "Erro: --input é obrigatório\n");
        argparse_usage(&argparse);
        return 1;
    }

    if (verbose) {
        printf("Entrada: %s\n", input);
        printf("Saída: %s\n", output ? output : "(automático)");
        printf("Formato: %s\n", format);
        printf("Contagem: %d\n", count);
    }

    // Lógica de conversão...
    return 0;
}

A função argparse_describe adiciona descrições extras ao help. OPT_STRING armazena diretamente em um ponteiro char*, e OPT_INTEGER em um int. O valor padrão é definido pela inicialização das variáveis.

7. Tratamento Avançado e Boas Práticas

Mensagens de erro amigáveis são cruciais. Com getopt, implemente uma função usage() que exiba a sintaxe e as opções. Com argparse, use argparse_usage() após detectar erro.

Validação de argumentos pode ser feita após o parsing. Por exemplo, verificar se o formato é um dos valores permitidos:

if (strcmp(format, "txt") != 0 && strcmp(format, "csv") != 0 && strcmp(format, "json") != 0) {
    fprintf(stderr, "Erro: formato '%s' inválido. Use txt, csv ou json.\n", format);
    return 1;
}

Para compatibilidade entre as bibliotecas, você pode usar getopt para opções curtas básicas e argparse para funcionalidades mais complexas, desde que não haja conflito de nomes. Em projetos que exigem portabilidade estrita (POSIX), prefira getopt. Para aplicações modernas com muitos argumentos, argparse reduz significativamente o código boilerplate.

8. Comparação e Escolha da Abordagem

Característica getopt argparse
Dependências Nenhuma (padrão POSIX) Cabeçalho único (C99+)
Opções longas getopt_long (GNU) Nativo
Tipos de dados Apenas string string, int, float, booleano
Argumentos posicionais Manual Nativo
Subcomandos Não Sim
Mensagens de erro Manual Automáticas
Tamanho do código Maior (mais controle) Menor (mais abstração)

Use getopt quando:
- O projeto precisa ser estritamente POSIX.
- O número de opções é pequeno (menos de 5).
- Você quer controle total sobre o parsing.

Use argparse quando:
- A CLI tem muitos argumentos ou subcomandos.
- Você precisa de validação de tipos automática.
- A produtividade é mais importante que o tamanho binário.

Ambas as bibliotecas são ferramentas valiosas no arsenal do programador C. A escolha depende do contexto do projeto e da complexidade da interface desejada.

Referências