Quando você executa um programa em um terminal, muitas vezes ele aceita opções e parâmetros que modificam seu comportamento, como ls -l ou grep -i padrão arquivo.txt. Em C, esses argumentos são passados para a função main através de dois parâmetros: argc (argument count) e argv (argument vector). Dominar esse mecanismo é essencial para criar ferramentas de linha de comando robustas e flexíveis, desde simples utilitários até programas complexos com muitas opções.

Nesta aula, vamos explorar a fundo como esses parâmetros funcionam, como acessá-los e processá-los, e como utilizar a biblioteca getopt para simplificar o parsing de opções. Você verá exemplos práticos que vão desde a impressão de argumentos até a implementação de um programa com múltiplas opções.

argc e argv

A função main pode ser declarada de duas formas: sem parâmetros (int main(void)) ou com os parâmetros argc e argv. Quando você usa a segunda forma, o sistema operacional fornece automaticamente os argumentos digitados na linha de comando.

argc é um inteiro que indica quantos argumentos foram passados, incluindo o nome do próprio programa. argv é um array de strings (ponteiros para char) onde cada elemento é um argumento. O primeiro elemento, argv[0], é o nome do programa como foi executado. Os argumentos seguintes, argv[1] até argv[argc-1], são os parâmetros fornecidos pelo usuário. Por convenção, argv[argc] é um ponteiro nulo (NULL), o que pode ser útil para percorrer o array sem usar argc.

Vejamos um exemplo simples que imprime todos os argumentos:

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Número de argumentos: %d\n", argc);
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    return 0;
}

Se você compilar e executar esse programa com, por exemplo, ./programa um dois tres, a saída será:

Número de argumentos: 4
argv[0] = ./programa
argv[1] = um
argv[2] = dois
argv[3] = tres

Note que os argumentos são separados por espaços. Se você precisar passar um argumento que contenha espaços, deve envolvê-lo em aspas duplas ou escapar os espaços no terminal. Por exemplo, ./programa "hello world" passará hello world como um único argumento.

Parsing manual

Muitas vezes, precisamos interpretar os argumentos de acordo com regras específicas, como reconhecer opções que começam com - ou --. O parsing manual envolve percorrer argv e decidir o que fazer com cada argumento. Isso dá controle total, mas exige cuidado para lidar com todos os casos.

Um padrão comum é processar as opções primeiro e depois os argumentos restantes. Por exemplo, vamos criar um programa que aceita uma opção -v (verbose) e um nome de arquivo. O parsing manual poderia ser assim:

#include <stdio.h>
#include <string.h>

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

    // Percorre todos os argumentos a partir do índice 1
    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-v") == 0) {
            verbose = 1;
        } else if (argv[i][0] == '-') {
            printf("Opção desconhecida: %s\n", argv[i]);
        } else {
            // Se ainda não temos filename, assume que é o arquivo
            if (filename == NULL) {
                filename = argv[i];
            } else {
                printf("Argumento extra ignorado: %s\n", argv[i]);
            }
        }
    }

    if (verbose) {
        printf("Modo verboso ativado.\n");
    }
    if (filename) {
        printf("Arquivo: %s\n", filename);
    } else {
        printf("Nenhum arquivo especificado.\n");
    }
    return 0;
}

Esse código é simples, mas pode ficar complexo quando há muitas opções, especialmente se algumas aceitam valores (como -o arquivo). Para opções com valor, você precisa verificar se o próximo argumento existe e consumi-lo. Por exemplo:

if (strcmp(argv[i], "-o") == 0) {
    if (i + 1 < argc) {
        output = argv[++i];
    } else {
        printf("Erro: -o requer um valor.\n");
        return 1;
    }
}

Essa abordagem é funcional, mas reinventa a roda. Para programas com muitas opções, é melhor usar uma biblioteca como getopt.

getopt (visão geral)

