Saisies de données au clavier
Quelques procédures utiles
Novembre 2003
Avant-propos
Le but de ce petit document est d'indiquer quelques
fonctionnalités utilisables pour le traitement des saisies
d'information au clavier. Ceci concerne l'environnement Unix de
façon générale, et Linux en particulier. Le rédacteur de
cette note avoue sa méconnaissance du fonctionnement précis sous
Windows...
1 Rappel
Chaque programme C dispose d'un flot dit << entrée
standard >> connu sous le sobriquet de stdin (macro-définition
présente dans le fichier d'en-tête stdio.h). C'est sur ce
flot accessible en lecture seulement que la procédure
scanf (ainsi que la macro/fonction getchar)
lit ses données.
Lorsque le programme C, une fois compilé, est lancé
depuis un émulateur de terminal, cette entrée standard est
<< connectée >> au clavier (de la même façon que la sortie
standard est << connectée >> vers l'écran) : lors d'une lecture sur
stdin, l'utilisateur tape sur les touches du clavier, et
lorsqu'il appuie sur la touche Return ou
Entrée, ce qu'il a frappé est disponible pour le
programme.
2 Fonctionnement par défaut du terminal
Vous avez sûrement constaté que les données transmises vers
le programme n'étaient pas exactement la succession des
caractères tapés au clavier :
-
l'utilisation de la touche Backspace ou
Ctrl-H (cette notation signifie : appuyer d'abord sur la
touche Ctrl et tout en la maintenant enfoncée, appuyer
sur la touche H) a pour effet d'effacer le dernier
caractère saisi ;
- la touche Delete ou Suppr efface totalement
la ligne ;
- l'utilisation de la combinaison Ctrl-C interrompt le
déroulement du programme.
Vous avez aussi constaté que lorsque vous tapez une touche
<< normale >>, vous voyez apparaître le caractère frappé à
l'écran.
Tout ceci est l'oeuvre d'un composant du système d'exploitation
que l'on nomme driver tty1 ou pilote de terminal.
Ce pilote de terminal a un rôle triple (au moins) :
-
c'est lui qui est chargé d'afficher les caractères tapés
(fonction d'écho) ;
- c'est lui qui est chargé de l'édition de la ligne en cours
de saisie (possibilité d'effacer un caractère, un mot, la ligne
complète) ; cette fonctionnalité particulière est souvent
désignée dans la documentation sous le vocable de
traitement canonique de l'entrée ou input canonical
processing ;
- c'est lui qui est chargé de traiter les caractères de
contrôle (le Ctrl-C qui interrompt le programme, mais
il en existe d'autres). On parle ici de traitement des
signaux ou de signal processing.
2.1 Traitement canonique
Le traitement canonique du pilote de terminal fonctionne
en mode ligne : les caractères tapés au clavier sont
utilisés pour construire une ligne complète de données, et
c'est lors de la frappe de la touche Return que la ligne
ainsi constituée devient disponible pour le programme. Rappelons
que le caractère << fin de ligne >> est présent dans les
données envoyées au programme.
Si l'on veut forcer l'envoi vers le programme d'une ligne
incomplète (non terminée par une fin de ligne), on peut
le faire en tapant la séquence Ctrl-D (ce caractère
n'est pas ajouté à la ligne). Attention : si le pilote
clavier reçoit une ligne vide, c'est-à-dire en tapant
Ctrl-D en début de ligne, il génèrera un indicateur
de fin de fichier pour le programme qui tente la lecture.
Dans ce cas, scanf (ou getchar) retourne la valeur
EOF (définie dans le fichier d'en-tête stdio.h
comme étant la valeur numérique -1). Certains interpréteurs
de commandes considèrent cette fin de fichier comme une volonté
de l'utilisateur de terminer le traitement, et terminent alors leur
fonctionnement !
Notons enfin que c'est toute la ligne qui est envoyée en une seule
fois au programme : ainsi, si l'on effectue une boucle de lecture
par getchar, le programme, lors du premier appel, va
attendre que la ligne ait été saisie et terminée par la touche
Entrée ; tous les appels successifs à getchar
seront immédiatement satisfaits jusqu'à ce que le caractère
fin de ligne ait été lu (pas d'attente) ; le getchar
suivant provoquera une nouvelle attente, etc.
Les diverses fonctionnalités du traitement canonique sont
accessibles par certains caractères tapés au clavier, dits
<< caractères spéciaux >>. Ces caractères spéciaux sont
indiqués ci-dessous, avec leur nom symbolique :
-
erase caractère spécial effaçant le dernier caractère
tapé ; en général, Ctrl-H ou Backspace ;
- kill caractère spécial effaçant la totalité de la
ligne ; en général, Ctrl-U ;
- eof (end of file) caractère spécial forçant l'envoi
de la ligne en
cours d'édition ; en général, Ctrl-D ;
- eol (end of line) caractère spécial marquant la fin de
ligne ; en
général, Ctrl-J ou Return ou Entrée ;
- eol2 autre caractère spécial marquant la fin de ligne ;
en général, non défini ;
- werase (word erase) caractère spécial effaçant le
dernier << mot >> (efface les caractères précédemment saisi
jusqu'au premier blanc) ; en général, Ctrl-W ;
- rprnt (reprint) caractère spécial permettant de
réafficher la totalité des caractères déjà saisis dans la
ligne d'édition ; en général, Ctrl-R ;
- lnext caractère spécial permettant de << banaliser >> le
caractère suivant (utilise pour saisir les caractères spéciaux
dans la ligne !) ; en général, Ctrl-V ; par exemple, si
l'on souhaite ajouter le caractère Ctrl-H dans la ligne,
on tape d'abord Ctrl-V puis Ctrl-H, si l'on
souhaite ajouter le caractère Ctrl-V, on tape deux fois
Ctrl-V ;
2.2 Traitement des signaux
Le pilote de terminal offre la possibilité à l'utilisateur de
générer des signaux pour les programmes en cours d'exécution.
Ce sont également des caractères spéciaux qui déclenchent
ces signaux. On dispose ainsi :
-
d'un signal d'interruption dit SIGINT ; en général,
c'est la combinaison Ctrl-C qui déclenche l'envoi de ce
signal ; la plupart des programmes, lorsqu'ils recoivent ce signal,
terminent prématurément et immédiatement leur fonctionnement ;
- d'un signal dit SIGQUIT ; en général, c'est la
combinaison Ctrl-\ qui déclenche l'envoi de ce
signal ; la plupart des programmes, lorsqu'ils recoivent ce signal,
terminent prématurément et immédiatement leur fonctionnement
(comme pour SIGINT) et génèrent une
copie de leur espace mémoire dans un fichier core ;
- d'un signal de suspension dit SIGTSTP ; en
général, c'est la combinaison Ctrl-Z qui déclenche
l'envoi de ce signal ; ceci permet de suspendre l'exécution d'un
programme, et redonne la main à l'interpréteur de commandes ; on
peut poursuivre ultérieurement l'exécution du programme stoppé
(commande bg et fg de l'interpréteur) ;
On peut insérer ces caractères spéciaux dans la ligne de
saisie en les précédant du caractère d'échappement
(lnext) décrit dans le paragraphe précédent : par
exemple, la séquence Ctrl-V Ctrl-C permet
d'ajouter le caractère Ctrl-C (code ASCII 3) dans la
ligne de saisie.
2.3 Traitement de l'écho
Par défaut, tout caractère tapé au clavier est affiché (en
cours de saisie) sur l'écran. Attention : cet écho ne
doit pas être confondu avec l'affichage des caractères écrits
par le programme sur la sortie standard, également connectée
à l'écran !
D'autres fonctions sont offertes par cette fonctionnalité d'écho :
-
echoe lors d'une frappe du caractère d'effacement, l'écho
génère la séquence : espace arrière, espace, espace
arrière (permet d'effacer << physiquement >> le caractère du
terminal) ;
- echok effectue un écho d'un saut de ligne après la frappe
d'un caractère d'effacement de la ligne ;
- echoctl effectue un écho des caractères de contrôle
(les combinaisons Ctrl-X) sous la forme
^X.
3 Quelques incidences sur la lecture de données
3.1 Un petit problème
Il faut bien avoir à l'esprit le mode de fonctionnement par
défaut du pilote de terminal lorsque l'on souhaite demander
interactivement des informations à l'utilisateur. Prenons
l'exemple de code suivant :
int n;
char c;
...
printf("Donner la valeur de n ?");
scanf("%d",&n);
printf("Taper un caractere pour continuer :");
scanf("%c",&c);
...
À l'exécution, le programme attend que l'utilisateur donne la
valeur de n, puis affiche le message Taper.. et
continue son exécution sans attendre !
Explication :
en fait, c'est le traitement par ligne qui
est responsable de ce petit problème. Lors de la saisie de la
valeur numérique n, l'utilisateur tape au clavier les
chiffres décimaux qui composent cette valeur, et valide sa ligne
en tapant sur la touche Entrée : la ligne, y compris le
caractère de fin de ligne, est donc transmise au programme.
Lorsque le premier scanf analyse les données saisies, il
interprète correctement les chiffres décimaux pour déterminer
la valeur de n : cette analyse se stoppe sur le caractère
fin de ligne, qui est remis dans les données en entrée.
Lors du second scanf, comme il reste des données de la
lecture précédente, on commence par analyser celles-ci : on n'a
besoin que d'un seul caractère (format %c) et c'est donc le
caractère fin de ligne qui est immédiatement lu, d'où la
poursuite sans attente de l'exécution du programme !
3.2 Comment éviter cela ?
3.2.1 La procédure fflush
La procédure fflush permet de forcer le vidage d'un flot :
par exemple, lorsque l'on écrit des données sur le flot
stdout, on ne voit rien apparaître à l'écran tant
que l'on n'écrit pas une fin de ligne (ou que l'on n'effectue pas
une lecture au clavier). L'utilisation de l'instruction
fflush(stdout) permet de forcer ce vidage.
Sur certains systèmes, on peut également utiliser
fflush sur un flot ouvert en lecture : l'effet est alors de
<< vider >> les données transmises au programme mais non encore
lues. C'est précisément ce que l'on cherche à faire. Hélas :
ce fonctionnement de fflush ne fait pas partie de la norme
ANSI-C, qui précise simplement que sur les flots ouverts en lecture,
l'effet de fflush est indéterminé. Linux s'en tient
à la stricte implémentation ANSI, et un appel à fflush
sur un flot ouvert en écriture retourne un code d'erreur.
On dispose sous Linux d'une fonction équivalente, nommée
__fpurge : bien qu'utilisable, le manuel précise qu'elle
n'est << ni standard ni portable >>. À utiliser avec modération,
donc...
3.2.2 Lecture en deux temps
On peut pallier ces inconvénients par la méthode dite de lecture
en deux temps :
-
toute lecture au clavier saisit une ligne complète dans un
tableau de caractères ;
- l'analyse se fait ensuite sur le tableau lu ;
La première phase peut s'effectuer par la fonction fgets
(l'utilisation de gets est totalement à éviter car
source de nombreuses failles de sécurité). Pour le seconde, on
peut avantageusement remplacer tout usage de scanf par un
appel à sscanf, procédure de comportement identique à
fscanf sauf qu'au lieu d'indiquer en premier paramètre un
flot, on indique un tableau de caractères.
Le petit exemple ci-dessus devient, avec cette méthode :
#define LINEMAX 1024
int n;
char c,line[LINEMAX];
...
printf("Donner la valeur de n ?");
fgets(line,LINEMAX,stdin);
sscanf("%d",&n);
printf("Taper un caractere pour continuer :");
fgets(line,LINEMAX,stdin);
sscanf(line,"%c",&c);
Lors du deuxième appel à fgets, les données
présentes dans le tableau line et non encore lues sont
tout simplement écrasées !
4 Programmation du pilote du terminal
Le pilote de terminal, dont le nombre de fonctionnalités est très
important (la page de manuel de ce pilote termios(3) est
souvent la plus longue du manuel Unix !), est totalement
configurable. Ce qui a été présenté ci-dessus est le
fonctionnement par défaut, et on peut donc à loisir le modifier.
4.1 Traitement non-canonique
On peut ainsi notamment supprimer le traitement canonique. Dans ce
cas, le mode de fonctionnement par ligne n'est plus actif, et les
caractères spéciaux associés (effacement d'un caractère, de
la ligne) sont interprétés comme des caractères standard.
Attention cependant : les caractères permettant de générer les
signaux sont conservés !
En mode non-canonique, les caractères saisis au clavier sont
transmis au programme selon le mode suivant :
-
le pilote satisfait (transmet les données) les demandes de
lecture dès qu'un nombre minimal de caractères a été
saisi ; attention : le nombre de caractères transmis au programme
peut être inférieur au nombre de caractères demandés ;
- le pilote dispose d'un timer ou intervalle de temps :
si cet intervalle de temps est dépassé après la lecture d'au
moins un caractère, le pilote n'attend pas que le nombre minimal de
caractères soit reçu pour transmettre les données au
programme (ceci permet d'éviter les attentes trop longues).
Ces deux paramètres sont réglables : le timer a une précision
du dixième de seconde et peut aller jusquà 25.5 secondes. Pour
le pilote, ils sont considérés comme des pseudo-caractères
spéciaux, dénommés min et time.
4.1.1 Exemple important
Il est fréquent de vouloir lire les caractères 1 par 1, dès
qu'ils sont tapés au clavier, et sans attente de la touche
Entrée : il suffit pour cela de placer le pilote de
terminal en mode non-canonique, en réglant le nombre minimal
de caractères à 1 et le timer à 0.
4.2 Écho, signaux
On peut également invalider la fonctionnalité d'écho : ceci
invalide presque toutes les fonctionnalités associées, mais on
conserve la possibilité de faire un écho du seul caractère
Entrée. Voir la documentation pour cela.
De la même façon, on peut invalider la fonctionnalité de
traitement des signaux.
5 Dans la pratique
La configuration du pilote de terminal peut se faire de deux
façons :
-
l'utilisation de la commande stty(1), lancée à
partir de l'interpréteur de commandes, modifie les paramètres du
terminal associé à l'interpréteur ; ces paramètres modifiés
le sont également pour tout programme lancé par ce dernier ;
- l'utilisation des fonctions de bibliothèque
termios(3), directement à partir du programme.
Il est à noter que certains interpréteurs (tcsh
notamment), sauvegardent les paramètres du terminal avant toute
commande, et les rétablissent ensuite : la première méthode
(par la commande stty) est donc totalement inopérante, et
c'est donc vers la deuxième qu'il faut se tourner.
5.1 Quelques utilisations de stty
Attention :
la syntaxe indiquée ici est celle
disponible sous Linux. Sous d'autres environnements Unix (Solaris,
IRIX, HP-UX), il peut y avoir quelques différences. Toujours se
reporte au manuel en ligne.
La suppression du traitement canonique se fait par la commande :
$ stty -icanon
ou la commande équivalente
$ stty cbreak
Pour revenir en mode canonique, on utilise l'une des deux versions :
$ stty icanon
$ stty -cbreak
Si l'on souhaite, en mode non-canonique, préciser les paramètres :
$ stty -icanon min 1 time 0
La suppression (et le rétablissement) du traitement des signaux se
font par :
$ stty -isig
$ stty isig
La suppression (et le rétablissement) de l'écho se font par :
$ stty -echo
$ stty echo
Enfin, on dispose des modes combinés raw (brut) et
cooked (cuisiné). Si l'on a mis son terminal dans un
état lamentable, on peut restaurer une configuration valable par
la commande
$ stty sane
5.2 Configuration en C du pilote de terminal
On a besoin d'une structure de données spécifique, nommée
struct termios pour effectuer cette configuration.
Notre but n'est pas d'expliquer en détail le contenu de cette
structure, ni comment on l'utilise. Les personnes intéressées
peuvent consulter le manuel en ligne, ou consulter les enseignants.
On trouvera néanmoins ci-dessous deux fonctions : l'une sauvegarde
la configuration du terminal, et le reconfigure de façon à
passer en mode non-canonique, sans écho et sans gestion des
signaux. Le nombre minimal de caractères est positionné à 1,
le timer à 0. La seconde fonction permet de restaurer la
configuration initiale (utile pour ne pas laisser le terminal dans
un état peu utilisable...). Ces fonctions sont utilisées dans
un petit programme qui affiche tous les caractères tapés : c'est
la touche Delete ou Suppr qui permet de terminer
le fonctionnement. Vous pouvez le compiler, le lancer, et essayer de
taper Ctrl-C, Backspace, Entrée...
#include <stdio.h>
#include <termio.h>
#include <unistd.h>
/* cette fonction reconfigure le terminal, et stocke */
/* la configuration initiale a l'adresse prev */
int reconfigure_terminal (struct termios *prev)
{
struct termios new;
if (tcgetattr(fileno(stdin),prev)==-1) {
perror("tcgetattr");
return -1;
}
new.c_iflag=prev->c_iflag;
new.c_oflag=prev->c_oflag;
new.c_cflag=prev->c_cflag;
new.c_lflag=0;
new.c_cc[VMIN]=1;
new.c_cc[VTIME]=0;
if (tcsetattr(fileno(stdin),TCSANOW,&new)==-1) {
perror("tcsetattr");
return -1;
}
return 0;
}
/* cette fonction restaure le terminal avec la */
/* configuration stockee a l'adresse prev */
int restaure_terminal (struct termios *prev)
{
return tcsetattr(fileno(stdin),TCSANOW,prev);
}
/* exemple d'utilisation */
int main (int argc,char *argv[])
{
struct termios prev;
int nb,c;
if (reconfigure_terminal(&prev)==-1)
return 1;
for (nb=0;;) {
c=getchar();
nb++;
(void) printf("carac[%d]=(%d,%o,%x)",nb,c,c,c);
if (c==127) {
printf(" char=DEL\n%d caracteres\n",nb);
break;
}
if (c>=32)
printf(" char=%c\n",c);
else
printf("\n");
}
if (restaure_terminal(&prev)==-1)
return 1;
return 0;
}
6 Modification de l'entrée standard
Ainsi que cela a été indiqué, tout programme lancé à
partir d'un interpréteur de commandes se retrouve avec son
entrée standard connectée sur le terminal de l'interpréteur.
Il est toutefois possible de modifier ce fonctionnement.
6.1 Redirection à partir d'un fichier
On peut utiliser en lieu et place du clavier un fichier dans
lequel on a écrit toutes les réponses/données nécessaires
pour le programme. Par exemple, imaginons un programme qui demande
à l'utilisateur un nombre de données, puis saisit le nombre
indiqué de valeurs numériques. On peut alors créer un fichier
prog.in (le nom n'a aucune importance !) qui contient :
3
123
345
206
et lancer le programme en indiquant que la saisie des paramètres
se fait dans le fichier prog.in, en lançant le
programme par :
$ ./prog < prog.in
NB :
si ce sont les résultats affichés par le
programme sur la sortie standard
que l'on souhaite conserver dans un fichier prog.out
(encore une fois, le nom n'a aucune importance) au lieu de les
écrire sur le terminal, on utilise la syntaxe suivante :
$ ./prog > prog.out
les deux opérations étant combinables :
$ ./prog < prog.in > prog.out
$ ./prog > prog.out < prog.in
6.2 Redirection à partir des résultats d'un programme
Il peut y avoir des cas où le fonctionnement suivant est attendu :
un premier programme affiche des résultats (sur sa sortie
standard), ces résultats devant être saisi sous la
même forme par un second programme pour obtenir le résultat
final. On peut penser à stocker les résultats intermédiaires
dans un fichier, sur lequel on redirige l'entrée standard du
second programme, comme dans l'exemple suivant :
$ ./prog1 > resultats
$ ./prog2 < resultats
$ rm resultats
On peut en fait éviter de créer ce fichier intermédiaire, et
tout effectuer en une seule commande :
$ ./prog1 | ./prog2
Attention à bien respecter l'ordre ! On peut même
généraliser ceci à n programmes :
$ ./prog1 | ./prog2 | ./prog3 | ./prog4
où pour tout i, la sortie standard de progi est
connectée sur l'entrée standard de progi+1. En
terminologie Unix, une telle succession de programmes porte le nom
de tube (pipe en anglais), les programmes
intermédiaires (ni le premier, ni le dernier) prenant le nom de
filtres. Il existe de très nombreux filtres, qui, en les
combinant astucieusement, permettent d'obtenir une grande
variété de résultats.
- 1
- le nom tty est
le diminutif de teletype désignant une imprimante
connectée à l'ordinateur par une ligne série ; ce nom est
resté pour désigner le pilote de la ligne série, sur
lesquelles on a connecté ensuite des terminaux alphanumériques ;
ces terminaux sont maintenant remplacés par des émulateurs de
terminal en environnement multi-fenêtré, mais le nom initial est
resté !
This document was translated from LATEX by
HEVEA.