Nesta aula, vamos desenvolver uma ferramenta de linha de comando em C do zero. Esse tipo de programa é fundamental no dia a dia de quem trabalha com sistemas Unix/Linux, pois permite criar utilitários que podem ser combinados e automatizados. Ao final, você terá um programa que processa argumentos, reconhece opções, executa uma lógica específica e produz saída formatada, tudo com boas práticas de programação em C.

Vamos construir uma ferramenta chamada textinfo que analisa um arquivo de texto e exibe estatísticas (número de linhas, palavras e caracteres), com opções para escolher o que mostrar. Isso envolve manipulação de ponteiros, funções, e a interface com o sistema operacional através de argc e argv. A aula é prática e progressiva: primeiro entendemos os fundamentos, depois implementamos cada parte, e por fim testamos o resultado.

argc/argv

Em C, a função main pode receber parâmetros que representam os argumentos passados na linha de comando. A assinatura padrão é int main(int argc, char *argv[]). Aqui, argc (argument count) é o número de argumentos, incluindo o nome do programa, e argv (argument vector) é um array de strings contendo cada argumento. Por exemplo, se você executar ./programa -l arquivo.txt, teremos argc = 3 e argv[0] = "./programa", argv[1] = "-l", argv[2] = "arquivo.txt".

É essencial verificar argc para garantir que o usuário forneceu os argumentos esperados. Programas bem escritos sempre validam a quantidade mínima e tratam erros com mensagens claras. Vamos começar com um exemplo simples que imprime todos os argumentos recebidos.

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

Esse código mostra como acessar cada argumento. Em nosso projeto, usaremos argc e argv para capturar opções e o nome do arquivo a ser analisado.

Opções

Opções são argumentos que começam com um hífen (ou dois, em versões longas) e modificam o comportamento do programa. No C, não há suporte nativo para parsing de opções, então precisamos implementar nossa própria lógica. Uma abordagem comum é percorrer argv a partir do índice 1 e verificar se cada argumento começa com '-'.

Vamos definir as opções para nossa ferramenta: -l para mostrar linhas, -w para palavras, -c para caracteres, e -a para todas (equivalente a combinar as três). Também permitiremos combinar, como -lw. A implementação deve ser flexível e tratar opções inválidas com uma mensagem de uso.

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

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

    for (int i = 1; i < argc; i++) {
        if (argv[i][0] == '-') {
            for (int j = 1; argv[i][j] != '\0'; j++) {
                switch (argv[i][j]) {
                    case 'l': show_lines = 1; break;
                    case 'w': show_words = 1; break;
                    case 'c': show_chars = 1; break;
                    case 'a':
                        show_lines = show_words = show_chars = 1;
                        break;
                    default:
                        fprintf(stderr, "Opção inválida: -%c\n", argv[i][j]);
                        return 1;
                }
            }
        } else {
            filename = argv[i];
        }
    }

    if (filename == NULL) {
        fprintf(stderr, "Uso: %s [-l] [-w] [-c] arquivo\n", argv[0]);
        return 1;
    }

    printf("Arquivo: %s\n", filename);
    printf("Mostrar linhas: %d, palavras: %d, caracteres: %d\n",
           show_lines, show_words, show_chars);
    return 0;
}

Esse código reconhece opções combinadas e armazena o nome do arquivo. Em uma ferramenta real, você pode usar bibliotecas como getopt para facilitar, mas aqui implementamos manualmente para entender o mecanismo.

Lógica

A lógica principal do programa é abrir o arquivo, ler seu conteúdo e contar linhas, palavras e caracteres. Vamos implementar uma função que recebe o nome do arquivo e retorna essas estatísticas. Para contar palavras, consideraremos como delimitadores espaços, tabulações e novas linhas. Usaremos fgetc para ler caractere por caractere, controlando o estado anterior para evitar contar sequências de espaços como múltiplas palavras.

#include <stdio.h>
#include <ctype.h>

typedef struct {
    long lines;
    long words;
    long chars;
} Stats;

int get_stats(const char *filename, Stats *stats) {
    FILE *fp = fopen(filename, "r");
    if (fp == NULL) {
        perror("Erro ao abrir arquivo");
        return 0;
    }

    stats->lines = 0;
    stats->words = 0;
    stats->chars = 0;

    int ch;
    int in_word = 0;
    while ((ch = fgetc(fp)) != EOF) {
        stats->chars++;
        if (ch == '\n') {
            stats->lines++;
        }
        if (isspace(ch)) {
            in_word = 0;
        } else if (!in_word) {
            in_word = 1;
            stats->words++;
        }
    }

    // Se o arquivo não terminar com nova linha, conta a última linha
    if (stats->chars > 0 && (ch != '\n')) {
        stats->lines++;
    }

    fclose(fp);
    return 1;
}

Essa função usa uma estrutura para armazenar as contagens. A lógica de palavras é clássica: quando encontramos um caractere não-espaço e não estávamos em uma palavra, incrementamos o contador. O tratamento da última linha garante que um arquivo sem nova linha final seja contado corretamente.

No main, chamamos get_stats e depois decidimos o que imprimir com base nas opções ativas.

Saída formatada

Para exibir os resultados de forma clara e profissional, usamos a formatação de printf. Em ferramentas Unix, é comum alinhar os números à direita e usar espaçamento consistente. Vamos criar uma saída similar ao comando wc (word count).

void print_stats(const Stats *stats, int show_lines, int show_words, int show_chars) {
    // Se nenhuma opção foi dada, mostra tudo por padrão
    if (!show_lines && !show_words && !show_chars) {
        show_lines = show_words = show_chars = 1;
    }

    if (show_lines) printf("%8ld ", stats->lines);
    if (show_words) printf("%8ld ", stats->words);
    if (show_chars) printf("%8ld ", stats->chars);
    printf("\n");
}