A função getopt é uma função padrão da biblioteca POSIX (disponível em sistemas Unix-like, incluindo Linux e macOS) que simplifica o parsing de opções de linha de comando. Ela processa automaticamente opções curtas (como -a, -b) e permite que algumas opções recebam valores (como -o valor).

Para usar getopt, você precisa incluir <unistd.h> e chamá-la em um loop. O protótipo é:

int getopt(int argc, char * const argv[], const char *optstring);

O parâmetro optstring é uma string que especifica quais opções são válidas. Se uma opção espera um valor, você coloca dois pontos (:) após a letra. Por exemplo, "vo:" indica que -v não recebe valor, mas -o recebe. A função retorna a letra da opção encontrada, ou -1 quando todas as opções foram processadas. Variáveis globais como optarg (valor da opção), optind (índice do próximo argumento) e opterr (controle de mensagens de erro) são usadas.

Um exemplo típico:

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

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

    // o: indica que -o espera um argumento
    while ((opt = getopt(argc, argv, "vo:")) != -1) {
        switch (opt) {
            case 'v':
                verbose = 1;
                break;
            case 'o':
                output = optarg;
                break;
            default: // '?' para opção desconhecida
                fprintf(stderr, "Uso: %s [-v] [-o arquivo]\n", argv[0]);
                return 1;
        }
    }

    // Após o loop, optind aponta para o primeiro argumento não-opção
    for (int i = optind; i < argc; i++) {
        printf("Argumento: %s\n", argv[i]);
    }

    if (verbose) printf("Verbose ativado\n");
    if (output) printf("Saída: %s\n", output);
    return 0;
}

Observe que getopt reorganiza argv de forma que os argumentos não-opção fiquem no final, e optind indica onde eles começam. Isso facilita o processamento subsequente.

Exemplos

Vamos consolidar com um exemplo mais completo que combina parsing manual e getopt. Vamos criar um programa que aceita opções -v (verbose), -n (número de repetições) e um texto opcional. Usaremos getopt para as opções e depois processaremos os argumentos restantes.

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

int main(int argc, char *argv[]) {
    int verbose = 0;
    int repeat = 1;
    int opt;

    // n: requer argumento; v: não requer
    while ((opt = getopt(argc, argv, "vn:")) != -1) {
        switch (opt) {
            case 'v':
                verbose = 1;
                break;
            case 'n':
                repeat = atoi(optarg);
                if (repeat < 1) {
                    fprintf(stderr, "Erro: -n deve ser positivo.\n");
                    return 1;
                }
                break;
            default:
                fprintf(stderr, "Uso: %s [-v] [-n vezes] [mensagem]\n", argv[0]);
                return 1;
        }
    }

    // Coleta a mensagem dos argumentos restantes
    char *message = NULL;
    if (optind < argc) {
        message = argv[optind];
    } else {
        message = "Olá, mundo!";
    }

    // Imprime a mensagem 'repeat' vezes, com verbosidade
    for (int i = 0; i < repeat; i++) {
        if (verbose) printf("[%d] ", i + 1);
        printf("%s\n", message);
    }

    return 0;
}

Teste com diferentes combinações:

$ ./programa -v -n 3 "Teste"
[1] Teste
[2] Teste
[3] Teste
$ ./programa -n 2
Olá, mundo!
Olá, mundo!

Esse exemplo mostra como getopt lida com opções que têm valores e como separar as opções dos argumentos posicionais.

Boas práticas e observações finais

Ao trabalhar com argumentos de linha de comando, siga estas recomendações:

  • Sempre valide a entrada: verifique se opções que exigem valor realmente receberam um, e se valores numéricos são válidos.
  • Forneça uma mensagem de uso (usage) clara, indicando as opções e argumentos esperados.
  • Prefira getopt para opções curtas e considere getopt_long para opções longas (como --verbose).
  • Documente as opções no código, para que outros programadores entendam a interface.
  • Lembre-se de que argv é modificado por getopt; se precisar preservar os argumentos originais, faça uma cópia antes.

