Implementando um cat em C
Tags: programming, tutorial, braziliandevs, c
Published: 2023-01-23 14:09:07

## Introdução

O `cat` é uma ferramenta muito utilizada para ler e escrever em arquivos.

Ela faz parte do Coreutils, que é um conjunto de utilitários para manipular arquivos, shell e texto, como `ls`, `chmod`, `mkdir`, `cd`, entre outros.

### Exemplos de utilização do `cat`

Ler um arquivo:

```bash
$ cat arquivo.txt
Esse
é
o 
conteúdo!
```

Ler mais de um arquivo:

```bash
$ cat arquivo1.txt arquivo2.txt
Conteúdo do arquivo 1
Conteúdo do arquivo 2
```

## Entendendo `stdin`, `stdout` e `stderr`

Esses três são streams de dados, ou seja, são um “caminho” por onde dado é recebido ou enviado.

`stdin` representa a **entrada padrão** (**st**an**d**ard **in**put) do programa, que no terminal geralmente é o teclado.

`stdout` representa a **saída padrão** (****st****an**d**ard ******out******put) do programa, e `stderr` representa a **saída padrão de erro** (****st****an**d**ard ******err******or), sendo que ambas são geralmente a tela do terminal.

O “geralmente” é usado por 2 motivos:

1. Essas 3 streams podem ser redirecionadas para arquivos e outros lugares
2. Em aparelhos como um Arduino a entrada pode vir de sensores e a saída pode ser para um display

Detalhe: no C, `stdin`, `stdout` e `stderr` são arquivos, então lidamos com eles da mesma forma que lidaríamos com qualquer outro arquivo.

### Redirecionando streams no terminal

Ainda no exemplo do `cat`, se rodarmos somente o comando sem nenhum argumento, o que ele faz é ler do `stdin` e escrever no `stdout`.

```bash
$ cat
oi            # eu escrevi
oi            # cat printou
tudo bem?     # eu escrevi
tudo bem?     # cat printou
```

A princípio pode parecer meio sem utilidade o `cat` ”imitar” o que você escreve, mas isso se torna útil quando você redireciona a saída para um arquivo usando o `>`.

```bash
$ cat > novo.txt
Escrevendo o
conteúdo
desse arquivo
# aperte ctrl + D para finalizar
```

```bash
$ cat < original.txt > copia.txt
```

## Iniciando a implementação em C usando `stdio.h`

Agora que os conceitos de entrada e saída padrão já estão mais claros, podemos começar a implementar o código do nosso `cat`.

Para isso vamos partir dessa estrutura básica:

```bash
#include <stdio.h>

int main(int argc, char *argv[]) {
	return 0;
}
```

E agora vamos implementar só a parte de ler do `stdin` e escrever no `stdout`:

```c
#include <stdio.h>

int main(int argc, char *argv[]) {

  // argc: número de argumentos
  // argc == 1 signfica que o programa foi chamado sem nenhum
  // argumento extra, exemplo: ./cat

  if (argc == 1) {
    while (1) {
      // lê um caracter do stdin
      int ch = getc(stdin);
      
      // EOF significa fim do arquivo (end of file)
      if (ch == EOF)
        break;

      // escreve o caracter no stdout
      putc(ch, stdout);
    }
  }

  return 0;
}
```

O que esse programa faz é ficar lendo cara caractere do `stdin` até o último do arquivo.

Se a entrava for o teclado, ele lê até forçamos um `EOF` apertando `ctrl` + `D`.

## Validando usando o `cat` original

Criei esse shell script para testar se o programa tem o mesmo comportamento do `cat`:

```bash
# para cada arquivo na pasta home
for f in ~/*; do

	# executa o cat real, o meu cat e tira o md5 da saída
	# < $f redireciona o conteúdo do arquivo atual para o stdin
	a=$(cat < $f | md5sum)
	b=$(./cat < $f | md5sum)
	
	# se o md5 for diferente, é sinal que algo está errado
	if [[ "$a" != "$b" ]]
	then
		echo "$f ($a vs $b)"
	fi
done
```

Isso não garante que nosso cat está totalmente correto, mas nos dá uma confiança maior de que está funcionando igual para muitos cenários.

 

## Implementando o `cat` com argumentos

Caso sejam passados argumentos, o `cat` vai tratá-los como nomes de arquivos, vai tentar ler um por um e escrever seu conteúdo no `stdout`.

