• Aucun résultat trouvé

Lectures formatées depuis un flux

Dans le document Programmation système en C sous (Page 129-135)

char * chaîne;

size_t taille;

ssize_t retour;

while (1) { taille = 0;

chaine = NULL;

retour = getline (& chaine, & taille, stdin);

if (retour == -1)

break;

fprintf (stdout, "%d caractères lus\n", retour);

fprintf (stdout, "%d caractères alloués \n", taille);

free (chaine);

}

return (0);

}

Lors de l'exécution, on arrête le programme en tapant directement sur Contrôle-D (EOF) en début de ligne pour provoquer un échec. Nous affichons également la taille du buffer alloué par la routine, afin de pouvoir le dépasser volontairement lors de la seconde saisie.

$ ./exemple_getline ABCDEFGHIJKLMNOPQRSTUVWXYZ 27 caractères lus

120 caractères alloués

ABCDEFGHIJKLMNOPORSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPORSTU VWXYZABCDEF

GHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPORSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ 157 caractères lus

240 caractères alloués

$

Nous avons vu comment lire des caractères ou des chaînes. Nous allons à présent nous intéresser à la manière de recevoir des informations correspondant à d'autres types de données.

Lectures formatées depuis un flux

La saisie formatée de données se fait avec les fonctions de la famille scanf( ). Comme pour la famille printf( ), il existe six versions ayant les prototypes suivants :

int scanf (const char * format, ...);

int vscanf (const char * format, va_list arguments);

int fscanf (FILE * flux, const char * format, ...);

int vfscanf (FILE * flux, const char * format, va_list arguments);

int sscanf (const char * chaine, const char * format, ...)

int vsscanf (const char * chaine, const char * format, va_list args);

Pareillement, il existe en fait trois types de fonctions, chacune disponible en deux versions, avec un nombre variable d'arguments ou avec une table d'arguments. Ce dernier type nécessite l'inclusion du fichier d'en-tête <stdarg.h>.

• scanf( ) et vscanf( ) lisent les données en provenance de stdin.

• fscanf( ) et vfscanf( ) analysent les informations provenant du flux qu'on transmet en premier argument.

• sscanf( ) et vsscanf( ) effectuent la lecture formatée depuis la chaîne de caractères transmise en premier argument.

L'argument de format se présente comme une chaîne de caractères semblable à celle qui est employée avec printf( ), mais avec quelques différences subtiles.

Les arguments fournis ensuite sont des pointeurs sur les variables qu'on désire remplir.

Contrairement à printf( ), qui est assez tolérante avec le formatage demandé puisqu'elle assure de toute manière une conversion de type, il faut indiquer ici, dans la chaîne de format, le bon type de donnée correspondant au pointeur à remplir. Si on demande par exemple à scanf( ) de lire un réel doubl e et qu'on lui transmette un pointeur sur un char, le compilateur fournira un avertissement, mais rien de plus. Lors de l'exécution du programme. l'écriture débordera de la place mémoire réservée au caractère.

Voici un exemple de programme qui plante à coup sûr. Le caractère c étant alloué comme une variable automatique de la fonction main( ), lorsqu'on l'écrase avec une écriture de type double, on détruit la pile, y compris l'adresse de retour de main( ) qui est, ne l'oublions pas, une fonction comme les autres avec la simple particularité d'être automatiquement invoquée par le chargeur de programme.

exemple_scanf_1.c #include <stdio.h>

int main (void) {

char c;

puts ("Je vais me planter dès que vous aurez entré un chiffre \n");

return (scanf ("%lf", & c) _= 1);

}

Et voici le résultat, dont il n'y a pas lieu d'être fier...

$ cc -Wall exemple_scanf_1.c -o exemple_scanf_1 exemple_scanf_1.c: In function 'main':

exemple_scanf_1.c:11: warning: double format, different type arg (arg 2)

$ ./exemple scanf_1

Je vais me planter dès que vous aurez entré un chiffre 12

Segmentation fault (core dumped)

$ rm core

$

Voyons donc à présent quels sont les bons indicateurs à fournir dans la chaîne de format, en correspondance avec le type de donnée à utiliser.

Type Format

char %c

char * %s

