Programar ficheiros e diretórios

Biblioteca C, descritores POSIX, transferências parciais, linhas, metadados e caminhos.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

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 CAPI POSIXDiferença
fopenopenDevolve FILE * ou int
fread, fwriteread, writeElementos na biblioteca, bytes na API
fseek, ftelllseekPosição do fluxo ou da descrição aberta
fcloseclosefclose 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 kk linhas de um ficheiro regular, uma solução simples faz duas passagens: conta LL linhas, reposiciona e imprime as linhas com índice pelo menos max⁡(0,L−k)\max(0,L-k), contando desde zero. Para stdin que pode ser um pipe, usa uma fila circular das últimas kk 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ó kk 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 é (s2−s1)+(n2−n1)/109(s_2-s_1)+(n_2-n_1)/10^9. 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
A quantidade devolvida conta o prefixo já enviado.
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 iguais
iguais

O 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.

Voltar à explicação
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.

Voltar à explicação
Ú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.

  1. 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).
  2. A entrada passa a ser um pipe. Que passo deixa de funcionar e que estado precisas de guardar?

Pista
Distingue o índice de início do número de linhas realmente lidas.
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.
Voltar à explicação
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
Na última leitura, só o primeiro byte do buffer é substituído.
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.
Voltar à explicação
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.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.