DEV Community

Plínio Ribeiro
Plínio Ribeiro

Posted on

[Estudos de C] Escrevendo uma versão simples do utilitário cat

O cat é um utilitário de linha de comando que lê arquivos, seja apenas um ou uma sequência informada, e escreve o seu conteúdo em fluxo de destino.

O nome cat é derivado de: (con)catenate files.

Conforme o manual (man cat) é descrito com duas funções: concatenar arquivos e imprimir o seu conteúdo em um fluxo de saída.

Utilizamos o cat da seguinte forma:

cat [OPTION]... [FILE]...
Enter fullscreen mode Exit fullscreen mode

Essa implementação é com fim de tentar a resolução, própria, do Exercício 01, da Piscine 10 de C, da Escola 42. Não participei de nenhuma piscina, apenas encontrei os PDFs relativos no Github e estou tentando fazer os exercícios.

São as regras a implementação desta versão:

Image description

Como pode se ler das orientações acima, a implementação desta versão tem limitações das funções que poderão ser utilizadas.

Nesta minha implementação, optei por utilizar apenas as funções: open, close, read e write.

Antes de adentrar diretamente na escrita da versão do cat , vou estar escrevendo algumas notas de introdutórias.

File Descritores

Um ************File Descritor (FD)************ é um número inteiro único maior que zero que guarda referência para um arquivo aberto pelo sistema operacional.

Quando pede ao sistema operacional o acesso a determinado arquivo ou fluxo de dados, o ***kernel*** retorna, um *********File Descritor********* de um registro na tabela global de registros de arquivos do sistema operacional.

Por meio dele o processo ou programa pode acessar os dados constantes no arquivo, ou fluxo referenciado pelo FD. Observa-se que o pedido de acesso tenha sido acatado com sucesso.

Em sistemas baseados em Unix e Linux os primeiros FD referem aos fluxos stdin, stdout e stderr.

Nome File Descriptor Descrição Abreviatura
Standard input 0 O fluxo de dados padrão para entrada, por exemplo, em um pipeline de comando. No terminal, o padrão é a entrada do teclado do usuário. stdin
Standard output 1 O fluxo de dados padrão para saída, por exemplo, quando um comando imprime texto. No terminal, o padrão é a tela do usuário. stdout
Standard error 2 O fluxo de dados padrão para a saída relacionada à ocorrência de um erro. No terminal, o padrão é a tela do usuário. stderr

Os valores dos FD acima descritos podem serem acessados indicando diretamente os seus valores ou por meio de constantes presentes nas bibliotecas <unistd.h> e <stdio.h>.

STDIN_FILENO stdin
STDOUT_FILENO stdout
STDOUT_FILENO stderr

Outra questão referente aos FD é a possibilidade de redirecionar os fluxos de dados entre diferentes FD por meio do *pipe*. Mas esse tópico tratarei em outro momento.

Referências:

  1. https://www.computerhope.com/jargon/f/file-descriptor.htm

Função open()

A função open() permite a realização de conexão entre um arquivo e um FD. Por meio dessa função, o programa tem acesso ao FD de um arquivo informado como parâmetro na chamada da função.

A função tem o seguinte protótipo:

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

E é disponibilizada pela biblioteca <fcntl.h>.

O primeiro argumento, path, é pelo qual passamos o caminho do caminho ou fluxo de dados que queremos acessar/abrir.

O segundo argumento é o tipo de acesso que desejamos(leitura, escrita, leitura e escrita, etc). Esse passamos informando a respectiva, quis são constantes definidas no cabeçalho <fcntl.h>. Exemplos de algumas **flags** definidas e mais utilizadas.

Flag Descrição
O_EXEC: Abre o arquivo para execução, apenas. Não se aplica a diretórios.
O_RDONLY Apenas leitura.
O_RDWR Abre o arquivo para leitura e escrita.
O_RDWR Abre um diretório para pesquisa, apenas. Não se aplica a não diretórios.
O_WRONLY Abre o arquivo apenas para a escrita.

O terceiro argumento, facultativo, é utilizado quando estamos criando um novo arquivo. Pois, é por meio dele que definimos as permissões iniciais do arquivo criado.

***************Retorno:*************** O retorno da chamada da função open() é um número inteiro positivo que representa FD do respectivo arquivo ou fluxo de dados requerido a ser aberto. Caso contrário, a função retorna -1 e define o tipo de erro que ocorreu.

O código abaixo é a implementação de utilização da função open() que cria, se não existir um arquivo e salva nele que o usuário digitar no stdin, por enquanto o programa estiver em execução.

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

#define MAX_BUF 1024

