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

 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.

 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 motifs

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 substitution fournie par une autre expression régulière. Voici le programme complet.

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

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.

 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