Argumentos de linha de comando são uma forma fundamental de interação entre o usuário e um programa em C. Quando você executa um programa no terminal, pode passar informações adicionais, como nomes de arquivos, opções ou valores de configuração. Em C, esses argumentos são acessados através dos parâmetros da função main: argc (argument count) e argv (argument vector). Dominar esse conceito é essencial para criar ferramentas de linha de comando eficientes e flexíveis, como utilitários Unix, scripts de automação e programas que precisam de configuração dinâmica.

Nesta aula, vamos explorar desde os fundamentos de argc e argv até técnicas avançadas de parsing, incluindo a biblioteca getopt, que facilita o tratamento de opções. Veremos exemplos práticos e, ao final, exercícios para consolidar o conhecimento.

argc e argv

Quando você executa um programa, o sistema operacional passa uma lista de strings para a função main. O primeiro parâmetro, argc, é um inteiro que indica o número de argumentos, incluindo o nome do próprio programa. O segundo, argv, é um array de ponteiros para strings, onde cada elemento contém um argumento. Por exemplo, se você executar ./meu_programa -f arquivo.txt, argc será 3, e argv[0] apontará para "./meu_programa", argv[1] para "-f" e argv[2] para "arquivo.txt".

É importante notar que argv[argc] é sempre NULL, conforme a especificação da linguagem. Isso pode ser útil para percorrer os argumentos com um ponteiro. O valor de argc é pelo menos 1, pois o nome do programa é sempre contado.

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;
}

Ao compilar e executar com argumentos, o programa exibirá cada um deles. Esse é o ponto de partida para qualquer manipulação de linha de comando.

Parsing manual

Muitas vezes, precisamos interpretar os argumentos de forma estruturada, identificando opções (como -f ou --verbose) e valores associados. O parsing manual envolve percorrer argv e decidir o que fazer com cada argumento. Isso é feito geralmente com um laço for ou while, comparando strings.

Vamos considerar um programa que aceita uma opção -o para especificar um arquivo de saída e uma flag -v para modo verboso. Um parsing manual poderia ser:

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

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

    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-o") == 0) {
            if (i + 1 < argc) {
                output = argv[++i];
            } else {
                fprintf(stderr, "Erro: -o requer um valor\n");
                return 1;
            }
        } else if (strcmp(argv[i], "-v") == 0) {
            verbose = 1;
        } else {
            fprintf(stderr, "Argumento desconhecido: %s\n", argv[i]);
            return 1;
        }
    }

    printf("Output: %s\n", output ? output : "(nenhum)");
    printf("Verbose: %s\n", verbose ? "sim" : "não");
    return 0;
}

Esse método é simples e funciona bem para programas pequenos, mas pode se tornar complexo quando há muitas opções, opções com valores opcionais, ou combinações como -abc. Por isso, bibliotecas como getopt foram criadas para padronizar o processo.

getopt (visão geral)

A função getopt é uma biblioteca padrão do POSIX que simplifica o parsing de opções de linha de comando. Ela processa argumentos que começam com - (opções curtas) e pode ser estendida para opções longas com getopt_long. A função é declarada em <unistd.h> e usa variáveis globais como optarg, optind, opterr e optopt para controlar o processo.

O uso básico envolve uma string de opções, onde cada caractere representa uma opção. Se a opção requer um argumento, colocamos dois pontos (:) após o caractere. Por exemplo, "vo:" indica que a opção -v não tem argumento e -o tem. A função retorna o caractere da opção encontrada, ou -1 quando não há mais opções. O valor do argumento é colocado em optarg.

Vejamos o mesmo exemplo anterior usando getopt:

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

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

    while ((opt = getopt(argc, argv, "vo:")) != -1) {
        switch (opt) {
            case 'v':
                verbose = 1;
                break;
            case 'o':
                output = optarg;
                break;
            default:
                fprintf(stderr, "Uso: %s [-v] [-o arquivo]\n", argv[0]);
                return 1;
        }
    }

    printf("Output: %s\n", output ? output : "(nenhum)");
    printf("Verbose: %s\n", verbose ? "sim" : "não");
    return 0;
}

getopt automaticamente lida com argumentos inválidos, imprime mensagens de erro (a menos que opterr seja 0) e permite que argumentos não-opção sejam tratados depois, via optind. Para opções longas, existe getopt_long, que permite definir estruturas com nomes longos e abreviações.

Exemplos

