Projeto: processador de arquivos
LidoNesta aula, desenvolvemos um processador de arquivos em C, abrangendo leitura e escrita, transformações de dados, tratamento de erros e robustez. O projeto integra conceitos de manipulação de arquivos, alocação dinâmica e boas práticas de programação defensiva.
Nesta aula, vamos construir um processador de arquivos em C, um programa que lê um arquivo de entrada, aplica transformações e escreve o resultado em um arquivo de saída. Esse tipo de ferramenta é comum em sistemas que precisam processar logs, dados tabulares ou configurações. Vamos explorar desde a abertura segura de arquivos até o tratamento de erros e a criação de um programa robusto, que não quebre perante entradas inesperadas.
O projeto será incremental: primeiro, implementaremos a leitura e escrita básica; depois, adicionaremos transformações; em seguida, trataremos erros de forma sistemática; e, por fim, reforçaremos a robustez com validações e liberação correta de recursos. Ao final, você terá um programa completo e reutilizável.
Lendo e escrevendo
O primeiro passo é criar funções para abrir arquivos de entrada e saída. Em C, usamos fopen para abrir arquivos e fclose para fechá-los. É crucial verificar se a abertura foi bem-sucedida, pois o retorno pode ser NULL em caso de erro (por exemplo, arquivo inexistente ou permissão negada).
Para leitura, podemos usar fgets para ler linhas inteiras ou fgetc para ler caractere a caractere. Para escrita, fprintf é útil para formatar saída. Aqui está um esqueleto inicial:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Uso: %s <entrada> <saida>\n", argv[0]);
return 1;
}
FILE *in = fopen(argv[1], "r");
if (in == NULL) {
perror("Erro ao abrir arquivo de entrada");
return 1;
}
FILE *out = fopen(argv[2], "w");
if (out == NULL) {
perror("Erro ao abrir arquivo de saida");
fclose(in);
return 1;
}
char buffer[1024];
while (fgets(buffer, sizeof(buffer), in) != NULL) {
fputs(buffer, out);
}
fclose(in);
fclose(out);
return 0;
}Esse programa copia o arquivo de entrada para o de saída. Observe que usamos perror para imprimir a mensagem de erro do sistema e fprintf(stderr, ...) para mensagens de uso. Sempre fechamos os arquivos que abrimos, mesmo em caso de erro, para evitar vazamento de recursos.
Transformações
Um processador de arquivos geralmente aplica transformações nos dados. Vamos implementar duas transformações comuns: converter todas as letras para maiúsculas e remover linhas em branco. Para isso, criaremos funções que operam sobre strings.
A primeira função, toupper_line, percorre a string e converte cada caractere minúsculo em maiúsculo usando a função toupper de ctype.h. A segunda, is_blank, verifica se a linha contém apenas espaços, tabulações ou é vazia. Veja o código:
#include <ctype.h>
void toupper_line(char *s) {
for (int i = 0; s[i] != '\0'; i++) {
s[i] = toupper((unsigned char)s[i]);
}
}
int is_blank(const char *s) {
while (*s) {
if (!isspace((unsigned char)*s)) {
return 0;
}
s++;
}
return 1;
}No loop principal, aplicamos essas transformações antes de escrever a linha. Modificamos o código anterior:
while (fgets(buffer, sizeof(buffer), in) != NULL) {
if (is_blank(buffer)) {
continue;
}
toupper_line(buffer);
fputs(buffer, out);
}Essas transformações são simples, mas ilustram como processar linha por linha. Em projetos reais, você pode ter transformações mais complexas, como substituir padrões ou converter formatos, mas a estrutura permanece a mesma.
Tratamento de erros
Um bom programa deve lidar com erros de forma clara e controlada. Além de verificar a abertura dos arquivos, devemos verificar erros durante a leitura e a escrita. Por exemplo, fgets retorna NULL tanto em fim de arquivo quanto em erro de leitura. Para distinguir, usamos ferror.
Também é importante verificar se a escrita foi bem-sucedida, pois o disco pode estar cheio. A função fputs retorna EOF em erro. Vamos criar uma função auxiliar para tratar erros de forma consistente:
void handle_error(const char *msg) {
perror(msg);
exit(EXIT_FAILURE);
}
// No main:
if (in == NULL) handle_error("Erro ao abrir entrada");
if (out == NULL) handle_error("Erro ao abrir saida");
while (fgets(buffer, sizeof(buffer), in) != NULL) {
if (is_blank(buffer)) continue;
toupper_line(buffer);
if (fputs(buffer, out) == EOF) {
handle_error("Erro ao escrever no arquivo de saida");
}
}
if (ferror(in)) {
handle_error("Erro ao ler arquivo de entrada");
}Usar exit pode ser aceitável em programas pequenos, mas em bibliotecas ou programas maiores, é melhor retornar códigos de erro. Vamos refatorar para que as funções retornem status e o main decida como proceder.
Robustez
Robustez significa que o programa não falha inesperadamente e lida com entradas malformadas. Algumas práticas: validar argumentos, verificar tamanhos de buffer, usar fgets com limite, e garantir que todos os recursos sejam liberados. Vamos melhorar nosso código com uma função que processa o arquivo e retorna um código de erro, permitindo que o main limpe os recursos adequadamente.
int process_file(const char *in_path, const char *out_path) {
FILE *in = fopen(in_path, "r");
if (in == NULL) {
perror("Erro ao abrir entrada");
return 1;
}
FILE *out = fopen(out_path, "w");
if (out == NULL) {
perror("Erro ao abrir saida");
fclose(in);
return 1;
}
char buffer[1024];
int error = 0;
while (fgets(buffer, sizeof(buffer), in) != NULL) {
if (is_blank(buffer)) continue;
toupper_line(buffer);
if (fputs(buffer, out) == EOF) {
perror("Erro ao escrever");
error = 1;
break;
}
}
if (!error && ferror(in)) {
perror("Erro ao ler");
error = 1;
}
fclose(in);
fclose(out);
return error;
}
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Uso: %s <entrada> <saida>\n", argv[0]);
return 1;
}
return process_file(argv[1], argv[2]);
}Além disso, poderíamos adicionar validações adicionais, como verificar se os caminhos são diferentes, ou usar getline para lidar com linhas de tamanho arbitrário (embora getline seja POSIX, não padrão C). Para manter a portabilidade, usamos fgets com um buffer grande, mas isso limita o tamanho da linha. Uma alternativa robusta é usar fscanf ou ler caractere por caractere.
Outra questão de robustez é o tratamento de sinais ou interrupções, mas isso está fora do escopo desta aula.
Boas práticas e observações finais
Ao desenvolver processadores de arquivos, sempre: verifique a abertura e operações de I/O; libere recursos em todos os caminhos de erro; use perror para mensagens de erro; evite usar gets (perigoso) e prefira fgets com limite; e considere usar setvbuf para controlar o buffer.
Este projeto pode ser expandido para suportar múltiplas transformações, opções de linha de comando (como -u para maiúsculas) ou processamento em lote. A estrutura modular facilita a manutenção e o teste.
Referências
- fopen - cppreference.com
- fgets - cppreference.com
- fputs - cppreference.com
- perror - cppreference.com
- toupper - cppreference.com
- GNU C Library: Streams
- fopen - POSIX
Exercícios
- Implemente uma função
remove_extra_spacesque reduz múltiplos espaços consecutivos a um único espaço, e aplique-a antes de escrever cada linha.
void remove_extra_spaces(char *s) {
char *dest = s;
int space = 0;
while (*s) {
if (isspace((unsigned char)*s)) {
if (!space) {
*dest++ = ' ';
space = 1;
}
} else {
*dest++ = *s;
space = 0;
}
s++;
}
*dest = '\0';
}int main(int argc, char *argv[]) {
// ...
int count = 0;
while (fgets(buffer, sizeof(buffer), in) != NULL) {
if (is_blank(buffer)) continue;
toupper_line(buffer);
fputs(buffer, out);
count++;
}
printf("Linhas processadas: %d\n", count);
// ...
}int main(int argc, char *argv[]) {
if (argc < 3 || argc > 4) {
fprintf(stderr, "Uso: %s <entrada> <saida> [upper|lower]\n", argv[0]);
return 1;
}
int upper = 1;
if (argc == 4) {
if (strcmp(argv[3], "lower") == 0) upper = 0;
else if (strcmp(argv[3], "upper") != 0) {
fprintf(stderr, "Transformacao invalida\n");
return 1;
}
}
// ... no loop:
if (upper) toupper_line(buffer);
else tolower_line(buffer);
// ...
}copy_file que copie um arquivo usando fread e fwrite em blocos de 4096 bytes, e use-a no programa (substituindo a leitura linha a linha).int copy_file(const char *src, const char *dst) {
FILE *in = fopen(src, "rb");
if (!in) { perror("abrir entrada"); return 1; }
FILE *out = fopen(dst, "wb");
if (!out) { perror("abrir saida"); fclose(in); return 1; }
char buffer[4096];
size_t n;
while ((n = fread(buffer, 1, sizeof(buffer), in)) > 0) {
if (fwrite(buffer, 1, n, out) != n) {
perror("escrever");
fclose(in); fclose(out);
return 1;
}
}
if (ferror(in)) { perror("ler"); fclose(in); fclose(out); return 1; }
fclose(in); fclose(out);
return 0;
}#include <string.h>
// no main:
if (strcmp(argv[1], argv[2]) == 0) {
fprintf(stderr, "Erro: arquivos de entrada e saida devem ser diferentes\n");
return 1;
}