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 : 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) :

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 :

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 : 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 :

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 : 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 : 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 : 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.