Vamos criar um programa mais completo que demonstra o uso de getopt_long e o processamento de argumentos posicionais. Suponha que queremos uma ferramenta que aceite as opções --input <arquivo>, --output <arquivo>, --verbose e um argumento posicional (um diretório).

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

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

    static struct option long_options[] = {
        {"input", required_argument, 0, 'i'},
        {"output", required_argument, 0, 'o'},
        {"verbose", no_argument, 0, 'v'},
        {0, 0, 0, 0}
    };

    while ((opt = getopt_long(argc, argv, "i:o:v", long_options, NULL)) != -1) {
        switch (opt) {
            case 'i':
                input = optarg;
                break;
            case 'o':
                output = optarg;
                break;
            case 'v':
                verbose = 1;
                break;
            default:
                fprintf(stderr, "Uso: %s [--input arquivo] [--output arquivo] [--verbose] diretório\n", argv[0]);
                exit(EXIT_FAILURE);
        }
    }

    if (optind < argc) {
        printf("Diretório: %s\n", argv[optind]);
    } else {
        fprintf(stderr, "Erro: diretório não especificado\n");
        exit(EXIT_FAILURE);
    }

    if (input) printf("Input: %s\n", input);
    if (output) printf("Output: %s\n", output);
    if (verbose) printf("Modo verboso ativado\n");

    return 0;
}

Esse exemplo mostra como combinar opções curtas e longas, e como acessar os argumentos restantes após as opções. O programa imprime os valores fornecidos. Note que optind indica o índice do primeiro argumento não-opção, permitindo que o resto seja processado separadamente.

Boas práticas

Ao trabalhar com argumentos de linha de comando, siga estas boas práticas: valide sempre a presença de argumentos obrigatórios e forneça mensagens de erro claras; use getopt ou getopt_long para padronizar o parsing; documente as opções no help do programa; considere o uso de opções longas para melhor legibilidade; e lembre-se de que argv[0] pode ser usado em mensagens de erro para indicar o nome do programa.

Referências

Exercícios

  1. Escreva um programa que imprima todos os argumentos recebidos, um por linha, sem incluir o nome do programa. Use argc e argv.
  2. ✓ Resposta:
    #include <stdio.h>
    
    int main(int argc, char *argv[]) {
        for (int i = 1; i < argc; i++) {
            printf("%s\n", argv[i]);
        }
        return 0;
    }
  3. Implemente um programa que soma todos os argumentos que são números inteiros. Se algum argumento não for numérico, ignore-o. Dica: use atoi e verifique se a string é numérica.
  4. ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    #include <ctype.h>
    
    int is_number(const char *s) {
        if (*s == '-' || *s == '+') s++;
        if (*s == '\0') return 0;
        while (*s) {
            if (!isdigit(*s)) return 0;
            s++;
        }
        return 1;
    }
    
    int main(int argc, char *argv[]) {
        int sum = 0;
        for (int i = 1; i < argc; i++) {
            if (is_number(argv[i])) {
                sum += atoi(argv[i]);
            }
        }
        printf("Soma: %d\n", sum);
        return 0;
    }
  5. Crie um programa que aceite as opções -a (sem argumento) e -b <valor> (com argumento) usando getopt. Imprima se a opção -a foi ativada e o valor de -b se fornecido.
  6. ✓ Resposta:
    #include <stdio.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[]) {
        int opt;
        int a_flag = 0;
        const char *b_value = NULL;
    
        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;
            }
        }
    
        if (a_flag) printf("-a ativada\n");
        if (b_value) printf("-b valor: %s\n", b_value);
        return 0;
    }
  7. Modifique o programa do exercício 3 para também aceitar a opção longa --alpha (equivalente a -a) e --beta <valor> (equivalente a -b). Use getopt_long.
  8. ✓ Resposta:
    #include <stdio.h>
    #include <getopt.h>
    
    int main(int argc, char *argv[]) {
        int opt;
        int a_flag = 0;
        const char *b_value = NULL;
    
        static struct option long_options[] = {
            {"alpha", no_argument, 0, 'a'},
            {"beta", required_argument, 0, 'b'},
            {0, 0, 0, 0}
        };
    
        while ((opt = getopt_long(argc, argv, "ab:", long_options, NULL)) != -1) {
            switch (opt) {
                case 'a':
                    a_flag = 1;
                    break;
                case 'b':
                    b_value = optarg;
                    break;
                default:
                    fprintf(stderr, "Uso: %s [--alpha] [--beta valor]\n", argv[0]);
                    return 1;
            }
        }
    
        if (a_flag) printf("--alpha ativada\n");
        if (b_value) printf("--beta valor: %s\n", b_value);
        return 0;
    }
  9. Escreva um programa que aceite um ou mais nomes de arquivos como argumentos posicionais e imprima o tamanho de cada um (use stat). Se nenhum arquivo for passado, imprima uma mensagem de erro.
  10. ✓ Resposta:
    #include <stdio.h>
    #include <sys/stat.h>
    
    int main(int argc, char *argv[]) {
        if (argc < 2) {
            fprintf(stderr, "Uso: %s arquivo1 [arquivo2 ...]\n", argv[0]);
            return 1;
        }
    
        for (int i = 1; i < argc; i++) {
            struct stat st;
            if (stat(argv[i], &st) == 0) {
                printf("%s: %ld bytes\n", argv[i], (long)st.st_size);
            } else {
                perror("stat");
            }
        }
        return 0;
    }