#+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 o acesso a arquivos para leitura e escrita, a alocação de espaço em memória, a criação de processos, o término de programas e muitas outras. Portanto, 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 eles. Isso também significa que todas as funções da biblioteca C padrão que nós utilizamos até agora, em algum momento, também fazem chamadas de sistema. Por exemplo, a função =fgets= utiliza a chamada de sistema =read= para preencher o buffer de entrada do programa e, em seguida, copiar parte desse conteúdo para um determinado endereço na memória: o que também envolve algumas outras chamadas de sistema. Se quisermos utilizar a /chamada de sistema/ =read= para escrever bytes diretamente no endereço de destino, evitando o buffer de entrada do processo, nós podemos utilizar a /função/ =read= da =glibc=, que é uma interface para a chamada de sistema =read= 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'=). Se quisermos garantir um byte para receber o terminador, nós precisamos fazer como na função =fgets=: ler =count - 1= bytes... #+begin_src c #include #define BUFMAX 10 int main(void) { char buf[BUFMAX]; read(STDIN_FILENO, buf, BUFMAX - 1); ... return 0; } #+end_src ** Conversão para string incluindo o terminador nulo Para converter o conteúdo do buffer em uma string, nós temos que implementar uma forma de acrescentar o terminador nulo -- por exemplo, utilizando a quantidade de bytes efetivamente 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 bytes = 0; if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) { return 1; // Erro ou nada foi lido } // Tratamento do terminador nulo... buf[bytes] = '\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= tenha um comportamento semelhante ao de =fgets=: - A leitura é feita até =BUFMAX - 1=; - O caractere seguinte no buffer (=buf[bytes]=) 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 =bytes= 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[bytes]= (=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 =bytes= 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 para receber dados digitados em um terminal, a digitação é acumulada em um buffer gerenciado pelo terminal e só será enviada para o processo do programa quando o usuário teclar =Enter=. No programa, a função =read= consome parte desses dados e os bytes que não forem consumidos permanecerão no buffer do terminal. Os dados residuais não são descartados e permanecem disponíveis para futuras chamadas de leitura (inclusive por processos subsequentes, caso o processo do programa termine) ou até o terminal ser fechado. Isso pode causar efeitos inesperados em programas interativos executados na sequência (como o próprio shell, por exemplo), pois o conteúdo remanescente no buffer do terminal será lido automaticamente. Veja este exemplo: #+begin_src c #include #include #define BUFMAX 10 int main(void) { char buf[BUFMAX]; ssize_t bytes = 0; if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) { return 1; // Erro ou nada foi lido } // Tratamento do terminador nulo... buf[bytes] = '\0'; printf("%s\n", buf); return 0; } #+end_src Compilando e executando: #+begin_example :~$ gcc -Wall teste.c :~$ ./a.out 123456789012345 123456789 :~$ 012345 <-- O Bash tentou interpretar e executar o conteúdo do buffer! bash: 012345: comando não encontrado #+end_example A função =read= consumiu apenas os 9 primeiros caracteres digitados, mas os 6 caracteres restantes continuaram no buffer do terminal e foram lidos e interpretados pelo processo do shell (interativo) como a linha de um comando. *** Descarga do buffer do terminal A solução para isso é simples e já é velha conhecida: consumir todos os bytes restantes na entrada padrão, por exemplo, com a função que nós criamos em outras aulas... #+begin_src c void flush_stdin(void) { char c; while((c = getchar()) != '\n' && c != EOF); } #+end_src Mas esta função utiliza =getchar= que, assim como =fgets= e =scanf=, fará a leitura do buffer de entrada criado no processo pela =glibc=. Certamente, o conteúdo do buffer do terminal será copiado para o buffer de entrada do programa. Porém, sem garantias de que haverá algo no buffer do terminal, pode acontecer que a nossa função fique presa em =getchar= até que o usuário tecle =Enter= novamente, enviando a quebra de linha que o loop =while= espera para terminar. Por exemplo, utilizando a função =flush_stdin=, o nosso exemplo funcionaria corretamente quando houvesse mais caracteres digitados do que o =read= pode consumir: #+begin_example :~$ gcc -Wall teste.c :~$ ./a.out 123456789012345 123456789 :~$ #+end_example Contudo, digitando menos caracteres do que o limite, nós teríamos que teclar =Enter= duas vezes: a primeira para a leitura de =read= e a segunda para =getchar=... #+begin_example :~$ gcc -Wall teste.c :~$ ./a.out 12345 <--- Aguardando um segundo ENTER 12345 <--- Quebra de linha na string impressa :~$ #+end_example A segunda linha em branco era esperada porque nós não fizemos nada para remover a quebra de linha lida por =read=, mas a primeira aconteceu porque eu tive que teclar =Enter= mais uma vez para que =getchar= tivesse algo para ler. *** Implementação de =flush_stdin= com a chamada =read= Antes de passarmos à solução da descarga do buffer do terminal, vamos aproveitar que estamos lidando com chamadas de sistema para implementar uma versão da função =flush_stdin= com =read=, em vez de =getchar=: #+begin_src c void flush_tty_buffer(void) { char c; while (read(STDIN_FILENO, &c, 1) == 1 && c != '\n'); } #+end_src #+begin_quote *Importante!* Tanto =read= quanto =STDIN_FILENO= dependem da inclusão de =unistd.h=. #+end_quote O princípio é o mesmo, só que, desta vez, o /loop/ vai continuar enquanto =read= retornar que leu 1 byte e este byte for diferente de ='\n'=. A outra diferença é que, em vez de ler o buffer do programa, a nossa nova função lerá diretamente o buffer do terminal. #+begin_quote Esta função não poderia ser utilizada se os bytes remanescentes estivessem no buffer de entrada do programa! #+end_quote Mesmo assim, o problema continua: se o buffer do terminal estiver vazio, =read= ficará esperando o recebimento de uma linha, o que só acontecerá quando nós digitarmos novamente um =Enter=. *** Descarga condicional do buffer do terminal Se a causa do problema é a possibilidade do buffer do terminal estar vazio, basta condicionar a chamada de =flush_stdin=, ou de =flush_tty_buffer=, à verificação desta condição, o que pode ser avaliado de, pelo menos, duas formas: utilizando os bytes lidos e a sua quantidade ou verificando o estado do buffer do terminal. *Solução 1: Teste da presença de ='\n'=* #+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 | 0x0A | 0X00 | +------+------+------+------+------+------+------+------+------+------+ #+end_example Aqui, a digitação foi =12345678\n= (exatamente =BUFMAX - 1= bytes) e, mesmo que fosse digitada qualquer quantidade de bytes menor que essa, o buffer do terminal estaria vazio e o caractere ='\n'= estaria presente na string. Logo, nós poderíamos utilizar a seguinte condição: #+begin_src c if (buf[bytes - 1] != '\n') { flush_stdout(); // Ou flush_tty_buffer } #+end_src É uma solução interessante porque, além de simples, ela já verifica a existência da quebra de linha na string e nós poderíamos aproveitar, se fosse o caso, para removê-la: #+begin_src c if (buf[bytes - 1] == '\n') { buf[bytes - 1] = '\0'; // Remoção da quabra de linha } else { flush_stdout(); // Ou flush_tty_buffer } #+end_src *Solução 2: verificação do estado do buffer do terminal* 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 criar uma função para retornar =0=, no caso de buffer vazio, ou a quantidade de bytes remanescentes no buffer do terminal: #+begin_src c #include ... int is_tty_empty(void) { int r = 0; return (ioctl(STDIN_FILENO, FIONREAD, &r) == 0); } #+end_src Portanto, nossa função retornaria =1= (verdadeiro), se o buffer do terminal estivesse vazio, ou =0= (falso), se não estivesse. #+begin_quote A ideia de criar uma função, em vez de utilizar =ioctl= diretamente, tem a ver com a atribuição de um valor semântico ao código. #+end_quote No nosso exemplo, ela poderia ser utilizada desta maneira: #+begin_src c if (!is_tty_empty()) { flush_stdout(); // Ou flush_tty_buffer } #+end_src *Qual solução utilizar?* Não existe uma resposta certa neste caso: tudo depende do contexto do programa. Se eu estivesse buscando uma solução mais robusta e aplicável em diversas situações, eu escolheria a segunda (=ioctl=), mas escolheria a primeira em contextos menos rigorosos, onde a quebra de linha tivesse que ser detectada e removida da string de qualquer forma. Eu vou optar pela primeira solução desta vez, então o nosso código ficará assim: #+begin_src c #include #include #define BUFMAX 10 void flush_tty_buffer(void) { char c; while (read(STDIN_FILENO, &c, 1) == 1 && c != '\n'); } int main(void) { char buf[BUFMAX]; ssize_t bytes = 0; if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) { return 1; // Erro ou nada foi lido } // Tratamento do terminador nulo... buf[bytes] = '\0'; if (buf[bytes - 1] == '\n') { // Remoção condicional da quebra de linha... buf[bytes - 1] = '\0'; } else { // Esvaziamento condicional do buffer do terminal... flush_tty_buffer(); // Ou flush_stdin } printf("%s\n", buf); return 0; } #+end_src Compilando e testando... #+begin_example :~$ gcc -Wall teste.c :~$ ./a.out 12345 <-- 6 bytes digitados (buffer tty vazio) 12345 <-- Quebra de linha removida :~$ ./a.out 12345678 <-- 9 bytes digitados (buffer vazio) 12345678 <-- Quebra de linha removida :~$ ./a.out 123456789012345 <-- 16 bytes digitados (7 bytes no buffer tty) 123456789 <-- Não havia quebra de linha para remover :~$ #+end_example ** Definindo um prompt O nosso programa precisa de um prompt, então vamos implementá-lo: #+begin_src c ... char buf[BUFMAX]; ssize_t bytes = 0; printf("Digite algo: "); // Nosso prompt! if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) { return 1; // Erro ou nada foi lido } ... #+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 <-- O prompt só foi impresso aqui! :~$ #+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 (geralmente, =8192 bytes=), quando recebe o caractere de quebra de linha (='\n'=) ou quando é esvaziada explicitamente. 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 com a função =fflush=: #+begin_src c ... 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; } ... #+end_src Compilando e testando: #+begin_example :~$ gcc -Wall teste.c :~$ ./a.out Digite algo: 1234567890 123456789 :~$ #+end_example #+begin_quote O problema da /bufferização por linha/ não é exatamente causado pelo uso da chamada de sistema =read=. Na verdade, as funções de mais alto nível (como =fgets= e =scanf=) é que são implementadas com mecanismos para descargar o buffer de saída automaticamente. #+end_quote