short int %hd %hi

unsigned short int %du %do %dx %dX

int %d %i

unsigned int %u %o %x %X

long int %ld %li

type Format

unsigned long int %lu %lo %lx %lX long long int %lld %Ld %lli %Li unsigned long long int

float %llu %Lu %llo %Lo %llx %Lx %llX %LX %e %f

%g

double %le %lf %1g

long double %lle %Le %llf %Lf %llg %Lg

void * %p

Nous voyons qu'il y a en définitive quelques indicateurs principaux et des modificateurs de type. Les indicateurs généraux sont :

Indicateur Type

d Valeur entière signée sous forme décimale

i Valeur entière signée exprimée comme les constantes en C (avec un préfixe 0 pour l'octal, 0x pour l'hexadécimal...)

u Valeur entière non signée sous forme décimale o Valeur entière non signée en octal

x ou X Valeur entière non signée en hexadécimal e, f ou g Valeur réelle

s Chaîne de caractères sans espace

c Un ou plusieurs caractères

À ceci s'ajoutent les modificateurs «h» pour short, «l » pour long (dans le cas des entiers) ou pour double (dans le cas des réels), et « ll » ou « L» pour long long ou pour long double. Il existe également des conversions « %C » et « %S » pour les caractères larges, nous arborderons ce sujet dans le chapitre 23.

Notons qu'on peut insérer entre le caractère % et l'indicateur de conversion une valeur numérique représentant la taille maximale à accorder à ce champ. Ce détail est précieux avec la conversion %s pour éviter un débordement de chaîne. Il est également possible de faire précéder cette longueur d'un caractère «a », qui demandera à scanf( )d'allouer automatiquement la mémoire nécessaire pour la chaîne de caractères à lire. Cela n'a de sens qu'avec une conversion de type %s. Dans ce dernier cas, il faut transmettre un pointeur de type char **.

L'indicateur de conversion «c» est précédé d'une valeur numérique : il indique le nombre de caractères qu'on désire lire. Par défaut, on lit un seul caractère, mais il est ainsi possible de lire des chaînes de taille quelconque. Contrairement à la conversion s, la lecture ne s'arrête pas au premier caractère blanc. On peut ainsi lire des chaînes contenant n'importe quel caractère d'espacement. Par contre, scanf( ) n'ajoute pas de caractère nul à la fin de la chaîne, il faut le placer soi-même.

Lorsqu'un caractère non blanc est présent dans la chaîne de format, il doit être mis en correspondance avec la chaîne reçue depuis le flux de lecture. Cela permet d'analyser facilement des

données provenant d'autres programmes, si le format d'affichage est bien connu. En voici un exemple :

exemple_scanf_2.c #include <stdio.h>

int main (void) {

int i, j, k;

if (fscanf (stdin, "i = %d j = %d k = %d", & i, & j, & k) == 3) fprintf (stdout, "0k (%d, %d, %d)\n", i, j, k);

else

fprintf (stdout, "Erreur \n");

return (0);

}

Ce programme réussit lorsqu'on lui fournit une ligne construite sur le modèle prévu, mais il échoue sinon :

$ ./exemple_scanf_2 i=1 j=2 k=3

Ok (1, 2, 3)

$ ./exemple_scanf_2 i= 4 j= 5 k= 006 Ok (4, 5, 6)

$ ./exemple_scanf_2 45 67 89

Erreur

$

Ici, les caractères blancs dans la chaîne de format servent à éliminer tous les caractères blancs éventuels présents dans la ligne lue. Les concepteurs de la bibliothèque stdio devaient être d'humeur particulièrement facétieuse le jour où ils ont défini le comportement de scanf( ) vis-à-vis des caractères blancs et de la gestion d'erreur. En effet, lorsque scanf( ) reçoit un caractère qu'elle n'arrive pas à mettre en correspondance avec sa chaîne de format, elle le réinjecte dans le flux de lecture avec la fonction ungetc(

). Ceci se produit par exemple lorsqu'on attend un caractère particulier et qu'un autre arrive, ou lorsqu'on attend un entier et qu'on reçoit un caractère alphabétique. De nombreux débutants en langage C se sont arraché les cheveux sur le comportement a priori incompréhensible de programmes comme celui-ci.

