#+title: Curso Básico da Linguagem C #+subtitle: Aula 13: Leitura da entrada padrão com 'read' #+author: Blau Araujo #+startup: show2levels #+options: toc:3 * Aula 13: Leitura da entrada padrão com 'read' [[https://youtu.be/bW3Xox6LP_U][Vídeo desta aula]] A função =read= é um /wrapper/ (uma /"embalagem"/) para simplificar o uso da chamada de sistema =read=. Chamadas de sistema são funções internas que o kernel implementa para que os programas (executados no /espaço de usuário/) tenham acesso a serviços e funcionalidades do sistema (disponíveis no /espaço do kernel/). Entre essas funcionalidades, estão: - Acesso a arquivos para leitura e escrita; - Alocação de espaço em memória; - Criação de processos; - Término de programas. Nossos programas em C não interagem diretamente com o hardware ou com recursos protegidos do sistema. Em vez disso, eles fazem /chamadas de sistema/ para "pedir" que o kernel realize essas operações por ele. Sendo assim, fica fácil concluir que todas as funções da biblioteca C padrão que nós utilizamos até agora, em algum momento, fazem chamadas de sistema. Por exemplo, a função =fgets= utiliza a chamada de sistema =read= para preencher o buffer de entrada do processo e, em seguida, copiar parte desse conteúdo para o buffer definido no programa para receber uma string. Se quisermos utilizar a chamada de sistema =read= para escrever bytes diretamente no buffer de destino, evitando o buffer de entrada do processo, nós podemos utilizar a função =read= da =glibc=, que é uma interface (um /wrapper/, ou /"embalagem"/) para a chamada de sistema propriamente dita. #+begin_quote As /man pages/ das interfaces para as chamadas de sistema implementadas na =glibc= podem ser encontradas na seção 2 (ex.: =man 2 read=). #+end_quote ** A função 'read' A função =read= é declarada no cabeçalho =unistd.h= da seguinte forma: #+begin_src c ssize_t read(int fd, void buf[.count], size_t count); #+end_src Onde: - =int fd= - O número de um descritor de arquivos aberto para leitura; - =void buf[.count]= - O endereço do buffer de destino dos bytes lidos; - =size_t count= - A quantidade máxima de bytes a serem lidos. #+begin_quote O tipo =ssize_t= é um apelido para =long= e =size_t= para =unsigned long=. #+end_quote Por exemplo: #+begin_src c #include #define BUFMAX 10 int main(void) { char buf[BUFMAX]; read(STDIN_FILENO, buf, BUFMAX); ... return 0; } #+end_src Isso fará com que até 10 bytes (=BUFMAX=) recebidos pela entrada padrão (descritor de arquivos =0=, valor expandido pela macro =STDIN_FILENO=) sejam escritos na memória a partir do endereço de =buf=. Mas, diferente de =fgets= e =scanf=, não haverá a inclusão do terminador nulo (='\0'=). *** Incluindo o terminador nulo Se quisermos que o conteúdo do buffer seja uma string, nós teremos que implementar uma forma de acrescentar o terminador nulo -- por exemplo, utilizando a quantidade de bytes lidos, que é retornada pela função =read= em caso de sucesso: #+begin_src c #include #define BUFMAX 10 int main(void) { char buf[BUFMAX]; ssize_t count = 0; if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) > 0) { buf[count] = '\0'; } /* ... */ return 0; } #+end_src #+begin_quote O retorno =0= indica o fim do arquivo e =-1= indica um erro, cujo identificador é atribuído à variável =errno= e pode ser exibido com a função =perror=. #+end_quote A solução do exemplo faz com que =read= se comporte quase como a função =fgets=, exceto por não criar um buffer de entrada no /heap/ do processo: - A leitura é feita até =BUFMAX - 1=; - O caractere seguinte no buffer (=buf[count]=) recebe ='\0'=; - O caractere ='\n'= será preservado se estiver no limite de leitura. Para demonstrar, digamos que o usuário digitou =12345= e teclou =Enter=. O valor de =count= será =6= e os bytes em =buf= serão: #+begin_example buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9] +------+------+------+------+------+------+------+------+------+------+ buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x0A | lixo | lixo | lixo | lixo | +------+------+------+------+------+------+------+------+------+------+ #+end_example Depois de alterado o byte em =buf[count]= (=buf[6]=), nós teremos: #+begin_example buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9] +------+------+------+------+------+------+------+------+------+------+ buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x0A | 0x00 | lixo | lixo | lixo | +------+------+------+------+------+------+------+------+------+------+ #+end_example Se for digitado =1234567890=, nós teremos inicialmente: #+begin_example buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9] +------+------+------+------+------+------+------+------+------+------+ buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | lixo | +------+------+------+------+------+------+------+------+------+------+ #+end_example Os caracteres =0x30= (=0=) e =0x0a= (='\n'=) não serão lidos, o valor de =count= será =9= e =buf[9]= receberá o terminador nulo: #+begin_example buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9] +------+------+------+------+------+------+------+------+------+------+ buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 | +------+------+------+------+------+------+------+------+------+------+ #+end_example *** Quando não converter os bytes lidos em string O tratamento anterior só faz sentido quando queremos utilizar a entrada como uma string. Mas nada disso será necessário, nem será adequado, se estivermos lendo... - Dados binários (arquivos binários em geral); - Dados em blocos fixos (setores de disco, pacotes de rede, etc); - Transferências de dados entre processos; - Dados para cálculos de /checksum/ e /hash/; - Dados para compressão ou criptografia... Entre outras tantas situações. ** O buffer do terminal Quando um programa com a função =read= é executado interativamente, os dados digitados são inicialmente armazenados em um buffer gerenciado pelo terminal. Ao chamar a função =read=, o programa consome parte desses dados da entrada padrão e os bytes que não forem consumidos permanecerão no buffer do terminal. Esses dados residuais não são descartados e permanecem disponíveis para futuras chamadas de leitura -- inclusive por processos subsequentes, caso o processo atual termine. Isso pode causar efeitos inesperados em programas interativos executados na sequência (como o próprio shell, por exemplo), pois os caracteres digitados anteriormente, mas ainda não consumidos, serão lidos automaticamente. Veja este exemplo: #+begin_src c #include #include #define BUFMAX 10 int main(void) { char buf[BUFMAX]; ssize_t count = 0; if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) { return 1; } // Tratamento do terminador nulo... buf[count] = '\0'; printf("%s\n", buf); return 0; } #+end_src Compilando e executando: #+begin_example :~$ gcc -Wall teste.c :~$ ./a.out 123456789012345 123456789 :~$ 012345 bash: 012345: comando não encontrado #+end_example A função =read= consumiu apenas os 9 primeiros caracteres digitados, os 6 caracteres restantes continuaram no buffer do terminal e foram lidos pelo processo do shell (interativo) que tentou executá-los como um comando. A solução para isso é simples (e já conhecida por nós): consumir todos os bytes restantes na entrada padrão: #+begin_src c #include #include #define BUFMAX 10 int main(void) { char buf[BUFMAX]; ssize_t count = 0; if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) { return 1; } // Tratamento do terminador nulo... buf[count] = '\0'; // Tratamento dos caracteres restantes no buffer do terminal... char c; while((c = getchar()) != '\n' && c != EOF); printf("%s\n", buf); return 0; } #+end_src Compilando e executando novamente: #+begin_example :~$ gcc -Wall teste.c :~$ ./a.out 123456789012345 123456789 :~$ #+end_example ** Definindo um prompt O nosso programa precisa de um prompt, então vamos implementá-lo: #+begin_src c #include #include #define BUFMAX 10 int main(void) { char buf[BUFMAX]; ssize_t count = 0; // Prompt... printf("Digite algo: "); if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) { return 1; } // Tratamento do terminador nulo... buf[count] = '\0'; // Tratamento dos caracteres restantes no buffer do terminal... char c; while((c = getchar()) != '\n' && c != EOF); printf("%s\n", buf); return 0; } #+end_src Agora, nós temos a impressão da string ="Digite algo: "= antes da leitura do terminal. Entretanto, ao compilar e executar o programa, nada é exibido até nós teclarmos =Enter=: #+begin_example :~$ gcc -Wall teste.c :~$ ./a.out 1234567890 Digite algo: 123456789 :~$ #+end_example Isso acontece porque =printf= utiliza o buffer de saída do programa (criado pela biblioteca padrão) e, por padrão, os dados na saída podem ser: - Totalmente bufferizados: quando o programa escreve em arquivos ou pipes; - Bufferizados por linha: quando a escrita é em um terminal. Como estamos escrevendo no terminal, a saída de =printf= é bufferizada por linha e só é liberada quando o buffer de saída fica cheio (~BUFSIZ = 8192 bytes~), quando recebe o caractere de quebra de linha (='\n'=) ou quando é esvaziada explicitamente (por exemplo, com =fflush(stdout)=). No caso do exemplo, a saída não foi descarregada porque a string na chamada de =printf= não tem uma quebra de linha (exatamente como nós queremos). Uma solução simples, porém, é descarregar o buffer explicitamente: #+begin_src c #include #include #define BUFMAX 10 int main(void) { char buf[BUFMAX]; ssize_t count = 0; // Prompt... printf("Digite algo: "); // Descarga do buffer de saída... fflush(stdout); if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) { return 1; } // Tratamento do terminador nulo... buf[count] = '\0'; // Tratamento dos caracteres restantes no buffer do terminal... char c; while((c = getchar()) != '\n' && c != EOF); printf("%s\n", buf); return 0; } #+end_src Compilando e testando: #+begin_example :~$ gcc -Wall teste.c :~$ ./a.out Digite algo: 1234567890 123456789 :~$ #+end_example ** Ainda temos um problema! Acontece, porém, que os nossos testes estão mascarando outro problema. Observe o que acontece quando digitamos menos caracteres do que a função =read= espera ler: #+begin_example :~$ gcc -Wall teste.c :~$ ./a.out Digite algo: 12345 12345 :~$ #+end_example A segunda linha em branco era esperada porque a quebra de linha da minha digitação foi preservada, mas a primeira linha em branco aconteceu porque eu tive que teclar =Enter= duas vezes! Isso aconteceu por causa da forma como estamos descarregando o restante do buffer de entrada do terminal: #+begin_src c // Tratamento dos caracteres restantes no buffer do terminal... char c; while((c = getchar()) != '\n' && c != EOF); #+end_src Quando digitamos menos caracteres do que o =read= espera ler, não há sobras no buffer do terminal e não há quebras de linha nem uma indicação de fim de arquivo (a menos que o usuário tecle =Ctrl+D=). Por isso, a limpeza do buffer deve ser condicionada de uma das seguintes formas: - Ou verificamos se o penúltimo caractere no buffer (=buf[count - 1]=) é diferente de ='\n'=, o que indicaria que o buffer não foi consumido completamente; - Ou verificamos se o buffer está vazio ou não com a chamada de sistema =ioctl=. **** Solução com o conteúdo de =buf[count - 1]= #+begin_src c // Verificar se '\n' faz parte da string... if (buf[count - 1] != '\n') { // Tratamento dos caracteres restantes no buffer do terminal... char c; while((c = getchar()) != '\n' && c != EOF); } #+end_src **** Solução com =ioctl= A chamada de sistema =ioctl=, no cabeçalho =sys/ioctl.h=, manipula parâmetros de dispositivos especiais (como terminais, por exemplo). Seus argumentos são: - O descritor de arquivos do dispositivo (ex.: =0= ou =STDIN_FILENO=); - O número da operação com o dispositivo (ex.: =FIONREAD=, quantidade de bytes disponíveis para leitura imediata); - Um ponteiro para o endereço que vai receber a informação obtida. Sendo assim, nós poderíamos utilizá-la desta forma para determinar se o buffer do terminal contém alguma coisa (=0= indica buffer vazio): #+begin_src c #include ... int main(void) { ... // Verificar se o buffer está vazio... int r = 0; if (ioctl(STDIN_FILENO, FIONREAD, &r) != 0) { // Tratamento dos caracteres restantes no buffer do terminal... char c; while((c = getchar()) != '\n' && c != EOF); } ... } #+end_src #+begin_quote Experimente as duas soluções e escolha a sua preferida. #+end_quote