Projeto: ferramenta de linha de comando em C
LidoNesta aula, você construirá uma ferramenta de linha de comando completa em C, aprendendo a lidar com argumentos (argc/argv), implementar opções, estruturar a lógica do programa e produzir saídas formatadas. O projeto prático consolida conceitos essenciais para criar utilitários profissionais.
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
- GNU C Library: Program Arguments
- cppreference: main function
- Linux man page: getopt
- GNU getopt documentation
- Linux man page: wc
Exercícios
- Modifique o programa para aceitar a opção
-nque exibe apenas o nome do arquivo após as estatísticas. Exemplo:./textinfo -l -n arquivo.txtdeve imprimir10 arquivo.txt. - Implemente uma opção
-Lque mostra o comprimento da linha mais longa do arquivo. - Altere o programa para ler da entrada padrão quando nenhum arquivo for fornecido. Dica: use
stdincomo FILE*. - Adicione tratamento para múltiplos arquivos, exibindo estatísticas individuais e totais no final (como
wc). - Crie uma opção
-hque exibe a ajuda e sai sem erro, usandoexit(EXIT_SUCCESS).
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.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.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.usage e depois exit(0). Coloque o case 'h' no switch.