Essa função imprime cada estatística com largura mínima de 8 caracteres, alinhada à direita. O usuário pode especificar quais colunas deseja ver. Para um arquivo de exemplo, a saída pode ser:

     10       50      300

Isso indica 10 linhas, 50 palavras e 300 caracteres. Em nosso projeto completo, vamos integrar todas as partes e adicionar uma mensagem de ajuda quando necessário.

O código final da ferramenta textinfo pode ser estruturado assim:

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

typedef struct {
    long lines;
    long words;
    long chars;
} Stats;

int get_stats(const char *filename, Stats *stats);
void print_stats(const Stats *stats, int show_lines, int show_words, int show_chars);
void usage(const char *progname);

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

    for (int i = 1; i < argc; i++) {
        if (argv[i][0] == '-') {
            for (int j = 1; argv[i][j] != '\0'; j++) {
                switch (argv[i][j]) {
                    case 'l': show_lines = 1; break;
                    case 'w': show_words = 1; break;
                    case 'c': show_chars = 1; break;
                    case 'a':
                        show_lines = show_words = show_chars = 1;
                        break;
                    default:
                        fprintf(stderr, "Opção inválida: -%c\n", argv[i][j]);
                        usage(argv[0]);
                        return 1;
                }
            }
        } else {
            filename = argv[i];
        }
    }

    if (filename == NULL) {
        usage(argv[0]);
        return 1;
    }

    Stats stats;
    if (!get_stats(filename, &stats)) {
        return 1;
    }

    print_stats(&stats, show_lines, show_words, show_chars);
    return 0;
}

int get_stats(const char *filename, Stats *stats) {
    FILE *fp = fopen(filename, "r");
    if (fp == NULL) {
        perror("Erro ao abrir arquivo");
        return 0;
    }

    stats->lines = 0;
    stats->words = 0;
    stats->chars = 0;

    int ch;
    int in_word = 0;
    while ((ch = fgetc(fp)) != EOF) {
        stats->chars++;
        if (ch == '\n') {
            stats->lines++;
        }
        if (isspace(ch)) {
            in_word = 0;
        } else if (!in_word) {
            in_word = 1;
            stats->words++;
        }
    }

    // Se o arquivo não terminar com nova linha, conta a última linha
    if (stats->chars > 0 && ch != '\n') {
        stats->lines++;
    }

    fclose(fp);
    return 1;
}

void print_stats(const Stats *stats, int show_lines, int show_words, int show_chars) {
    if (!show_lines && !show_words && !show_chars) {
        show_lines = show_words = show_chars = 1;
    }

    if (show_lines) printf("%8ld ", stats->lines);
    if (show_words) printf("%8ld ", stats->words);
    if (show_chars) printf("%8ld ", stats->chars);
    printf("\n");
}

void usage(const char *progname) {
    fprintf(stderr, "Uso: %s [-l] [-w] [-c] [-a] arquivo\n", progname);
    fprintf(stderr, "  -l: mostra número de linhas\n");
    fprintf(stderr, "  -w: mostra número de palavras\n");
    fprintf(stderr, "  -c: mostra número de caracteres\n");
    fprintf(stderr, "  -a: mostra todas as estatísticas (padrão)\n");
}

Boas práticas e observações

Ao criar ferramentas de linha de comando, sempre forneça mensagens de erro claras e uma função de uso (usage). Valide todos os argumentos e trate erros de arquivo com perror para incluir a descrição do sistema. Além disso, considere usar const em parâmetros que não serão modificados, e divida o código em funções pequenas e bem definidas.

Para projetos maiores, você pode usar getopt da biblioteca padrão para facilitar o parsing de opções, mas a implementação manual é didática e útil em sistemas embarcados.

Teste sua ferramenta com diferentes arquivos e combinações de opções para garantir robustez.

Referências

Exercícios

  1. Modifique o programa para aceitar a opção -n que exibe apenas o nome do arquivo após as estatísticas. Exemplo: ./textinfo -l -n arquivo.txt deve imprimir 10 arquivo.txt.
  2. ✓ Resposta: Adicione uma variável show_name e no parsing reconheça '-n'. Na impressão, após os números, se show_name estiver ativo, imprima o nome do arquivo. Você precisará passar o nome para print_stats ou imprimir no main.
  3. Implemente uma opção -L que mostra o comprimento da linha mais longa do arquivo.
  4. ✓ Resposta: Na função get_stats, adicione um campo max_line_len. Ao ler cada caractere, mantenha um contador de comprimento da linha atual; quando encontrar nova linha ou EOF, compare com o máximo. Ajuste a estrutura e a impressão.
  5. Altere o programa para ler da entrada padrão quando nenhum arquivo for fornecido. Dica: use stdin como FILE*.
  6. ✓ Resposta: Se filename for NULL, defina fp = stdin em vez de abrir arquivo. Modifique get_stats para receber um FILE* já aberto, ou crie uma função auxiliar que usa stdin. Cuidado para não fechar stdin.
  7. Adicione tratamento para múltiplos arquivos, exibindo estatísticas individuais e totais no final (como wc).
  8. ✓ Resposta: Colete todos os nomes de arquivo em um array (ou processe cada um e acumule totais). Após processar todos, se houver mais de um, imprima uma linha de totais. Use uma função para imprimir cada arquivo e outra para totais.
  9. Crie uma opção -h que exibe a ajuda e sai sem erro, usando exit(EXIT_SUCCESS).
  10. ✓ Resposta: No loop de parsing, se encontrar '-h', chame a função usage e depois exit(0). Coloque o case 'h' no switch.