2009-04-05

Ficheiros, parte I

O primeiro exercício do terceiro guião diz o seguinte: Implemente em C um programa com a funcionalidade do cat. O objectivo é fazer isso, mas sem usar a stdio.h (printf, scanf, etc). No lugar dessas funções temos de usar as chamadas ao sistema que estão no guião.

O que é o cat?

Vamos começar então por perceber exactamente o que temos de fazer:

$ whatis cat
cat - concatenate files and print on the standard output
man cat

O cat concatena os ficheiros passados como argumento e imprime-os no standard output. Se não tiver argumentos, imprime o que ler do standard input. Simples.

Estrutura geral

Vamos ter então um ciclo que imprime os ficheiros passados como argumento um a um.

int main(int argc, char** argv)
{
int i;

if(argc == 1)
{
// ler do standard input para o standard output
}

// Começar em 1 porque argv[0] é o nome do executável ("cat")
for(i = 1; i < argc; i++)
{
// ler do ficheiro argv[i] para o standard output
}

return 0;
}

Uma visita ao manual

Vamos agora ver como funcionam as system calls que temos à nossa disposição.

$ man open

Resumidamente, abre um ficheiro e devolve um descritor para esse ficheiro. Um descritor é um número inteiro não negativo que representa um ficheiro. Para cada processo o kernel mantém uma lista de ficheiros abertos. Podem ver o descritor como o índice de um ficheiro nessa lista.

Os descritores 0, 1 e 2 são especiais e representam, respectivamente, o standard input, o standard output e o standard error.

O primeiro parâmetro é o caminho do ficheiro. O segundo, flags, dita o modo de acesso. Deve obrigatoriamente incluir uma destas três flags:

  • O_RDONLY: para abrir só para leitura;
  • O_WRONLY: para abrir só para escrita;
  • O_RDWR: para abrir para leitura e escrita.

Há outras flags opcionais, das quais as mais relevantes são:

  • O_APPEND: serve para acrescentar ao ficheiro;
  • O_CREAT: cria o ficheiro se este não existir;

Para especificar mais do que uma flag usa-se o operador | (bitwise or), assim:

    fd = open("file", O_WRONLY | O_APPEND);

Quando se usa O_CREAT para criar um ficheiro deve usar um terceiro parâmetro que diz o modo (aka, as permissões):

    fd = open("file", O_WRONLY | O_CREAT, 0700);

O modo é o mesmo que se usa com o comando chmod, em octal (daí o 0 à esquerda). Se preferirem existem algumas constantes predefinidas para vários modos. Essas constantes são listadas na página do manual.

$ man close

Fecha um ficheiro dado o seu descritor. Devolve zero se tiver sucesso.

$ man read

Ooops... Página errada. Este read não é o que estamos à procura. O que nós queremos está na secção 2: system calls.

$ man 2 read

Agora sim. read lê uma quantidade de bytes de um ficheiro para um buffer e devolve o número de bytes lidos. Quando o ficheiro chegar ao fim, devolve 0.

read pode ler menos bytes do que foram pedidos porque podemos estar a ler do teclado, de um pipe, de um socket, etc. Se não souberem o que são pipes ou sockets, não faz mal. Basta que saibam que read nem sempre lê tantos bytes como pedimos.

$ man 2 write

Escreve uma quantidade de bytes de um buffer para um ficheiro e devolve o número de bytes escritos.

Tal como read, write também pode escrever menos bytes do que foram pedidos.

Todas estas system calls devolvem –1 em caso de erro.

Acabar a main

Para ler de um ficheiro para outro, temos de ler bytes de um com read e escrevê-los com write no outro. Vamos ter um ciclo até ao fim do ficheiro (quando read devolve 0) a fazer isto. Como vamos fazer esta operação mais que uma vez vamos escrever uma função para isso:

int copy(int fdin, int fdout);

Com isto já podemos acabar a função main:

int main(int argc, char** argv)
{
int i;

if(argc == 1)
{
if(copy(0, 1) == -1)
{
perror("copy");
exit(1);
}
}

// Começar em 1 porque argv[0] é o nome do executável ("cat")
for(i = 1; i < argc; i++)
{
int fd = open(argv[i], O_RDONLY);
if(fd == -1)
{
perror("open");
exit(1);
}
if(copy(fd, 1) == -1)
{
perror("copy");
exit(1);
}
if(close(fd) == -1)
{
perror("close");
exit(1);
}
}

return 0;
}

Porque é que usei tantos ifs? Deve-se verificar sempre o resultado das system calls. A função perror imprime a mensagem do último erro com o prefixo que passamos como parâmetro.

Mas estes ifs todos tornam o código difícil de ler. Vamos capturar este padrão numa função e tornar isto mais DRY:

void guard(int result, const char* message)
{
if(result == -1)
{
perror(message);
exit(1);
}
}

int main(int argc, char** argv)
{
int i;

if(argc == 1)
guard(copy(0, 1),"copy");

// Começar em 1 porque argv[0] é o nome do executável ("cat")
for(i = 1; i < argc; i++)
{
int fd;
guard(fd = open(argv[i], O_RDONLY), "open");
guard(copy(fd, 1), "copy");
guard(close(fd), "close");
}

return 0;
}

Falta agora apenas a função copy.

3 commentários:

Anónimo disse...

Para quem não tem a secção 2 do manual a funcionar como eu, executem o seguinte comando na consola: sudo apt-get install manpages-dev ou então procurem no vosso gestor de aplicações (no meu caso synaptic) manpages-dev.

Anónimo disse...

Isto é que é um blog à homem. lol Os meus parabéns.

Uma pequena correcção:
* O_RDONLY: para abrir só para leitura;
* O_RDWR: para abrir só para escrita;
* O_WRONLY: para abrir para leitura e escrita.

Tens a 2ª e 3ª linha trocadas. :)

Anónimo disse...

Não estão trocadas...

Enviar um comentário