exemple_scanf_3.c #include <stdio.h>

int main (void) {

int i;

fprintf (stdout, "Veuillez entrer un entier : ");

while (1) {

if (scanf ("%d", & i) == 1) break;

fprintf (stdout, "\nErreur, un entier svp :");

}

fprintf (stdout, "\n0k\n");

return (0);

}

La saisie se passe très bien tant que l'utilisateur ne commet pas d'erreur :

$ ./exemple_scanf_3

Veuillez entrer un entier : 4767 Ok

$

Par contre, si on entre un caractère alphabétique à la place d'un chiffre, scanf( ) le refuse, le réinjecte dans le flux et indique qu'elle n'a pu faire aucune conversion. Toute notre belle gestion d'erreur s'effondre alors, car à la tentative suivante nous allons relire à nouveau le même caractère erroné ! Cela se traduit alors par une avalanche de messages d'erreur que seul un Contrôle-C peut interrompre :

$ ./exemple_scanf_3

Veuillez entrer un entier : A Erreur, un entier svp

Erreur, un entier svp Erreur, un entier svp Erreur, un entier svp Erreur, un entier svp Erreur, un entier svp Erreur, un entier svp Erreur, un entier svp [...]

Erreur, un entier svp Erreur, un entier svp Erreur, un entier svp Erreur, un entier svp Erreur, un en (Contrôle-C)

$

Le seul moyen simple de gérer ce genre de problème est de passer par une étape de saisie intermédiaire de ligne, à l'aide de la fonction fgets( ).

exemple_scanf_4.c #include <stdio.h>

int main (void) {

char ligne [128]; int i;

fprintf (stdout, "Veuillez entrer un entier : ");

while (1){

if (fgets (ligne, 128, stdin) == NULL) {

fprintf (stderr, "Fin de fichier inattendue \n");

return (1);

}

if (sscanf (ligne, "%d", & i) == 1) break;

fprintf (stdout, "\nErreur, un entier svp : ");

}

fprintf (stdout, "Ok\n");

return (0);

}

Cette fois-ci, le comportement est celui qu'on attend :

$ ./exemple scanf 4

Veuillez entrer un entier : 12 Ok

$ ./exemple_scanf_4

Veuillez entrer un entier : A Erreur, un entier svp : Z Erreur, un entier svp : E Erreur, un entier svp : 24 Ok

$

L'autre piège classique de scanf( ) c'est qu'un caractère blanc dans la chaîne de format élimine tous les caractères blancs présents dans le flux en lecture. Lorsqu'on parle de caractères blancs, il s'agit de l'espace et de la tabulation bien sûr, mais également des retours à la ligne. En fait, il s'agit des caractères correspondant à la fonction isspace( ) que nous verrons dans le chapitre 23, c'est-à-dire l'espace, les tabulations verticales et horizontales « \t» et « \v», le saut de ligne « \n», le retour chariot « \r », et le saut de page «

\f ».

Cela a des conséquences inattendues sur un programme aussi simple que celui-ci.

exemple_scanf_5.c #include <stdio.h>

int main (void) {

fprintf (stdout, "Entrez un entier : ");

if (scanf ("%d", & i) == 1)

fprintf (stdout, "0k i=%d\n", i);

else

fprintf (stdout, "Erreur \n");

return (0);

}

Tout se passe correctement avec ce programme :

$ ./exemple_scanf_5 Entrez un entier : 12 Ok i=12

$ ./exemple_scanf_5 Entrez un entier : A Erreur

$

Par contre, supposons qu'on introduise un caractère blanc supplémentaire à la fin de la chaîne de format. Par exemple, on pourrait en croyant bien faire y ajouter un retour à la ligne « \n » pour marquer la fin de la saisie. La ligne de scanf( ) deviendrait :

if (scanf ("%d\n", & i) == 1)

Mais le comportement serait particulièrement surprenant :

$ ./exemple_scanf_6 Entrez un entier : 12 A

Ok i=12

$

Nous avons appuyé trois fois sur la touche « Entrée » à la suite de notre saisie, puis en désespoir de cause, nous avons retapé une lettre quelconque (A) suivie de «Entrée». Et c'est à ce moment seulement que notre saisie initiale a été validée !