Dominar argumentos de linha de comando é um passo importante para criar ferramentas de terminal profissionais. Com as técnicas desta aula, você pode construir programas flexíveis e fáceis de usar.

Referências

Exercícios

  1. Escreva um programa que imprima o número de argumentos e cada argumento em uma linha, sem usar argc (use a condição argv[i] != NULL).
  2. ✓ Resposta:
    #include <stdio.h>
    
    int main(int argc, char *argv[]) {
        int i = 0;
        while (argv[i] != NULL) {
            printf("argv[%d] = %s\n", i, argv[i]);
            i++;
        }
        return 0;
    }
    
  3. Crie um programa que receba dois números via linha de comando e imprima a soma. Trate o caso de argumentos insuficientes ou não numéricos.
  4. ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(int argc, char *argv[]) {
        if (argc != 3) {
            fprintf(stderr, "Uso: %s num1 num2\n", argv[0]);
            return 1;
        }
        char *endptr;
        double a = strtod(argv[1], &endptr);
        if (*endptr != '\0') {
            fprintf(stderr, "Erro: '%s' não é um número válido.\n", argv[1]);
            return 1;
        }
        double b = strtod(argv[2], &endptr);
        if (*endptr != '\0') {
            fprintf(stderr, "Erro: '%s' não é um número válido.\n", argv[2]);
            return 1;
        }
        printf("Soma: %f\n", a + b);
        return 0;
    }
    
  5. Escreva um programa que use getopt para aceitar as opções -a (ativa algo) e -b valor (define um valor). Imprima o que foi configurado.
  6. ✓ Resposta:
    #include <stdio.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[]) {
        int a_flag = 0;
        char *b_value = NULL;
        int opt;
        while ((opt = getopt(argc, argv, "ab:")) != -1) {
            switch (opt) {
                case 'a':
                    a_flag = 1;
                    break;
                case 'b':
                    b_value = optarg;
                    break;
                default:
                    fprintf(stderr, "Uso: %s [-a] [-b valor]\n", argv[0]);
                    return 1;
            }
        }
        printf("a_flag: %d\n", a_flag);
        printf("b_value: %s\n", b_value ? b_value : "(não definido)");
        return 0;
    }
    
  7. Modifique o exemplo da aula para que a opção -n aceite um valor negativo e imprima uma mensagem de erro clara, retornando um código de erro.
  8. ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[]) {
        int verbose = 0;
        int repeat = 1;
        int opt;
        while ((opt = getopt(argc, argv, "vn:")) != -1) {
            switch (opt) {
                case 'v':
                    verbose = 1;
                    break;
                case 'n':
                    repeat = atoi(optarg);
                    if (repeat <= 0) {
                        fprintf(stderr, "Erro: -n deve ser um inteiro positivo.\n");
                        return 1;
                    }
                    break;
                default:
                    fprintf(stderr, "Uso: %s [-v] [-n vezes] [mensagem]\n", argv[0]);
                    return 1;
            }
        }
        char *message = (optind < argc) ? argv[optind] : "Olá, mundo!";
        for (int i = 0; i < repeat; i++) {
            if (verbose) printf("[%d] ", i + 1);
            printf("%s\n", message);
        }
        return 0;
    }
    
  9. Pesquise sobre getopt_long (para opções longas) e escreva um exemplo que use uma opção longa --verbose e uma curta -v como sinônimos.
  10. ✓ Resposta:
    #include <stdio.h>
    #include <getopt.h>
    
    int main(int argc, char *argv[]) {
        int verbose = 0;
        int opt;
        static struct option long_options[] = {
            {"verbose", no_argument, 0, 'v'},
            {0, 0, 0, 0}
        };
        while ((opt = getopt_long(argc, argv, "v", long_options, NULL)) != -1) {
            switch (opt) {
                case 'v':
                    verbose = 1;
                    break;
                default:
                    fprintf(stderr, "Uso: %s [--verbose]\n", argv[0]);
                    return 1;
            }
        }
        if (verbose) printf("Verbose ativado\n");
        return 0;
    }