7 octobre

Expressions régulières en C

J'ai déjà commis un article sur les expressions régulières en C++. En C c'est un peu moins maniable.

Introduction

Pour utiliser les expressions régulières en C il faut d'abord inclure l'entête regex.h

#include <regex.h>

Nous utiliserons ici les fonctions regcomp et regexec. Voir le man de regex.

La première regcomp «compile» l'expression régulière, sous une forme nécessaire à la seconde.

Sa signature est :

int regcomp(regex_t *preg, const char *regex, int cflags);

La seconde regexec est celle qui nous permettra de trouver les champs du motif dans la cible.

Sa signature est :

int regexec(const regex_t *preg, const char *string, size_t nmatch,
                   regmatch_t pmatch[], int eflags);

preg est l'adresse de l'expression régulière «compilée» et string est la cible.

Le nombre de champs trouvés par regexec est placé dans preg->re_nsub.

Les correspondances sont renvoyées dans pmatch.

pmatch[0] est le modèle en entier.

pmatch[k] contient les décalages par rapport au début de la cible du champ n° k.

Ainsi pmatch[k].rm_so est le décalage du début de la correspondance et pmatch[k].rm_eo celui de la fin. La longueur de ce champ est donc pmatch[k].rm_eo - pmatch[k].rm_so.

Les exemples ci-dessous font référence à ceux de l'article Regex en C++.

Exemple 1

Un classique : reconnaître les champs d'une URL.

Exemple : "http://arad.free.fr/regexc.html" est l'url de cet article et nous voudrions y identifier les champs "http", "arad", "free", "fr", "regexc.html".

L'expression régulière que nous utiliserons sera :

char *pattern = "(http|https|ftp|ftps)://(.*)\\.(.*)\\.(.*)/(.*)";

Voici le code d'un programme mettant en œuvre regex

Le source est ici : regex1.c.

     1	/* regex1.c
     2	:w | !gcc % -o .%<
     3	:w | !gcc % -o .%< && ./.%<
     4	*/
     5	#include <stdio.h>
     6	#include <string.h>
     7	#include <regex.h>
     8	#include <assert.h>
     9	//
    10	int main(int argc, char *argv[]) 
    11	{
    12	   char url[] = "http://arad.free.fr/index.html";
    13	   char *pattern = "(http|https|ftp|ftps)://(.*)\\.(.*)\\.(.*)/(.*)";
    14	
    15	   regex_t reg; 
    16	   int err;
    17	   // compile pattern
    18	   err = regcomp(&reg, pattern, REG_EXTENDED);
    19	   assert(!err);
    20	   size_t nmatch = reg.re_nsub;
    21	   regmatch_t pmatch[nmatch + 1];
    22	   // exec regex
    23	   err = regexec(&reg, url, nmatch + 1, pmatch, 0);
    24	   assert(!err);
    25	   // find domain i.e. free.fr
    26	   char domain[256] = {0};
    27	   size_t start = pmatch[3].rm_so;
    28	   size_t len = pmatch[4].rm_eo - start;
    29	   strncat(domain, url + start, len);
    30	   // replace free.fr by pagesperso-orange.fr
    31	   char rpl[256] = {0};
    32	   strncat(rpl, url, start);
    33	   strcat(rpl, "pagesperso-orange.fr");
    34	   strcat(rpl, url + start + len);
    35	   // prints
    36	   printf("url: %s\n", url);
    37	   printf("domain: %s\n", domain);
    38	   printf("replace domain: %s\n", rpl);
    39	}

Commentaires

J'utilise assert pour sortir du programme en cas d'erreur (lignes 19 et 24).

Ligne 18

Compilation du motif dans reg.

Ligne 23

Exécution de regex. Nous récupérerons les correspondances dans reg.

Lignes 26 à 29

Récupération du domaine "free.fr" dans domain. Il correspond aux champs 3 et 4. Il commence à start = pmatch[3].rm_so et finit à pamtch[4].rm_eo. Sa longueur est dans len.

Lignes 31 à 34

Remplacement du domaine "free.fr" par "pagesperso-orange.fr" : On copie le début de la chaine dans rpl (ligne 32), on y ajoute le nouveau nom de domaine (ligne 33), puis on copie le reste de la chaine (ligne 34).

Lignes 36 à 38

Affichage de l'url de départ, le domaine, et l'url modifiée.

Le code source est en téléchargement ici regex1.c.

Exemple 2

Il s'agit ici de reconnaître le motif de notation d'une note de musique utilisé par lilypond. L'expression régulière que nous utiliserons est :

