Programar ficheiros e diretórios
Biblioteca C, descritores POSIX, transferências parciais, linhas, metadados e caminhos.
Queremos copiar oito bytes para a saída padrão. Se a primeira escrita entregar só três, o programa tem de conservar os cinco restantes e retomar no endereço certo. A saída padrão costuma estar ligada ao terminal, mas a shell pode ligá-la a um ficheiro ou pipe.
Podemos usar a biblioteca C, que representa um fluxo por FILE * e pode acumular bytes num buffer, ou as interfaces POSIX open, read e write, que usam um descritor inteiro. POSIX normaliza interfaces de biblioteca e de sistema.
FILE e descritor
| Biblioteca C | API POSIX | Diferença |
|---|---|---|
fopen | open | Devolve FILE * ou int |
fread, fwrite | read, write | Elementos na biblioteca, bytes na API |
fseek, ftell | lseek | Posição do fluxo ou da descrição aberta |
fclose | close | fclose também trata buffers da biblioteca |
fopen falha com NULL; open falha com -1. O descritor 0 é válido. Não uses if (!fd) para testar um erro de abertura.
fread(buf, tamanho, quantidade, fp) devolve o número de elementos completos lidos. Com tamanho == 1, esse valor também conta bytes. Uma leitura curta pode significar fim de ficheiro ou erro; consulta feof e ferror. read devolve ssize_t: positivo é a quantidade de bytes, zero indica EOF e -1 indica erro. Guarda o resultado num tipo com sinal antes de o interpretar. Num pedido de zero bytes, read pode devolver zero sem testar EOF; os ciclos desta página pedem sempre uma quantidade positiva.
A biblioteca pode antecipar leituras e acumular escritas num buffer, por isso evita alternar fread e read sobre a mesma abertura sem compreenderes a sincronização: a posição no núcleo pode já estar adiante dos bytes que a biblioteca entregou à aplicação.
Ler e escrever todos os bytes
Uma chamada a write(fd, buf, n) não garante que escreve n bytes, porque o núcleo pode aceitar só uma parte ou a chamada pode falhar com EINTR. Traça um pedido de 8 bytes em que a primeira tentativa devolve 3: faltam 5, por isso a próxima tentativa usa o endereço buf + 3 com quantidade 5. Repetir os 8 duplicaria os 3 já entregues. O programa seguinte copia a entrada padrão para a saída padrão e conserva a parte ainda não escrita.
Guarda-o como copiar.c e compila num terminal UNIX com cc -std=c17 -Wall -Wextra -Wpedantic copiar.c -o copiar.
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
int main(void) {
char buf[4096];
for (;;) {
ssize_t n = read(STDIN_FILENO, buf, sizeof buf);
if (n < 0) {
if (errno == EINTR) continue;
perror("read");
return 1;
}
if (n == 0) return 0;
ssize_t enviados = 0;
while (enviados < n) {
ssize_t k = write(STDOUT_FILENO, buf + enviados,
(size_t)(n - enviados));
if (k < 0 && errno == EINTR) continue;
if (k <= 0) {
if (k < 0) perror("write");
else fputs("write nao progrediu\n", stderr);
return 1;
}
enviados += k;
}
}
}
printf 'abc\nxyz\n' | ./copiar > copia.txt
cat copia.txt
A saída são as duas linhas originais. Não acrescentámos \0 ao buffer, porque trabalhamos com a quantidade lida, não com uma string. Um ficheiro vazio termina na primeira leitura com zero.
Uma versão que recebe um nome precisa de validar argc, abrir com O_RDONLY, usar o descritor nas leituras e fechar os descritores que abriu. Para criar um destino novo ou substituir o conteúdo antigo, abre primeiro sem truncar, com O_WRONLY | O_CREAT e o argumento de permissões, por exemplo 0666, modificado pela umask. Compara depois a identidade das duas aberturas com fstat, pelos pares st_dev e st_ino: dois nomes, incluindo hard links, podem identificar o mesmo ficheiro. Só trunca quando confirmares que são ficheiros distintos, para não destruires a origem antes de a ler.
Posição e reabertura
Numa abertura de ficheiro regular, a posição começa normalmente em zero. Cada leitura avança-a pela quantidade devolvida. lseek(fd, 0, SEEK_SET) volta ao início e falha com (off_t)-1.
Duas chamadas independentes a open sobre o mesmo ficheiro podem coexistir e têm posições independentes. Já dup e os descritores herdados por fork referem a mesma descrição de ficheiro aberto e partilham a posição. Fecha o que deixas de usar para evitar perder descritores, não porque seja proibido abrir o nome novamente.
Na biblioteca, rewind(fp) volta ao início e limpa indicadores de erro e EOF. fseek permite reposicionamento com retorno que deves verificar. Pipes e terminais não se comportam como ficheiros regulares reposicionáveis.
Reposicionar por bytes
lseek(fd, deslocamento, origem) define a posição e devolve a nova posição absoluta. SEEK_SET parte do início, SEEK_CUR da posição atual e SEEK_END do tamanho lógico. Num ficheiro de cinco bytes abcde, lseek(fd, -2, SEEK_END) coloca a posição em 3; uma leitura de um byte entrega d e avança para 4.
EOF não é um carácter guardado no fim. Na posição 5, uma leitura com pedido positivo devolve 0. Para inverter os bytes, começa na posição tamanho-1, lê e escreve um byte, depois reposiciona em tamanho-2. A leitura também avança a posição, por isso repetir apenas lseek(fd, -1, SEEK_CUR) pode reler sempre o mesmo byte. Guarda o índice que pretendes visitar, do último até zero.
Usa off_t para posições e testa (off_t)-1 como erro. Um ficheiro vazio não tem último byte. Um ficheiro regular pode admitir posições além do seu fim; só reposicionar não aumenta o tamanho. Uma escrita posterior pode criar uma região esparsa, cujas leituras dão bytes zero. Pipes não admitem esse reposicionamento.
Esta inversão é de bytes. Inverter os bytes de um carácter UTF-8 com vários bytes não preserva o texto. Um programa que promete inverter caracteres precisa de tratar a codificação.
Linhas com getline
getline é POSIX, não C17. Precisa de _POSIX_C_SOURCE 200809L antes dos includes quando o ambiente exige macros de funcionalidades. Recebe um buffer que pode reservar ou aumentar.
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
int main(void) {
char *linha = NULL;
size_t capacidade = 0, numero = 0;
ssize_t n;
while ((n = getline(&linha, &capacidade, stdin)) != -1) {
if (printf("%zu: ", ++numero) < 0 ||
fwrite(linha, 1, (size_t)n, stdout) != (size_t)n) {
free(linha);
return 1;
}
}
int erro = ferror(stdin) || !feof(stdin);
if (fflush(stdout) == EOF) erro = 1;
free(linha);
return erro ? 1 : 0;
}
O comprimento inclui o \n se existir e exclui o terminador \0. O programa conserva uma última linha sem mudança de linha. getline também pode ler bytes zero; funções de strings parariam cedo nesse caso. Liberta o buffer mesmo quando a leitura termina em EOF. O retorno -1 de getline também pode ser erro, incluindo falha de reserva: só consideramos o fim normal se feof estiver ativo e não houver erro de leitura. fflush verifica ainda a entrega da saída que ficou em buffer.
Para imprimir as últimas linhas de um ficheiro regular, uma solução simples faz duas passagens: conta linhas, reposiciona e imprime as linhas com índice pelo menos , contando desde zero. Para stdin que pode ser um pipe, usa uma fila circular das últimas linhas, porque não podes contar e voltar atrás. O teste de leitura é o ciclo exterior; não coloques um ciclo que lê todo o ficheiro dentro de outro que supostamente lê só linhas.
Strings, opções e pesquisa
getopt permite processar opções como -l e -c e deixa optind no primeiro argumento que não é opção. Define no enunciado se -c conta bytes ou caracteres e o que é uma palavra. Contar palavras por transições de espaço para não espaço trata espaços repetidos sem inventar palavras vazias.
Para procurar uma substring numa linha, strstr encontra a próxima ocorrência. Se permites sobreposição, avança um byte depois do início encontrado; se não permites, avança o comprimento da substring. Rejeita a substring vazia ou define o seu significado antes do ciclo.
Metadados e diretórios
stat(caminho, &info) preenche a estrutura apenas quando tem sucesso. st_size mede o tamanho lógico em bytes. st_blocks mede blocos de 512 bytes nas interfaces UNIX usuais e pode diferir do tamanho por causa de ficheiros esparsos. Usa S_ISREG(info.st_mode) e S_ISDIR(info.st_mode), não testes improvisados aos bits do tipo.
stat segue um link simbólico; lstat consulta o próprio link. Para um descritor já aberto, fstat evita voltar a resolver o nome.
opendir devolve DIR *; readdir devolve uma entrada ou NULL; closedir fecha. d_name é apenas o nome da entrada, não o caminho completo. Se abriste pasta estando fora dela, stat(entrada->d_name, ...) procuraria no diretório de trabalho errado.
Podes juntar o diretório e o nome, verificando comprimentos, ou usar fstatat(dirfd(dir), entrada->d_name, &info, 0). Esta segunda opção resolve o nome relativamente ao diretório aberto. Trata . e .. antes de uma travessia recursiva e define se segues links simbólicos, para não criar ciclos. Imediatamente antes de cada chamada a readdir, pôr errno = 0 permite distinguir um fim normal de um erro quando retorna NULL.
O programa seguinte lista uma pasta com fstatat, sem mudar o diretório de trabalho. Guarda-o em listar.c e compila num terminal UNIX com cc -std=c17 -Wall -Wextra -Wpedantic listar.c -o listar.
#define _POSIX_C_SOURCE 200809L
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "uso: %s pasta\n", argv[0]);
return 1;
}
DIR *d = opendir(argv[1]);
if (d == NULL) {
perror("opendir");
return 1;
}
int dfd = dirfd(d);
int erro = 0;
struct dirent *e;
for (;;) {
errno = 0;
e = readdir(d);
if (e == NULL) {
if (errno != 0) { perror("readdir"); erro = 1; }
break;
}
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) {
continue;
}
struct stat info;
if (fstatat(dfd, e->d_name, &info, 0) < 0) {
perror("fstatat");
erro = 1;
continue;
}
printf("%s: %lld bytes%s\n", e->d_name, (long long)info.st_size,
S_ISDIR(info.st_mode) ? " [dir]" : "");
}
if (closedir(d) < 0) { perror("closedir"); erro = 1; }
return erro;
}
Execução observada numa pasta com a.txt de 4 bytes, um link simbólico para a.txt e uma subpasta (a ordem de readdir varia; aqui ordenada para leitura):
mkdir -p demo/sub
printf 'ola\n' > demo/a.txt
ln -sf a.txt demo/lig.txt
./listar demo | sort
a.txt: 4 bytes
lig.txt: 4 bytes
sub: 64 bytes [dir]
O fstatat sem flags segue o link e mede o destino. O tamanho em bytes de um diretório depende do sistema de ficheiros; o marcador [dir] vem de S_ISDIR. Os campos de info só são lidos após sucesso. Um link pendente faz fstatat falhar; o programa assinala essa falha, continua a listar e termina com estado 1. O errno é reposto imediatamente antes de cada readdir, para que esse erro de metadados não seja confundido com uma falha da travessia.
Datas e medição
Um time_t representa tempo de calendário; em POSIX conta segundos desde 1970-01-01 00:00:00 UTC. localtime converte para campos locais, com tm_year relativo a 1900 e tm_mon entre 0 e 11. strftime formata esses campos. O tamanho concreto de time_t depende do sistema.
Para duração decorrida, prefere clock_gettime(CLOCK_MONOTONIC, ...): o relógio civil pode ser corrigido. Uma diferença de timespec em segundos é . Tempo de CPU mede trabalho executado pelo processo; tempo decorrido também inclui esperas. Um programa bloqueado pode demorar um segundo e consumir quase nenhum tempo de CPU.
Exercícios
Uma escrita incompleta
Tens oito bytes em buf. A primeira write devolve 3. Indica o endereço e a quantidade da próxima tentativa e explica por que não repetes os oito. Depois implementa uma cópia entre dois ficheiros regulares, com origem e destino recebidos como argumentos. Completa escritas parciais, trata erros e rejeita dois nomes do mesmo ficheiro antes de truncar o destino. Assume que nenhum outro processo altera os ficheiros durante a cópia. Usa o terminal UNIX.
Pista
Mais uma pista
Não podes reenviar esse prefixo sem duplicar dados.
Solução
Traça primeiro um pedido de 8 bytes em que a primeira write devolve 3: faltam 5, por isso a próxima tentativa usa o endereço buf + 3 com quantidade 5. Repetir os 8 duplicaria os 3 já entregues. O retorno usa ssize_t e deve ser testado antes de converter para size_t, porque -1 significa erro.
Guarda este programa em copiar2.c e compila com cc -std=c17 -Wall -Wextra -Wpedantic copiar2.c -o copiar2.
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
static int escrever_tudo(int fd, const char *buf, size_t n) {
size_t feito = 0;
while (feito < n) {
ssize_t k = write(fd, buf + feito, n - feito);
if (k < 0 && errno == EINTR)
continue;
if (k <= 0) {
if (k < 0)
perror("write");
else
fputs("write nao progrediu\n", stderr);
return -1;
}
feito += (size_t)k;
}
return 0;
}
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "uso: %s origem destino\n", argv[0]);
return 1;
}
int origem = open(argv[1], O_RDONLY);
if (origem < 0) {
perror("open origem");
return 1;
}
int destino = open(argv[2], O_WRONLY | O_CREAT, 0666);
if (destino < 0) {
perror("open destino");
close(origem);
return 1;
}
struct stat a, b;
if (fstat(origem, &a) < 0 || fstat(destino, &b) < 0) {
perror("fstat");
close(origem); close(destino);
return 1;
}
if (!S_ISREG(a.st_mode) || !S_ISREG(b.st_mode) ||
(a.st_dev == b.st_dev && a.st_ino == b.st_ino)) {
fputs("usa dois ficheiros regulares distintos\n", stderr);
close(origem); close(destino);
return 1;
}
if (ftruncate(destino, 0) < 0) {
perror("ftruncate");
close(origem); close(destino);
return 1;
}
char buf[4096];
int erro = 0;
for (;;) {
ssize_t n = read(origem, buf, sizeof buf);
if (n < 0 && errno == EINTR)
continue;
if (n < 0) {
perror("read");
erro = 1;
break;
}
if (n == 0)
break;
if (escrever_tudo(destino, buf, (size_t)n) < 0) {
erro = 1;
break;
}
}
if (close(origem) < 0) {
perror("close origem");
erro = 1;
}
if (close(destino) < 0) {
perror("close destino");
erro = 1;
}
return erro;
}Execução observada com o programa compilado sem avisos:
printf 'abc\nxyz\n' > entrada.txt
./copiar2 entrada.txt saida.txt
cmp entrada.txt saida.txt && echo iguaisiguaisO programa restringe esta cópia a ficheiros regulares e assume que nenhum outro processo os altera durante a operação. Compara st_dev e st_ino das aberturas antes de truncar: nomes diferentes podem ser hard links do mesmo ficheiro. O mesmo nome e um hard link da origem são rejeitados sem alterar os seus bytes. A origem abre-se antes do destino: com origem em falta, o programa termina com estado 1 e conserva o destino existente. Com ficheiro vazio, a primeira leitura devolve 0 e o destino fica com 0 bytes. Em cada passo, o descritor da origem serve as leituras, o do destino serve as escritas completadas pelo ciclo, e o ciclo termina no EOF.
Confere a tua resposta:
- Retomei no endereço buf + 3.
- Pedi os cinco bytes restantes.
- Conservei a interpretação de erro com sinal.
- Rejeitei o mesmo par st_dev e st_ino antes de truncar.
- Completei as transferências em ciclo, tratei erros e fechei os descritores.
Erros frequentes
Assumir que um retorno positivo sempre iguala o pedido, ou converter -1 para uma quantidade enorme.
Consultar uma entrada de outra pasta
O diretório de trabalho é /tmp e abriste /home/ana/notas com
opendir.readdir devolve
aula.txt. Porque pode
stat("aula.txt", &info) falhar apesar de a entrada existir?
Pista
Os dois pedidos podem usar pontos de partida diferentes.
Mais uma pista
stat com caminho relativo usa o diretório de trabalho.
Solução
stat procuraria /tmp/aula.txt. Usa o caminho
/home/ana/notas/aula.txt, construído com espaço suficiente,
ou fstatat relativamente ao descritor do diretório aberto. Só lê os campos
de info se a consulta teve sucesso.
Confere a tua resposta:
- Expliquei que d_name não é o caminho completo.
- Resolvi relativamente ao diretório aberto ou juntei o caminho.
- Só usei struct stat após sucesso.
Erros frequentes
Achar que opendir muda o diretório de trabalho ou que a
estrutura já contém dados antes de stat.
Últimas linhas e entrada sem reposicionamento
Queremos imprimir as últimas k linhas, com k positivo. Uma última sequência de bytes sem newline conta como linha.
- Num ficheiro regular, propõe um algoritmo de duas passagens e calcula o primeiro índice, contado desde zero, para
(L,k)=(3,5),(8,3)e(0,3). - A entrada passa a ser um pipe. Que passo deixa de funcionar e que estado precisas de guardar?
Pista
Solução
Na primeira passagem, lê com getline até EOF e conta apenas os retornos bem-sucedidos. Reposiciona no início e, na segunda, imprime a partir de max(0,L-k): os índices são 0, 5 e 0. No ficheiro vazio não há leitura bem-sucedida, portanto o índice 0 não leva a imprimir uma linha inexistente.
Um pipe não permite voltar ao início. Usa uma fila circular de capacidade k com as últimas linhas e os seus comprimentos. Cada nova linha substitui a mais antiga quando a fila enche. No fim, imprime da mais antiga conservada para a mais recente; para a última linha sem newline, escreve exatamente o comprimento lido.
Confere a tua resposta:
- Calculei o primeiro índice para ficheiros curtos e vazios.
- Limitei a impressão às leituras bem-sucedidas.
- Expliquei por que um pipe exige guardar as últimas linhas.
A última leitura não enche o buffer
Um ficheiro regular contém exatamente os nove bytes abcdefghi, sem newline. O programa lê com fread(buffer, 1, 4, origem) e, para cada retorno positivo n, faz fwrite(buffer, 1, 4, destino). Assume leituras sem erro, escritas completas e ficheiro sem alterações concorrentes.
Indica os retornos das leituras e os bytes escritos no destino. Corrige a quantidade passada a fwrite. Depois deixa de assumir que não há erros: que indicadores consultas quando fread devolve menos do que pediste?
Pista
Solução
As leituras devolvem 4, 4, 1 e 0. Os buffers que chegam à escrita contêm abcd, efgh e ifgh: o último i substitui e, mas os outros três bytes conservam o que lá estava. O programa escreve abcdefghifgh, 12 bytes, em vez dos nove pedidos.
Usa fwrite(buffer, 1, n, destino) e verifica que o retorno é n. Assim escreves abcd, efgh e i, obtendo abcdefghi. n tem tipo size_t porque fread conta elementos; aqui cada elemento tem um byte. Não uses %s para transferir o buffer, que não recebeu terminador zero.
Uma leitura curta exige consultar ferror(origem) para detetar erro e feof(origem) para reconhecer fim. O ciclo ainda pode ter recebido bytes úteis antes dessa condição. Verifica também erros da saída, incluindo a descarga final do buffer.
Confere a tua resposta:
- Obtive leituras de 4, 4, 1 e depois 0 bytes.
- Escrevi apenas n bytes e obtive os nove bytes originais.
- Distingui fim de ficheiro de erro de leitura.
O recuo que relê o mesmo byte
O ficheiro contém os três bytes abc. Queres imprimi-los por ordem inversa. Pões a posição no fim e repetes três vezes: lseek(fd, -1, SEEK_CUR), leitura de um byte e escrita desse byte na saída. Assume sucesso e leituras de um byte.
Que saída obténs? Dá a sequência correta de posições para obter cba e explica o caso vazio. Inverter assim os bytes de qualquer texto UTF-8 preservaria os caracteres?
Solução
Obténs ccc. A posição inicial é 3; o recuo põe-na em 2, mas a leitura de c volta a avançá-la para 3. Cada repetição volta ao mesmo byte.
Guarda o tamanho num off_t e visita os índices 2, 1 e 0 com lseek(fd, indice, SEEK_SET) antes de cada leitura. Um ciclo que começa com pos=tamanho, continua enquanto pos > 0 e decrementa pos antes de a usar evita tentar uma posição negativa no caso vazio. Confere reposicionamento, leitura e escrita no programa completo.
A inversão é de bytes. Um carácter UTF-8 pode ter vários bytes cuja ordem interna é necessária, portanto esta operação não é uma inversão geral de caracteres.
Confere a tua resposta:
- Localizei c na posição 2 e o avanço da leitura para 3.
- Mostrei que recuar um byte a partir de 3 relê c.
- Visitei explicitamente as posições 2, 1 e 0 e tratei o ficheiro vazio.