.. | ||
README.org |
Curso Básico da Linguagem C
Aula 11: Leitura da entrada padrão com 'scanf'
A entrada padrão (stream stdin
ou descritor de arquivos 0
) pode estar ligada…
- A um terminal para leitura interativa (digitação);
- A um arquivo via redirecionamento de leitura;
- À saída de um processo via pipe ou arquivo FIFO;
- A um arquivo de forma programática (ex: funções
freopen
,dup2
).
Para verificar se o fluxo stdin
está ligado a um terminal, nós podemos
utilizar a função isatty
(unistd.h
):
int isatty(int fd);
Onde int fd
é o descritor de arquivos testado. O retorno será o inteiro 1
,
se fd
estiver ligado a um terminal, ou 0
, se estiver ligado a um pipe ou
redirecionado para um arquivo.
Exemplo (test-tty.c
):
#include <stdio.h>
#include <unistd.h>
int main(void) {
if (isatty(0)) {
puts("STDIN ligada ao terminal.");
} else {
puts("STDIN ligada a um arquivo ou pipe.");
}
return 0;
}
Compilação e testes:
:~$ gcc -Wall test-tty.c :~$ ./a.out STDIN ligada ao terminal. :~$ ./a.out < /dev/null STDIN ligada a um arquivo ou pipe. :~$ : | ./a.out STDIN ligada a um arquivo ou pipe.
O comando interno do shell
:
é o comando nulo, que não faz nada e sempre termina com sucesso.
Para associar programaticamente a entrada padrão a um arquivo (de nome recebido
como argumento, por exemplo), nós podemos utilizar a função freopen
para abrir
um arquivo para leitura através de um fluxo (stream) previamente aberto: no caso
o stream stdin
…
FILE *freopen(const char *caminho, const char *modo, FILE *stream);
Onde:
caminho
: string do nome do arquivo;modo
: string de modo de abertura ("r"
,"w"
,"rw"
);stream
: um fluxo de dados previamente aberto.
Retorna um ponteiro para o tipo FILE
ou NULL
, em caso de erro.
O propósito original desta função é alterar o arquivo associado a um dos fluxos padrão (
stdin
,stdout
oustderr
).
Exemplo (redir-stdin.c
):
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
// Diretório 'fd' do processo de 'a.out'...
puts("Antes do redirecionamento...");
system("ls -l /proc/$(pidof a.out)/fd");
// Nome do arquivo...
char *filename;
if (argc > 1) {
// Primeiro argumento...
filename = argv[1];
} else {
// Dispositivo nulo...
filename = "/dev/null";
}
// Redirecionamento...
FILE* stream = freopen(filename, "r", stdin);
if (!stream) {
perror("freopen");
return EXIT_FAILURE;
}
// Diretório 'fd' do processo de 'a.out'...
puts("Depois do redirecionamento...");
system("ls -l /proc/$(pidof a.out)/fd");
return EXIT_SUCCESS;
}
Compilação…
:~$ gcc -Wall redir-stdin.c
Executando sem argumentos (redireciona para /dev/null
):
:~$ ./a.out Antes do redirecionamento... total 0 lrwx------ 1 blau blau 64 abr 24 09:16 0 -> /dev/pts/0 lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0 lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0 Depois do redirecionamento... total 0 lr-x------ 1 blau blau 64 abr 24 09:16 0 -> /dev/null lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0 lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0
Executando com argumentos (nome do arquivo no primeiro argumento):
:~$ ./a.out /etc/shells Antes do redirecionamento... total 0 lrwx------ 1 blau blau 64 abr 24 09:16 0 -> /dev/pts/0 lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0 lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0 Depois do redirecionamento... total 0 lr-x------ 1 blau blau 64 abr 24 09:16 0 -> /etc/shells lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0 lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0
No caso de um erro de abertura (ex: arquivo "banana" não existe):
:~$ ./a.out banana Antes do redirecionamento... total 0 lrwx------ 1 blau blau 64 abr 24 09:21 0 -> /dev/pts/0 lrwx------ 1 blau blau 64 abr 24 09:21 1 -> /dev/pts/0 lrwx------ 1 blau blau 64 abr 24 09:21 2 -> /dev/pts/0 freopen: No such file or directory
Leitura interativa da entrada padrão
Seja o conteúdo de um arquivo ou uma entrada digitada no terminal, a leitura da entrada padrão pode ser feita com várias formas e com diversas funções da biblioteca padrão:
- Leitura de caracteres:
getchar
,getc(stdin)
efgetc(stdin)
; - Leitura de linhas (até
\n
):fgets(buf, size, stdin)
,getline(&buf, &size, stdin)
; - Leitura formatada de linhas:
scanf
,fscanf(stdin, ...)
;
Mas, como a leitura das linhas de um arquivo é mais controlada (o conteúdo é previamente conhecido), nós vamos concentrar nossa atenção na leitura de linhas digitadas no terminal por um usuário (leitura interativa). Para isso, nós estudaremos três funções (nesta e nas próximas aulas):
- Leitura formatada de linhas:
scanf
; - Leitura de linhas:
fgets
; - Leitura de caracteres: chamada de sistema
read
.
Leitura formatada de linhas com 'scanf'
A função scanf
lê os bytes de uma linha enquanto encontra casamentos com os
especificadores definidos em uma string de formato (como no printf
). A parte
da linha que encontrar casamento é passada para um ou mais endereços de
variáveis (ou endereços em ponteiros) e tudo que vier depois permanece no
buffer de entrada do processo.
Outros pontos importantes:
- Cada especificador de formato é casado com apenas uma palavra da linha lida (palavras são cadeias de caracteres delimitadas por espaços, tabulações e quebras de linha).
- Retorna o número de casamentos com as especificações de formatos que foram encontrados e atribuídos (o que pode ser menor do que a quantidade de especificadores fornecidos).
- Em caso de erro, retorna zero ou
EOF
(com vários significados possíveis). - Altera a variável
errno
(errno.h
), o que possibilita obter descrições de erros, por exemplo, com as funçõesperror
(stdio.h
) oustrerror
(string.h
). - No caso da formatação como string, o caractere nulo (
'\0'
) é inserido após o último byte do casamento encontrado. - Tudo que delimita a quantidade de bytes lidos é a forma como a string de formato é construída.
- Uma string de formato mal construída pode levar a vulnerabilidades de memória.
A função scanf
é considerada insegura por muitos programadores, mas a
verdade é que ela é reconhecidamente difícil de ser utilizada corretamente.
De man 3 scanf
:
It is very difficult to use these functions correctly, and it is preferable to read entire lines with fgets(3) or getline(3) and parse them later with sscanf(3) or more specialized functions such as strtol(3).
Ou seja, o próprio manual diz que é muito difícil utilizar corretamente
as funções da família do scanf
e pode ser mais fácil ler linhas inteiras
com funções como fgets
e getline
e fazer os processamentos necessários
com a função sscanf
ou outras funções mais especializadas em conversões
de cadeias de caracteres em valores numéricos.
Para reduzir as chances de erro
A maior parte dos problemas com scanf
decorre da conversão de strings.
Por isso, é sempre bom atentar para alguns detalhes, como:
- Sempre validar o retorno de
scanf
e fazer o tratamento dos erros. - Na leitura de uma linha inteira como string (por exemplo, uma linha
que contenha apenas uma palavra), sempre limitar a quantidade de
caracteres lidos no especificador
%s
(ex:%9s
para ler até 9 bytes). - Na leitura de caracteres únicos, o especificador
%c
deve ser precedido de um espaço.
Exemplos
Leitura de duas strings
char str1[10];
char str2[10];
// "%Ns" não deve incluir '\0'!
scanf("%9s", str1);
scanf("%9s", str2);
Separando a entrada digitada em campos
char str1[10];
char str2[10];
scanf("%9s %9s", str1, str2);
printf("str1: %s\n", str1);
printf("str2: %s\n", str2);
Lendo caracteres
Neste caso, apenas o primeiro caractere será casado, seja ele qual for.
printf("Confirma (s/n)? ");
char reply;
scanf("%c", &reply);
Mas, se o usuário digitar espaços antes dos caracteres esperados, o primeiro espaço é que será consumido. Para evitar isso…
scanf(" %c", &reply);
Lendo vários caracteres
char a, b, c;
scanf(" %c %c %c", &a, &b, &c);
printf("A:%c B:%c C:%c\n", a, b, c);
Lendo e formatando números
Inteiros:
printf("Digite um inteiro: ");
int num;
scanf("%d", &num);
Ponto flutuante:
printf("Digite um float: ");
float f;
int c = scanf("%f", &f);
Descarregando o buffer de entrada
A função scanf
não consome nada que não case com o formato especificado,
o que inclui a quebra de linha. Todo o restante da linha permanece no buffer
de entrada e, na ausência de uma função da biblioteca padrão para descarregá-lo,
nós podemos criar uma função como esta:
void stdin_flush(void) {
char ch;
while ((ch = getchar()) != '\n' && ch != EOF);
}
Ela consumiria todos os caracteres restantes no buffer de entrada enquanto
o caractere atribuído a ch
fosse diferente da quebra de linha (\n
) ou de -1
,
que é o valor retornado por getchar
quando tenta ler caracteres após o fim
de um arquivo (constante simbólica EOF
).
Isso é particularmente necessário quando houver leituras consecutivas da
entrada padrão com scanf
…
int a;
scanf("%d", &b);
stdin_flush();
int b;
scanf("%d", &b);
stdin_flush();
/* ... */
Inicialização de valores numéricos
Nos exemplos anteriores, nós temos um problema que precisa ser considerado:
a leitura pode falhar e, nesse caso, nada será escrito nos endereços das
variáveis passados para scanf
. Como as variáveis não foram inicializadas,
seu conteúdo será lixo de memória. Portanto, é importante que essas variáveis
sejam devidamente inicializadas com algum valor, por exemplo:
int a = 0;
scanf("%d", &b);
stdin_flush();
int b = 0;
scanf("%d", &b);
stdin_flush();
/* ... */
Este problema é relevante com variáveis que recebem valores numéricos (char
,
int
, float
, double
, etc), porque, em geral, elas são utilizadas em cálculos
e até o lixo de memória pode ser tratado como número válido, levando a
resultados enganosos.
Buffer overflow
A leitura de strings com scanf
é bastante suscetível a causar vulnerabilidades
graves de memória, como o buffer overflow, que é quando a quantidade de dados
escritos a partir do endereço de um buffer excedem seu limite máximo e continuam
sendo escritos em endereços válidos subsequentes.
Quando o programa tentar escrever os bytes excedentes em uma região inválida da memória, nós temos uma falha de segmentação.
Observe este exemplo:
char ref[10] = "123456789"; // Uma string qualquer.
char str[10]; // Buffer para a stringa digitada no terminal.
scanf("%s", str); // Especificador %s sem limite.
printf("str: %s\n", str);
printf("ref: %s\n", ref);
Ao ser compilado e executado, os 10 bytes de ref
serão escritos na pilha
e, acima deles (em um endereço 10 bytes mais baixo), serão reservados os
10 bytes para o buffer str
.
Se o usuário digitar abc
, esta parte da pilha ficará assim:
+------+------+------+------+------+------+------+------+------+------+ ref | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 | +------+------+------+------+------+------+------+------+------+------+ 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F 0x10 0x11 0x12 0x13 +------+------+------+------+------+------+------+------+------+------+ str | 0x61 | 0x62 | 0x63 | 0x00 | lixo | lixo | lixo | lixo | lixo | lixo | +------+------+------+------+------+------+------+------+------+------+ 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09
A impressão no terminal será:
str: abc ref: 123456789
Mas, se digitar abcdefghjij
, este será o resultado:
buffer overflow ↓ +------+------+------+------+------+------+------+------+------+------+ ref | 0x00 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 | +------+------+------+------+------+------+------+------+------+------+ 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F 0x10 0x11 0x12 0x13 +------+------+------+------+------+------+------+------+------+------+ str | 0x61 | 0x62 | 0x63 | 0x64 | 0x65 | 0x66 | 0x67 | 0x68 | 0x69 | 0x6A | +------+------+------+------+------+------+------+------+------+------+ 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09
E isso vai imprimir:
str: abcdefghij ref:
Porque o caractere nulo (0x00
) foi escrito no décimo primeiro endereço
após o endereço de str
(endereço 0x0A
), que é o primeiro byte de ref
.
Esse problema poderia ser evitado especificando o limite do tamanho da string:
char ref[10] = "123456789"; // Uma string qualquer.
char str[10]; // Buffer para a stringa digitada no terminal.
scanf("%9s", str); // Limite de 9 bytes para a string.
printf("str: %s\n", str);
printf("ref: %s\n", ref);
Executando e digitando abcdefghij
, os dados na pilha seriam…
+------+------+------+------+------+------+------+------+------+------+ ref | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 | +------+------+------+------+------+------+------+------+------+------+ 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F 0x10 0x11 0x12 0x13 +------+------+------+------+------+------+------+------+------+------+ str | 0x61 | 0x62 | 0x63 | 0x64 | 0x65 | 0x66 | 0x67 | 0x68 | 0x69 | 0x00 | +------+------+------+------+------+------+------+------+------+------+ 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09
E nós veríamos isso no terminal:
str: abcdefghi <- O 'j' não foi consumido... ref: 123456789