int main(int argc, char **argv)
{
    char    *file;
    char    buf[1024];
    int     fd;
    int     flags;
    int     bytes_reads;
    int     bytes_writes;

    if (argc < 2)
    {
        perror("Arquivo de destino não definido.\n");
        return (1);
    }

    flags = O_WRONLY | O_CREAT | O_TRUNC;
    file = argv[1];

    fd = open(file, flags, S_IRUSR | S_IWUSR);
    if (fd == -1)
    {
        perror("Erro ao abrir o arquivo!\n");
        return (1);
    }

    while ((bytes_reads = read(STDIN_FILENO, buf, MAX_BUF - 1)) > 0)
    {
        bytes_writes = write(fd, buf, bytes_reads);
        if (bytes_writes != bytes_reads)
        {
            perror("Error ao escrever no arquivo\n.");
            close(fd);
            return (1);
        }
    }   

    if (bytes_reads == -1)
    {
        perror("Erro na leitura do STDIN\n");
        close(fd);
        return (1);
    }   

    close(fd);
    return (0);
}
Enter fullscreen mode Exit fullscreen mode

O código ficou um pouco longo, mas bom que aproveito como exemplo para as explicações abaixo, sobre as demais funções.

Para mais informações: man 2 open.

Referências:

  1. https://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html
  2. https://en.wikipedia.org/wiki/Open_(system_call)

Função close()

A função close() fecha um FD aberto e referenciado. A chamamos quando precisamos fechar um arquivo ou fluxo de dados abertos. Fazemos isso passamos o arquivo ou fonte de dados como parâmetro, no qual será terminado a conexão com o FD referente.

A função tem o seguinte protótipo:

int close(int fd);

E é disponibilizada pela biblioteca <unistd.h>.

A função retorna 0 no caso de sucesso ou -1, em caso tenha ocorrido algum erro.

Como pose se ver no código anteriormente exposto, a chamamos para fechar o FD do arquivo aberto antes do retorno final da função main().

Função read()

A função read() lê uma certa quantidade de bytes a partir de um FD e os salva em um buffer.

Ela tem o seguinte protótipo:

ssize_t read(int fd, void *buf, size_t count);

O primeiro argumento é a indicação do FD no qual será feita a leitura. O segundo é o do buffer que será salvo os dados lidos e o terceiro é a quantidade de **bytes** a serem lidos/transferidos.

E é disponibilizada pela biblioteca <unistd.h>.

Quanto ao retorno:

Cada chamada retorna uma contagem do número de bytes transferidos. Na leitura, o número de bytes retornados pode ser menor que o número solicitado.
Um valor de retorno de zero bytes implica o fim do arquivo, e -1 indica algum tipo de erro.

Referências: RITCHIE, Dennis M. et al. The C programming language. Bell Sys. Tech. J, v. 57, n. 6, p. 1991-2019, 1978.

Como pode ser observado no código recortado do exposto acima, a função read() é chamada no contexto de um loop while. Por enquanto o valor retornado for maior que zero, os dados lidos, por quantidade definida: a vez, são salvos no na variável buf e valor da quantidade de bytes lidos são salvos na variável bytes_reads.

Após a execução, é feita uma verificação se ocorreu algum erro na leitura.

while ((bytes_reads = read(STDIN_FILENO, buf, MAX_BUF - 1)) > 0)
    {
        bytes_writes = write(fd, buf, bytes_reads);
        if (bytes_writes != bytes_reads)
        {
            perror("Error ao escrever no arquivo\n.");
            close(fd);
            return (1);
        }
    }   

    if (bytes_reads == -1)
    {
        perror("Erro na leitura do STDIN\n");
        close(fd);
        return (1);
    }   
Enter fullscreen mode Exit fullscreen mode

Função write()

A função write() escreve uma quantidade n de bytes provenientes de um buffer em um FD informado.

Ela tem o seguinte protótipo:

ssize_t write(int fd, const void *buf, size_t count);

O primeiro argumento é o FD onde serão escritos os dados, o segundo é do ***buffer*** no qual serão provenientes e o terceiro é quantidade de **bytes** que serão escritos por vez.

A função é disponibilizada pela biblioteca <unistd.h>.

Quanto ao retorno:

Para gravação, o valor de retorno é o número de bytes gravados. Ocorreu um erro se esse número não for igual ao número solicitado.

Referências: RITCHIE, Dennis M. et al. The C programming language. Bell Sys. Tech. J, v. 57, n. 6, p. 1991-2019, 1978.

Implementação de uma versão simples do utilitário cat

Escrita as breves notas acima, agora é vez de escrever sobre a implementação do utilitário cat.

Nas primeiras linhas são escritas as inclusões das bibliotecas necessárias, a definição da constante referente à quantidade de ***bytes*** que serão lidos e escritos por vez e o protótipo das funções.

#include <unistd.h>
#include <fcntl.h>

#define MAX_BUF 1024

void    ft_cat(int fd);
void    clear_buf(char *);
Enter fullscreen mode Exit fullscreen mode