Pourtant ce fonctionnement est tout à fait normal. Comme nous avons mis un «\n» en fin de chaîne de format – mais le résultat aurait été le même avec n'importe quel caractère blanc – scanf( ) élimine tous les caractères blancs se trouvant à la suite de notre saisie décimale. Seule-ment, pour pouvoir éliminer tous les caractères blancs, elle est obligée d'attendre d'en recevoir un qui ne soit pas blanc. Tout ceci explique l'inefficacité de nos multiples pressions sur la touche « Entrée », et qu'il ait fallu attendre un caractère non blanc, en l'occurrence A, pour que scanf( ) se termine. Notons que ce caractère non blanc est replacé dans le flux pour la lecture suivante.

Le comportement de scanf( ) est parfois déroutant lorsqu'elle agit directement sur les flux. Pour cela, il est souvent préférable de faire la lecture ligne par ligne grâce à fgets( ) ou à getline( ), et d'analyser ensuite le résultat avec sscanf( ). Celle-ci aurait en effet, dans notre dernier exemple, rencontré la fin de la chaîne, qu'elle aurait traitée comme un EOF, ce qui lui aurait permis d'arrêter la recherche d'un caractère non blanc. En voici la preuve avec le programme suivant (le test d'erreur sur fgets( ) a été supprimé pour simplifier l'exemple).

exemple_scanf_7.c #include <stdio.h>

int main (void) {

char ligne [128];

int i;

fprintf (stdout, "Entrez un entier : ");

fgets (ligne, 128, stdin);

if (sscanf (ligne, "%d\n", & i) == 1) fprintf (stdout, "0k i=%d\n", i);

else

fprintf (stdout, "Erreur \n");

return (0);

}

$ ./exemple_scanf_7 Entrez un entier : 12 Ok i=12

$ ./exemple_scanf_7 Entrez un entier : A Erreur

$

Les fonctions de la famille scanf( ) offrent également quelques possibilités moins connues que nous allons voir rapidement.

• La saisie de l'adresse d'un pointeur, avec la directive %p : ceci ne doit être utilisé qu'avec une extrême précaution, le programme étant prêt à capturer un signal SIGSEGV dès qu'il va essayer de lire le contenu du pointeur si l'utilisateur a fait une erreur.

• La lecture d'un champ sans stockage dans une variable, en insérant un astérisque juste après le caractère %. Le champ est purement et simplement ignoré, sans stockage dans un pointeur ni incrémentation du nombre de champs correctement lus. Ceci est surtout utilisé pour ignorer des valeurs lors de la relecture de la sortie d'autre programme. Imaginons par exemple un programme de dessin vectoriel qui affiche les coordonnées X et Y de tous les points qu'il a en mémoire. Lors d'une relecture de ces données, le numéro du point ne présente pas d'intérêt, aussi préfère-t-on l'ignorer avec une lecture du genre :

scanf (" point %*d : X = %lf Y = %lf", & x, & y).

• La directive %n n'effectue pas de conversion mais stocke dans le pointeur correspondant, qui doit être de type int *, le nombre de caractères lus jusqu'à présent. Cela peut servir dans l'analyse d'une chaîne contenant plusieurs champs.

Supposons par exemple que le premier champ indique de manière numérique le type du champ suivant (0 = entier, 1 = réel). Il est alors commode de stocker la position atteinte après cette première lecture, pour reprendre ensuite l'extraction avec le format approprié dans un second sscanf( ). En voici une illustration :

exemple_scanf_8.c

#define _GNU_SOURCE /* pour avoir getline( ) */

#include <stdio.h>

#include <stdlib.h>