char pattern[] = "([a-g]es?|is?)([',]{0,3})([0-9]{0,3})([.]?)";

Dans le code qui suit j'ai défini une fonction match qui renvoie une correspondance sous forme de chaine de caractères.

Le source est ici : regex2.c.

     1	/* regex2.c
     2	:w | !gcc % -o .%<
     3	:w | !gcc % -o .%< && ./.%<
     4	*/
     5	#include <stdio.h>
     6	#include <string.h>
     7	#include <regex.h>
     8	#include <assert.h>
     9	//
    10	char * match(char *dest, char *src, regmatch_t *m, int n)
    11	{
    12	   *dest = 0;
    13	   size_t start = m[n].rm_so;
    14	   size_t end = m[n].rm_eo;
    15	   strncat(dest, src + start, end - start);
    16	   return dest;
    17	}
    18	//
    19	int main(int argc, char *argv[]) 
    20	{
    21	   char lily[] = " { aes4 c. }";
    22	   char pattern[] = "([a-g]es?|is?)([',]{0,3})([0-9]{0,3})([.]?)";
    23	   regex_t reg; 
    24	   int err;
    25	   // compile pattern
    26	   err = regcomp(&reg, pattern, REG_EXTENDED);
    27	   assert(!err);
    28	   size_t nmatch = reg.re_nsub;
    29	   regmatch_t m[nmatch + 1];
    30	   // exec regex
    31	   err = regexec(&reg, lily, nmatch + 1, m, 0);
    32	   assert(!err);
    33	   char tmp[24];
    34	   for(int k=0; k<=nmatch; k++) {
    35	      printf("field[%d]: %s\n", k, match(tmp, lily, m, k));
    36	
    37	   }
    38	}

Commentaires

Lignes 10 à 17

dest est la chaine de retour,

src est la chaine cible,

m est le tableau que construit regexec,

n est le numéro de champ, 0 pour la chaine correspondant au motif dans la cible.

Remarque : strncat est censée fermer la chaine avec un caractère nul.

Lignes 21 et 22

Données : lily est la chaine lilypond à analyser et pattern[] est le motif.

Ligne 26

Compilation du motif dans reg.

Ligne 28

Nombre de champs trouvés dans le motif

Ligne 29

Tableau de retour des correspondances.

Ligne 31

Recherche du motif pattern dans lily.

Lignes 34 et 35

Affichage des champs du motif.

Le code source est en téléchargement ici regex2.c.

Exemple 3

Dans cet exemple nous allons implémenter une fonction qui recherche dans un texte un modèle fourni par une expression régulière et remplacer toutes ses occurrences par une chaine de substitution fournie par une autre expression régulière. Voici le programme complet.

Lee source est ici : regex_replace.c.

     1	/* regex_replace.c
     2	:w | !gcc % -o .%<
     3	./.%<
     4	 */
     5	#include <stdlib.h>
     6	#include <stdio.h>
     7	#include <string.h>
     8	#include <regex.h>
     9	
    10	void  // *str MUST can be freed, i.e. obtainde by strdup, malloc, ...
    11	regex_replace(char **str, const char *pattern, const char *replace) {
    12		regex_t reg;
    13		// when regex can't commpile pattern, abort
    14		if(regcomp(&reg, pattern, REG_EXTENDED)) {
    15			printf("Could not compile regex: %s\n", replace);
    16			return;
    17		}
    18		size_t nmatch = reg.re_nsub; 
    19		regmatch_t m[nmatch + 1];
    20		const char *rpl, *p;
    21		// count back references in replace
    22		int br = 0;
    23		p = replace;
    24		while(1) { 
    25			while(*++p > 31); 
    26			if(*p) br++; 
    27			else break;
    28		} // if br is not equal to nmatch, leave
    29		if(br != nmatch) return;
    30		// look for matches and replace
    31		char *new;
    32		while(!regexec(&reg, *str, nmatch + 1, m, REG_NOTBOL)) {
    33			// make enough room
    34			new = (char *)malloc(strlen(*str) + strlen(rpl));
    35			if(!new) exit(EXIT_FAILURE);
    36			*new = 0;
    37			p = rpl = replace;
    38			int c;
    39			strncat(new, *str, m[0].rm_so); // test before pattern
    40			for(int k=0; k<nmatch; k++) {
    41				while(*++p > 16); // skip printable char
    42				c = *p;  // back referenc (e.g. \1, \2, ...)
    43				strncat(new, rpl, p - rpl); // add head of rpl
    44				// concat match
    45				strncat(new, *str + m[c].rm_so, m[c].rm_eo - m[c].rm_so);
    46				rpl = p++; // skip back reference, next match
    47			}
    48			strcat(new, p ); // trailing of rpl
    49			strcat(new, *str + m[0].rm_eo); // trainling text in *str
    50			free(*str);
    51			*str = strdup(new);
    52			free(new);
    53		}
    54		// ajust size
    55		*str = (char *)realloc(*str, strlen(*str) + 1);
    56	}
    57	
    58	int main(int argc, char *argv[]) 
    59	{
    60		char *pattern = "\\[([^-]+)->([^]]+)\\]";
    61		char *str = strdup("before [link->address] some text [link2->addr2] trail");
    62		char rpl[] = "<a href=\"\2\">\1</a>";
    63		puts(str);
    64		regex_replace(&str, pattern, rpl);
    65		puts(str);
    66		free(str);
    67	}

