Contenu des fichiers
Introduction
Les fichiers sont un élément fondamental dans la programmation car ils permettent de stocker des informations qui survivent à l’extinction de la machine. Leur taille leur permet aussi de contenir bien plus d’informations que la mémoire vive. Par ailleurs, certains fichiers dits spéciaux ne sont que des interfaces à des sources de données. C’est pourquoi il existe plusieurs manières d’accéder au contenu d’un fichier, chaque méthode étant parfois possible pour plusieurs usages.
La méthode d’accès de bas niveau permet d’accéder aux fichiers quels qu’ils soient et d’effectuer des opérations basiques. Elle consiste principalement en l’ouverture des fichiers avec open(), leur lecture avec read(), l’écriture avec write() et la fermeture avec close(). D’autres opérations comme la lecture non bloquante (voir la recette "Effectuer une lecture non bloquante d’un fichier") ou la pose d’un verrou sur un fichier (recette "Poser un verrou sur un fichier") ne sont possibles qu’avec un accès bas niveau.
La méthode d’accès par flux implémente entre autres un tampon et un gestionnaire d’erreurs qui permettent de réduire les accès au disque, et par conséquent, d’améliorer...
Lire un fichier
Problème
Vous désirez lire un fichier.
Solution
Pour lire un fichier, commencez par l’ouvrir en lecture. S’il s’agit d’un fichier binaire, ou d’un fichier texte que vous voulez lire comme un fichier binaire, lisez-le enregistrement par enregistrement. S’il s’agit d’un fichier texte, lisez-le ligne par ligne. Il est possible aussi d’utiliser g_file_get_contents() de la bibliothèque glib.
Discussion
L’ouverture d’un fichier se fait avec fopen(). Pour lire le fichier, nous disposons de trois fonctions principales que vous choisissez selon vos besoins. Pour les fichiers texte, fgets() permet de lire une ligne. Pour les fichiers texte formaté, fscanf() attend un certain format fourni en argument et remplit les variables dont les pointeurs sont spécifiés en argument. Pour les fichiers binaires, nous disposons de la fonction fread() qui lit par enregistrements, leur nombre et taille étant donnés en argument. Voici un exemple de lecture d’un fichier texte, ligne par ligne :
FILE *fd;
char line[BUFSIZ];
if (NULL == (fd = fopen (fichier, "r")))
{
fprintf (stderr, "Impossible d'ouvrir le fichier\n");
exit (EXIT_FAILURE);
}
while (fgets (line, BUFSIZ, fd))
{
printf ("%s", line);
}
if (!feof(fd))
{
fprintf (stderr, "Problème de lecture\n");
exit (EXIT_FAILURE);
}
fclose (fd);
printf ("\n");
Ce programme utilise un espace mémoire dont la taille est donnée par la macro BUFSIZ. Cette dernière est celle qui donne la taille préférée du système pour le tampon interne utilisé par le gestionnaire de flux. En général, il s’agit de la valeur utilisée, même si elle peut être changée avec setvbuf() (ce qui est déconseillé à moins que vous ne sachiez ce que vous faites). Par ailleurs, nous utilisons feof() pour tester la fin de fichier. Cela nous garantit que si fgets()...
Écrire dans un fichier
Problème
Vous souhaitez écrire dans un fichier.
Solution
Ouvrez le fichier en écriture. Pour un fichier binaire, ou un fichier texte que vous souhaitez lire caractère par caractère, lisez-le enregistrement par enregistrement. Pour un fichier texte, lisez-le ligne par ligne.
Discussion
Pour écrire dans un fichier, commencez par l’ouvrir en écriture avec fopen(). Pour écrire un fichier texte, utilisez fputs(). Si vous souhaitez une sortie formatée, préférez fprintf(). Dans le cas de fichiers binaires, optez pour fwrite(). Notez que fputs() et fwrite() s’exécutent aussi vite car dans le pire des cas, fputs(s,fd) n’est qu’un alias pour fwrite(s, sizeof(char), strlen(s), fd). Voici un exemple d’écriture d’un tableau de chaînes de caractères (terminé par un élément nul) dans un fichier.
int
dump_array_into_file (char **lines, char *filename)
{
FILE *fd;
int i;
if (NULL == (fd = fopen (fichier, "w"))) return (-1);
for (i = 0; lines[i]; i++)
{
if (0 > fputs (line[i], fd)) return (-2);
}
fclose (fd);
return (0);
}
Pour écrire des enregistrements, voici un exemple où fprintf() écrit dans un fichier texte des données fournies sous forme binaire à la fonction :...
Lire un fichier de configuration simple
Problème
Vous souhaitez récupérer les données d’un fichier de configuration simple.
Solution
En considérant que le format du fichier est un format clé=valeur, lisez le fichier ligne par ligne et isolez la clé de la valeur en fonction du caractère d’attribution.
Discussion
Lire un fichier de configuration simple est relativement facile, en obtenir les données passe par trois étapes : lecture, analyse syntaxique et stockage du résultat. Pour la lecture, voyez la première recette de ce chapitre. Pour l’analyse syntaxique, le chapitre "Chaînes de caractères" vous aidera. Pour le stockage du résultat, il faudra une structure adaptée à la gestion de système clé/valeur comme une table de hachage ou un arbre.
En regroupant ces trois étapes, voici un exemple, qui ajoute la gestion des caractères de commentaires et des lignes vides. Chaque ligne aura donc le format clé=’valeur’ (les espaces étant autorisés en fin de lignes ou autour du signe égal), ou sera vide. Tout caractère « # » en dehors des apostrophes délimitant la valeur signifie que le reste de la ligne ne doit pas être pris en compte (présence de commentaire). Le résultat est stocké dans une table de hachage grâce à glib. En outre, même si cela n’est pas conforme aux GNU Coding Standards, nous considérerons qu’une ligne contient un maximum de 1024 octets. Un fichier de configuration simple avec des lignes plus longues n’est plus très lisible !
#define LINE_MAX_SIZE 1024
GHashTable *
config_parse (const char *config_file_name)
{
FILE *fd;
char buf[LINE_MAX_SIZE];
GHashTable *config; ...
Rechercher une donnée dans un fichier texte
Problème
Vous voulez trouver une donnée dans un fichier texte à partir d’un motif.
Solution
Lisez le fichier ligne par ligne et testez le motif sur chaque ligne.
Discussion
Pour trouver une donnée dans un fichier texte, il suffit de le lire ligne par ligne et de tester le motif sur chaque ligne. Voici un exemple ou le test de motif consiste à tester la présence du motif dans la chaîne :
int
file_parse (const char *file_name, const char *pattern, char *buffer,
int buf_size)
{
FILE *fd;
int r; /* valeur de retour */
if (NULL == (fd = fopen (file_name, "r")))
return (-1);
r = 1;
while ((1 == r) && (NULL != fgets (buffer, buf_size, fd)))
{
if (0 == strcmp (buffer, pattern))
r = 0;
}
if ((1 == r) && !feof (fd))
r = -1;
fclose (fd);
return (r);
}
Dans cet autre exemple, nous utilisons une expression régulière pour tester le motif :
int
file_parse (const char *file_name, const char *pattern, char *buffer,
int buf_size)
{
FILE *fd;
regex_t preg;
int r; /* valeur de retour */
if (regcomp (&preg, pattern, REG_NOSUB))
return (-1);
if (NULL == (fd = fopen (file_name, "r")))
return (-1);
r = 1; ...
Ajouter des données à un fichier
Problème
Vous désirez insérer des données dans un fichier, au début, à la fin voire n’importe où.
Solution
Pour insérer des données à la fin d’un fichier, ouvrez-le en mode ajout (append en anglais). Écrivez les données comme indiqué dans la deuxième recette de ce chapitre.
Pour insérer des données au début ou à n’importe quel autre endroit d’un fichier, soit vous en créez un nouveau dans lequel vous placez ce que vous désirez en fonction de l’ancien, soit vous utilisez le curseur d’entrées/sorties pour écrire les données de la fin plus loin dans le fichier, et ensuite écrire au bon endroit les données à ajouter.
Discussion
Pour insérer des données à la fin d’un fichier, ouvrez le fichier avec fopen() avec "a" (référez-vous à la seconde recette de ce chapitre pour plus de détails).
Pour insérer des données ailleurs qu’à la fin, plusieurs stratégies sont possibles. Celle qui vient à l’esprit en premier pour sa simplicité consiste à créer un nouveau fichier et à le remplir comme souhaité. Si tout se passe bien, supprimez l’ancien fichier et renommez le nouveau comme l’ancien :
#define LINE_MAX_SIZE 1024
int
file_insert (const char *file_name, const char **lines,
int endroit_ou_inserer)
{
FILE *fd, *fdtmp;
char *tmp_file; ...
Remplacer des données dans un fichier
Problème
Vous désirez remplacer un enregistrement ou toute autre donnée dans un fichier.
Solution
La méthode est semblable à celle de l’insertion de données dans un fichier. La différence se trouve dans le déplacement des données de fin de fichier : le décalage ne correspond pas à la taille des données à insérer, mais à la différence entre celle-ci et la taille des données à remplacer.
Discussion
Pour résoudre ce problème, nous pouvons nous reporter au problème de la recette précédente. Dans la méthode avec utilisation d’un fichier temporaire, il faut, après avoir inséré des lignes fournies en argument, exécuter fseek(fd, nb_a_remplacer, SEEK_CUR);.
Dans la seconde méthode, sans fichier temporaire, lors de l’initialisation de write_pos, il faut tenir compte de la taille des données à remplacer. Si elles sont moins nombreuses que les nouvelles, le fichier grandit et il suffit de reprendre le code de la recette précédente et de l’adapter. Si elles sont plus nombreuses que celles que nous allons mettre à leur place, le fichier réduit de taille. Alors nous remplaçons ce qu’il faut, puis appliquons la recette suivante pour supprimer le surplus au milieu...
Supprimer une partie d’un fichier
Problème
Vous voulez supprimer un enregistrement ou toute donnée dans un fichier.
Solution
À partir de l’endroit où vous souhaitez supprimer des données, recopiez la suite des données qui doivent rester dans le fichier. Une fois arrivé à la fin de la copie, réduisez la taille du fichier avec truncate().
Discussion
À l’inverse de l’insertion de données qui nécessite une recopie des données en partant de la fin, la suppression de données est plus simple car elle ressemble fortement à la copie de fichier, excepté que tout se passe dans le même fichier. La seule particularité se trouve dans la réduction de la taille de fichier avec truncate(). Voici un exemple :
int
file_remove (const char *file_name, int pos, int len)
{
FILE *fd;
char buffer[1024];
int read_pos, write_pos;
int file_length;
int erreur = 0;
if (NULL == (fd = fopen (file_name, "r+")))
return (-1);
/* Initialisation des pointeurs de position */
fseek (fd, 0, SEEK_END);
file_length = ftell (fd) - len;
read_pos = pos + len;
write_pos = pos;
/* Déplacement des données */
while ((0 == erreur) && !feof (fd))
{
int l;
fseek (fd, read_pos, SEEK_SET); ...
Calculer combien de lignes ou de caractères contient un fichier
Problème
Vous souhaitez connaître le nombre de ligne et/ou de caractères d’un fichier texte.
Solution
Parcourez le fichier en comptant le nombre de caractères de retour chariot et/ou le nombre de caractères.
Discussion
Le nombre de caractères se compte sans difficulté. Pour le nombre de lignes, nous ne comptons que les caractères de retour chariot. Un problème se pose, lié à l’interprétation que vous pouvez faire de ce caractère. Correspond-il à un début de ligne ou à une fin de ligne. Dans le premier cas, le nombre de lignes est le nombre de caractères de retour chariot incrémenté d’une unité car la dernière ligne, par définition, ne se termine pas par ce caractère. Si son dernier caractère était celui-là, nous considérerions que c’est une nouvelle ligne, certes vide, qui commence. Dans le second cas, le fichier contient autant de lignes que de caractères de retour chariot, sauf dans le cas fréquent où la dernière ligne ne se termine pas par ce caractère. Alors vous devez incrémenter le nombre de lignes d’une unité. L’exemple suivant illustre ce second cas :
#include <stdlib.h>
#include <stdio.h>
...
Calculer la taille d’un fichier
Problème
Vous avez besoin de connaître la taille d’un fichier.
Solution
Si le fichier n’a pas pour but d’être ouvert, utilisez stat(). Si le fichier est ouvert ou le sera avant d’avoir besoin de sa taille, vous pouvez vous servir de ftell() lorsque le pointeur de fichier est à la fin.
Discussion
Pour obtenir la taille d’un fichier sans l’ouvrir, il suffit d’appeler stat() ainsi :
size_t
file_len (char *filename)
{
struct stat buf;
if (-1 == stat (filename, &buf)) return (-1);
return (buf.st_size);
}
Si le fichier est ouvert, nous pouvons aussi utiliser ftell(). Si vous êtes déjà à la fin du fichier, cette fonction vous renvoie directement la taille du fichier. Sinon, il faut déplacer le pointeur de fichier à la fin avec fseek(). Pour un fichier dont le descripteur est fd, cela donne : fseek(fd,0,SEEK_END); taille = ftell(fd);. Bien sûr, il ne faut pas oublier de replacer le pointeur à l’endroit souhaité si vous ne souhaitez pas lire ou écrire à la fin.
Prototype
#include <sys/types.h>
#include <sys/stat.h>
int stat (const char *path, struct stat *sb);
Voir aussi la page de manuel de la fonction stat().
Effectuer une lecture non bloquante d’un fichier
Problème
Vous voulez lire un fichier, mais ne pas être bloqué si les données sont temporairement indisponibles.
Solution
Ouvrez le fichier en lecture avec fopen(fichier, O_RDONLY | O_NONBLOCK).
Discussion
La lecture non bloquante des données n’est pas possible avec les fonctions sur les flux. Il faut donc lire les données avec les fonctions de bas niveau open(), read() et close(). Il n’y a pas de particularité à la lecture non bloquante sauf la possibilité de lire une chaîne de taille nulle, ce qu’il faut donc tester. Prenons un exemple :
char buffer[1024];
int l;
int fd = open (fichier, O_RDONLY | O_NONBLOCK);
if (-1 == fd)
{
fprintf (stderr,
"Impossible d'ouvrir '%s' en lecture non bloquante\n",
fichier);
}
while (-1 != (l = read (fd, buffer, 1024)))
{
if (l > 0) printf ("Lu %d octets ('%$s')\n", l, l, buffer);
else sleep (1);
}
close (fd);
Voir aussi la recette "Créer un serveur TCP/IP mono-processus sans thread" du chapitre "Réseau" car il est aussi possible d’utiliser select() ou poll() comme décrit dans cette recette ; en effet, sockets et descripteurs de fichiers reviennent au même.
Classer un fichier texte
Problème
Vous souhaitez classer les lignes d’un fichier texte de manière alphabétique ou selon l’ordre de votre choix.
Solution
Il faut lire le fichier et le mettre en mémoire dans son intégralité afin de le classer là. Puis nous écrivons le résultat sur le disque en remplaçant l’ancien fichier. Si le fichier est de grande taille, par rapport à la taille disponible, nous préférerons un tri directement sur le disque, au prix d’une perte de performances due à une complexité plus élevée de l’algorithme de tri.
Discussion
La lecture du fichier se fait afin d’obtenir un tableau de chaînes de caractères contenant les lignes. Puis nous appliquons un algorithme de tri quel qu’il soit. Le résultat est mis dans le fichier qui est réouvert en écriture. Voici une implémentation simplifiée du programme sort qui utilise la fonction fget_line() vue dans la première recette du chapitre. Ce programme est extrêmement simplifié car il n’est capable que de lire le fichier dont le nom est donné en premier argument, et de mettre le résultat dedans, effaçant son contenu au préalable.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define A_INC 10
char...
Lire un fichier au format DOS
Problème
Vous lisez un fichier au format DOS et ne souhaitez pas être gêné par les caractères <cr><lf> de fin de ligne.
Solution
À chaque lecture du caractère de retour chariot <cr> (caractère ’\r’ ou 0xD), ne tenez pas compte de ce caractère s’il est immédiatement suivi d’un caractère de retour à la ligne <lf> (caractère ’\n’ ou 0xA).
Discussion
Lorsque vous lisez une ligne, qui prend donc fin au caractère ’\n’, vérifiez la fin de ligne et supprimez les caractères qui vous gênent. Supprimez les caractères ’\n’ et ’\r’ de cette manière :
int l = strlen (ligne) - 1;
if ('\n' == ligne[l]) ligne[l--] = '\0';
if ('\r' == ligne[l]) ligne[l--] = '\0';
Poser un verrou sur un fichier
Problème
Vous voulez signaler aux autres processus qu’un fichier est en cours de lecture ou d’écriture.
Solution
Posez un verrou avec lockf(), ou, dans le cas de la création d’un nouveau fichier, créez-le avec open() et le drapeau O_CREAT | O_EXCL ou, si votre compilateur respecte la norme C11, avec fopen() et l’attribut "x".
Discussion
La fonction lockf() permet de poser un verrou POSIX sur un fichier ouvert. Tout processus qui souhaite tester si le fichier est ainsi verrouillé peut utiliser cette même fonction pour le savoir.
int fd;
if(-1 == (fd = open (filename, O_RDWR | O_CREAT, 0640)))
{
printf ("Impossible d'ouvrir '%s' en écriture\n", filename);
exit (EXIT_FAILURE);
}
if (0 > lockf (fd, F_TLOCK, 0))
{
printf ("Le fichier %s était déjà verrouillé\n", filename);
}
else
{
printf ("Nous venons de verrouiller le fichier %s\n", filename);
lockf (fd, F_ULOCK, 0);
printf ("Nous venons de déverrouiller le fichier %s\n", filename);
}
close (fd);
Le code ci-dessus verrouille le fichier s’il ne l’est pas déjà, et le déverrouille juste après. Nous pouvons également tester si le fichier est verrouillé sans...
Créer des fichiers temporaires
Problème
Vous souhaitez créer un fichier temporaire ou tout autre fichier avec un nom unique.
Solution
Utilisez mktemp().
Discussion
La fonction mktemp() permet de créer un nom de fichier unique en fonction d’un modèle formé d’un nom de fichier et de six lettres X à la fin. Voici un exemple créant un nom de fichier à partir d’un nom existant :
char *
mon_mkstemp (const char *filename)
{
char *result;
int l = strlen (filename);
if (NULL == (result = malloc ((l + 8) * sizeof *result)))
return (NULL);
memcpy (result, filename, l);
result[l] = '.';
memset (result + l + 1, 'X', 6);
result[result + l + 8] = '\0';
return (mktemp (result));
}
Si vous souhaitez créer le fichier en même temps que son nom, vous pouvez utiliser mkstemp() qui renvoie un descripteur du fichier, ouvert avec l’attribut O_EXCL. Mais cette façon de procéder est à relativiser car en fonction du nom de fichier fourni en modèle, il y a peu de chances qu’un autre fichier soit créé avec le même nom (par pur hasard) au même instant.
Lire en continu dans un fichier qui croît
Problème
Vous voulez lire un fichier qui croît, tel un fichier de journalisation (log).
Solution
Lisez le fichier normalement, avec une boucle infinie, mais effectuez un appel à fseek(fd,SEEK_CUR,0) à chaque fois que la fin de fichier est atteinte.
Discussion
Nous pouvons lire et afficher le contenu d’un fichier qui croît avec la commande tail -f des systèmes Unix, qui se programme ainsi :
#include <stdio.h>
#include <stdlib.h>
int
main (int argc, char **argv)
{
FILE *fd;
char buffer[BUFSIZ + 1];
if (NULL == (fd = fopen (argv[1], "r")))
{
fprintf (stderr, "Impossible d'ouvrir %s\n", argv[1]);
exit (EXIT_FAILURE);
}
while (1)
{
ssize_t l;
if (BUFSIZ != (l = fread (buffer, sizeof (char), BUFSIZ, fd)))
{
if (feof (fd))
fseek (fd, 0, SEEK_CUR);
else
{
fprintf (stderr, "Problème de lecture\n");
exit (EXIT_FAILURE);
}
}
buffer[l] = '\0';
fprintf (stdout, "%s", buffer);
fflush (stdout);
}
exit (EXIT_SUCCESS);
}