# Programar ficheiros e diretórios

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

Página: https://resumos.rgo.pt/cadeiras/so/ficheiros-api/

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 $k$ linhas de um ficheiro regular, uma solução simples faz duas passagens: conta $L$ linhas, reposiciona e imprime as linhas com índice pelo menos $\max(0,L-k)$, contando desde zero. Para stdin que pode ser um pipe, usa uma fila circular das últimas $k$ 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ó $k$ 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 é $(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](https://resumos.rgo.pt/cadeiras/so/ficheiros-api/#ler-e-escrever-todos-os-bytes)

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](https://resumos.rgo.pt/cadeiras/so/ficheiros-api/#metadados-e-diret%C3%B3rios)

Ú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](https://resumos.rgo.pt/cadeiras/so/ficheiros-api/#linhas-com-getline)

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](https://resumos.rgo.pt/cadeiras/so/ficheiros-api/#file-e-descritor)

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](https://resumos.rgo.pt/cadeiras/so/ficheiros-api/#reposicionar-por-bytes)