int main (void) {

char * ligne;

int taille;

int type_champ;

int entier;

float reel;

while (1) {

fprintf (stdout, "<type> <valeur> :\n");

ligne = NULL; taille = 0;

if (getline (& ligne, & taille, stdin) = -1) break;

if (sscanf (ligne, "%d %n", & type_champ, & position) != 1) { fprintf (stdout, "Entrez le type (O=int, !=float) "

"suivi de la valeur \n");

free (ligne);

continue;

}

if (type_champ = 0) {

if (sscanf (& (ligne [position]), "%d", &entier) != 1) fprintf (stdout, "Valeur entière attendue \n");

else

fprintf (stdout, "Ok : %d\n", entier);

} else if (type_champ == 1) {

if (sscanf (& (ligne [position]), "%f", & reel) != 1) fprintf (stdout, "Valeur réelle attendue \n");

else

fprintf (stdout, "Ok : %e\n", reel);

} else {

fprintf (stdout, "Type inconnu (O ou Mn");

}

free (ligne);

}

return (0);

}

On arrête la boucle principale de ce programme en faisant échouer getline( ), en lui envoyant EOF (Contrôle-D) en début de ligne. Voici un exemple d'exécution :

$ ./exemple_scanf_8

<type> <valeur> :

Entrez le type (0=int, !=float) suivi de la valeur

<type> <valeur>

0 A

Valeur entière attendue

<type> <valeur>

0 12 Ok : 12

<type> <valeur>

1 Z

Valeur réelle attendue

<type> <valeur>

1 23.4

Ok : 2.340000e+01

<type> <valeur>

2 ZZZ

Type inconnu (0 ou 1)

<type> <valeur> :

$

Il est également possible de restreindre le jeu de caractères utilisables lors d'une saisie de texte, en utilisant une directive %[] à la place de %s. et en indiquant à l'intérieur des crochets les caractères autorisés. On peut signaler des intervalles du genre %[A-Za-z], des négations avec le signe ^ en début de directive, comme %[^0-9] pour refuser les chiffres. Si on veut mentionner le caractère « ] », il faut le placer en premier, et pour indiquer « [ », on le place en dernier, comme dans % [ ] ( ) {} [] , qui regroupe les principaux symboles d'encadrement. On notera que cette conversion ne saute pas automatiquement les espaces en tête, contrairement à %s. Comme pour cette dernière conversion, il y a lieu d'être prudent pour éviter les débordements de chaînes, soit en mentionnant une taille maximale %5[A-Z] qui convertit au plus cinq majuscules, soit en demandant à la bibliothèque d'allouer la mémoire nécessaire (en lui passant un pointeur sur un pointeur sur une chaîne).

Avec toutes leurs possibilités, les fonctions de la famille scanf( ) sont très puissantes.

Toute-fois, elles réclament beaucoup d'attention lors de la lecture des données si plusieurs champs sont présents sur la même ligne. Lorsque la syntaxe d'une ligne est très compliquée et qu'une lecture champ par champ comme dans notre dernier exemple est vraiment rébarbative, il est possible de se tourner vers un analyseur syntaxique qu'on pourra construire à l'aide de flex et bison , par exemple.

Conclusion

Nous avons examiné ici les différentes fonctions d'entrée-sortie simples pour un programme.

Comme nous l'avions déjà indiqué avec printf( ), l'évolution actuelle des interfaces graphiques conduit les utilisateurs à ,se détourner des applications dont les données sont saisies depuis un terminal classique. A moins de construire un programme qui, à la manière d'un filtre, reçoive sur son entrée standard des données provenant d'une autre application, il est de plus en plus rare d'utiliser scanf( ) ou fscanf( ) sur stdin Par contre. l'emploi de sscanf( ) est toujours d'actualité. En effet, la saisie de données par l'intermédiaire d'une interface graphique se fait souvent dans une boîte de dialogue, dont les composants de saisie renvoient leur contenu sous forme de chaîne de caractères. Il est alors du ressort du programme appelant de convertir ces chaînes dans le format de donnée qu'il désire (entier, réel, voire pointeur). Il peut utiliser à ce moment sscanf( ) ou d'autres fonctions de conversion que nous étudierons ultérieurement, comme strtol( ), strtod( ) ou strtoul( ).

Les commandes de redirection des entrées-sorties standards sont présentées. par exemple, dans [NEWHAM 1995] Le shell Bash. La plupart des fonctions de la bibliothèque C Ansi et pincipalement stdi o sont décrites dans [KERNIGHAN 1994] Le langage C, qui reste une référence incontournable.

11

Ordonnancement

Dans le document Programmation système en C sous (Page 129-135)