Em seguida tem o corpo da função main().

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

    if (argc == 1)
    {
        fd = STDIN_FILENO;
        ft_cat(fd);
    }
    else 
    {
        i = 1;
        while (i < argc)
        {
            fd = open(argv[i], O_RDONLY);
            if (fd == -1)
            {
                write(2, "Error opening the file\n", 30);
                return (1);
            }
            ft_cat(fd);
            close(fd);
            i++;
        }
    }

    return (0);
}
Enter fullscreen mode Exit fullscreen mode

Nela é declarada duas variáveis do tipo inteiro: i e fd.

Em seguida é verificado se foi informado arquivos para serem lidos ou não. Caso não tenham sido informados pelo usuário, o variável fd recebe o FD referente ao stdin, por meio da constante STDIN_FILENO. Após, é chamada a ft_cat(fd) passando o respectivo fd.

Por outro lado, na situação do usuário tenha informado um ou mais arquivos; é atribuído o valor 1 para a variável i e dado início ao loop, que será executado por enquanto i for menor que o valor da variável argc.

Para cada interação do loop: Um arquivo é aberto por meio da função open() e o seu respectivo FD é salvo na variável fd; é feita uma verificação de erro e, se estiver tudo ok, a função ft_cat() é chamada tedndo o FD atual como parâmetro.

Por fim, as últimas linhas de execução do loop é: fechar o arquivo após a execução da função ft_cat() e o incremento da variável i para acessar o próximo arquivo, se houver.

Antes de passar para a explicação da função ft_cat(); apenas anotar aqui sobre a função clear_buf(). Que tem a tarefa de limpar a memória de alguma array recebida como parâmetro.
Assim, evita que haja restos de dados no buffer, principalmente quando estamos o utilizando no contexto do loop e leitura e escrita.

void    clear_buf(char *buf)
{
    int     i;
    i = 0;
    while(i < MAX_BUF)
        buf[i++] = '\0';
}
Enter fullscreen mode Exit fullscreen mode

Agora, as notas para função ft_cat(). Segue o código da respectiva.

void    ft_cat(int fd)
{
    int     bytes_reads;
    int     bytes_writes;
    char    buf[MAX_BUF];

    bytes_reads = 0;
    bytes_writes = 0;
    clear_buf(buf);

    while ((bytes_reads = read(fd, buf, MAX_BUF - 1)) > 0)
    {
        buf[bytes_reads - 1] = '\0';
        bytes_writes = write(1, buf, bytes_reads);
        if (bytes_writes != bytes_reads)
        {
            write(2, "Error reading the file\n", 30);
            return;
        }
    }

    if (bytes_reads == -1)
    {
        write(2, "Error reading the file\n", 30);
        return;
    }

    write(1, "\n\n", 2);   
}
Enter fullscreen mode Exit fullscreen mode

Ela recebe como parâmetro um inteiro que representa um FD de de onde os dados serão lidos. A escolha é pela opção, ao invés de receber o caminho de arquivo a ser lido, é pela possibilidade do FD referir tanto para arquivos, como também para outras fontes de da dados: tais como o stdin.

De início são declaradas três variáveis: duas do tipo inteiro e uma cadeia de caráteres com tamanho definido pela constante MAX_BUF. As variáveis bytes_reads e bytes_writes , como pelo nome dá para saber, serão onde armazenaremos os retornos das funções read() e write(). E a função buf[] será onde salvaremos os dados lidos e a serem, posteriormente, escritos no stdout.

Em seguida, são atribuídas as variáveis valores zero, com fim de evitar que haja dados remanescentes da memórias.

A atividade de leitura e escrita é feita no contexto de um *loop* while*.*

O *loop* é executado por enquanto o retorno da chamada da função read() for maior que zero. Na chamada da dessa função passamos, como parâmetro o FD atual da chamada da função ft_cat() , o buf onde os dados serão salvos e a quantidade de **bytes** que serão lidos por vez.

No corpo do *loop, adicionamos o caractere de terminação na última posição do buf. Após é feita a chamada da função write() para escrever no stdout os dados presentes no buf. Um verificação de erro é feita para checar se quantidade de *bytes** escritos é igual a quantidade a ser escrita. Caso haja alguma diferença, é disparado um erro.

Por fim, encerrada a execução do while, outra verificação de erro é feita relativa à chamada da função read() .

Conclusão

Bem, está foi uma implementação simples do utilitário cat. Observando a proposta do desafio que apresentou restrições de recursos, o intuito com esta atividade é aprender a utilização das funções mais básicas de manipulação de entradas e saídas de dados na linguagem C.

Referências

https://twobithistory.org/2018/11/12/cat.html

https://en.wikipedia.org/wiki/Cat_(Unix)

https://history-computer.com/understanding-cat-command-in-linux-with-examples/

https://austinedger.medium.com/unix-commands-lets-build-cat-59b8a91b9708

Top comments (0)