- Obs.: o cat também recebe opcionalmente alguns outros argumentos, mas não vamos implementá-los aqui.

```c
#include <stdio.h>

int main(int argc, char *argv[]) {
  if (argc == 1) {
    ...
  } else {
    for (int i = 1; i < argc; i++) {

      // abrimos o arquivo no modo leitura
      FILE *file = fopen(argv[i], "r");

      while (1) {
        // lê um caracter do arquivo, printa no stdout
        int ch = getc(file);
        if (ch == EOF)
          break;
        putc(ch, stdout);
      }

      // fechamos o arquivo para evitar problemas
      fclose(file);
    }
  }

  return 0;
}
```

## Tratando arquivos que não podem ser lidos

Se você passar um arquivo que não existe ou que você não tem permissão para ler, vai acontecer um segmentation fault:

```bash
$ ./cat naoexiste     
[1]    99032 segmentation fault (core dumped)  ./cat naoexiste
```

Isso acontece porque quando o arquivo não pode ser aberto, o `fopen` retorna `NULL` e altera a variável global `errno` para o código do erro.
Precisamos tratar esses erros antes de tentar fazer qualquer operação com o arquivo, senão teremos comportamentos indefinidos no nosso programa.

```c
#include <stdio.h>
#include <errno.h>

// usamos a função strerror para printar uma
// mensagem de erro com base no errno
#include <string.h>

int main(...) {
  // se tudo correr bem, esse valor não vai mudar,
  // e vamos retornar 0, que signifca ok
  int exit_status = 0;

  ...

  else {
      FILE *file = fopen(argv[i], "r");

      // arquivo não existe / não pode ser lido
      if (file == NULL) {

        // printar na saída de erro no mesmo formato que o cat, exemplo:
        // cat: naoexiste: No such file or directory
        fprintf(stderr, "%s: %s: %s\n", argv[0], argv[i], strerror(errno));

        // se o arquivo não existe, o cat não encerra imediatamente,
        // mas continua lendo os próximos arquivos e só depois 
        // finaliza com código de erro
        exit_status = errno;
        continue;
      }

      ...
    }
  }

  // retornando o status
  return exit_status;
}
```

Também seria necessário tratar erros de leitura e escrita em arquivos, mas vamos deixar para a parte 2.

## Código completo feito com `stdio.h`

```c
#include <errno.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
  int exit_status = 0;

  if (argc == 1) {
    while (1) {
      int ch = getc(stdin);
      if (ch == EOF)
        break;
      putc(ch, stdout);
    }
  } else {
    for (int i = 1; i < argc; i++) {
      FILE *file = fopen(argv[i], "r");

      if (file == NULL) {
        fprintf(stderr, "%s: %s: %s\n", argv[0], argv[i], strerror(errno));
        exit_status = errno;
        continue;
      }

      while (1) {
        int ch = getc(file);
        if (ch == EOF)
          break;
        putc(ch, stdout);
      }
    }
  }

  return exit_status;
}
```

## Dúvidas comuns

### Ler/escrever byte por byte não prejudica a performance?

Não, porque os arquivos no C por padrão são “buferizados” (buffered), ou seja, quando você chama `putc`, esse byte é escrito num array (*buffer*) em memória até uma certa quantidade pré-definida, e depois esse array inteiro é escrito no arquivo de uma vez.

Dessa forma a escrita se torna mais rápida, pois escrever na memória é ordens de vezes mais rápido que no disco.
Essa escrita no arquivo de fato é chamada de “flush”, e você pode forçar um flush a qualquer momento no C usando `fflush`.

Da mesma forma a leitura de um arquivo com `getc` lê vários bytes e guarda num buffer em memória. Próximas chamadas para `getc` vão ler desse buffer e não do disco, até que seja necessário consultar o disco de novo.

Usei `getc` e `putc` como exemplos, mas o buffer é aplicado independente da função que você está usando para leitura ou escrita.

## Conclusão

Bom, já falei bastante, apesar de não ter dado para abordar alguns assuntos.
Espero que esse texto tenha sido útil e tenha te trazido insights sobre C, sobre o programa `cat` e sobre como lidar com `stdin`, `stdout` e `stderr`.

Na parte 2 eu pretendo reimplementar esse código de forma mais baixo nível, falando diretamente com o kernel, implementando manualmente os buffers de leitura e escrita e usando outra estratégia para tratar erros.

Como sempre qualquer correção, sugestão ou informação que agregue é muito bem vinda. 

Até a próxima!