Nous voudrions lui fournir un texte, par exemple

"préambule [lien->adresse] suite du texte".

L'expression régulière à rechercher est "[(.+?)->(.+?)]",

et l'expression régulière du remplacement est "<a href="\2">\1</a>".

les caractères \1 et \2 sont les références aux concordances trouvées.

La sortie du programme devrait être :

"préambule <a href="adresse">lien</a> suite du texte".

Il n'y a rien de nouveau sauf l'usage qui est fait du tableau et qu'il ne faut pas perdre le nord pour mettre les choses à leur place.

Le code source est disponible ici : regex_replace.c.

Remarques

Faiblesse de regex ?

POSIX regex du C ne comprend pas les motifs tels que (?:..) en dépit du drapeau REG_EXTENDED.

Une documentation assez complète POSIX Extended Regular Expression Syntax se trouve chez boost. Sinon il y a le info de regex.

Une connerie

Reprenons l'exemple 1.

Voici un programme qui recherche les correspondances avec le motif, et les affiche toutes.

Le source est ici : regex3.c.

     1	/* regex3.c
     2	:w | !gcc % -o .%<
     3	:w | !gcc % -o .%< && ./.%<
     4	*/
     5	#include <stdio.h>
     6	#include <stdlib.h>
     7	#include <string.h>
     8	#include <regex.h>
     9	#include <assert.h>
    10	int main(int argc, char *argv[]) 
    11	{
    12	   char url[] = "ftp://arad.free.fr/regexc.html";
    13	   char *pattern = "(http|https|ftp|ftps)://(.*)\\.(.*)\\.(.*)/(.*)";
    14	   regex_t reg; 
    15	   // compile pattern in reg
    16	   assert(regcomp(&reg, pattern, REG_EXTENDED) == 0); 
    17	   // number of matchs
    18	   size_t nmatch = reg.re_nsub, mlen;
    19	   // offsets
    20	   regmatch_t match[nmatch + 1];
    21	   // matchs
    22	   assert(!regexec(&reg, url, nmatch + 1, match, 0));
    23	   // list of matchs
    24	   char words[256] = {0}, *ptr = words;
    25	   for(int n=1; n<=nmatch; n++) {
    26	      mlen = match[n].rm_eo - match[n].rm_so;
    27	      ptr = strncpy(ptr, url + match[n].rm_so, mlen) + mlen + 1;
    28	   }
    29	   // prints
    30	   puts(url);
    31	   for(ptr=words; *ptr!=0; ptr += strlen(ptr) + 1) 
    32	      printf("\"%s\" ", ptr);
    33	   puts("");
    34	}

Jusqu'à la ligne 22, rien de plus banal.

Ensuite, la variable words est dimensionnée suffisamment et initialisée avec des '\0'.

Ma coquetterie est dans les lignes 25 à 28, et particulièrement la ligne 27.

Ici je simule une liste de chaines de caractères. Dans la variable words, j'enchaine les portions trouvées en les séparant par des caractères '\0'. C'est pourquoi dans la ligne 27 j'ajoute mlen + 1 au pointeur renvoyé par strncpy.

Le résultat dans words est ftp,arad,free,fr,regexc.html,, où les virgules représentent le caractère '\0'. Le seul intérêt est de ne consommer aucun caractère imprimable comme délimiteur.

Ainsi pour l'affichage, (ligne 31) il suffit d'enjamber ces carctères nuls, d'où le + 1 à la fin du for. Ça s'arrête lorsque deux caractères nuls se suivent.

On s'amuse comme on peut.

Le code source est en téléchargement ici regex3.c.





Réalisé avec Qlam - LGPL