I. Historique▲
28 Novembre 2005
Rectification d'une fuite mémoire dans le second exemple
Ajout du caractère de fin de ligne après le strncpy dans le second exemple
II. Introduction▲
Les expressions régulières (1) sont des chaînes de caractères qui se contentent de décrire un motif. Elles ont la réputation d'avoir une syntaxe difficile à apprendre et à relire, ce qui, j'espère vous en convaincre, est faux.
Ce tutoriel utilise la bibliothèque regex qui fait partie du projet GNU. Cette bibliothèque nécessite un système compatible POSIX.2 (la compilation s'est déroulée sans problème avec cygwin sous Windows XP).
III. Les fonctions de la bibliothèque regex▲
III-A. regex_t▲
Le type regex_t est une structure de données destinée à stocker une expression régulière sous forme compilée. Voici la définition de la structure telle qu'elle est déclarée dans regex.h :
struct
re_pattern_buffer
{
/* [[[begin pattern_buffer]]] */
/* Space that holds the compiled pattern. It is declared as
`unsigned char *' because its elements are
sometimes used as array indexes. */
unsigned
char
*
buffer;
/* Number of bytes to which `buffer' points. */
unsigned
long
allocated;
/* Number of bytes actually used in `buffer'. */
unsigned
long
used;
/* Syntax setting with which the pattern was compiled. */
reg_syntax_t syntax;
/* Pointer to a fastmap, if any, otherwise zero. re_search uses
the fastmap, if there is one, to skip over impossible
starting points for matches. */
char
*
fastmap;
/* Either a translate table to apply to all characters before
comparing them, or zero for no translation. The translation
is applied to a pattern when it is compiled and to a string
when it is matched. */
char
*
translate;
/* Number of subexpressions found by the compiler. */
size_t re_nsub;
/* Zero if this pattern cannot match the empty string, one else.
Well, in truth it's used only in `re_search_2', to see
whether or not we should use the fastmap, so we don't set
this absolutely perfectly; see `re_compile_fastmap' (the
`duplicate' case). */
unsigned
can_be_null : 1
;
/* If REGS_UNALLOCATED, allocate space in the `regs' structure
for `max (RE_NREGS, re_nsub + 1)' groups.
If REGS_REALLOCATE, reallocate space if necessary.
If REGS_FIXED, use what's there. */
#define REGS_UNALLOCATED 0
#define REGS_REALLOCATE 1
#define REGS_FIXED 2
unsigned
regs_allocated : 2
;
/* Set to zero when `regex_compile' compiles a pattern; set to one
by `re_compile_fastmap' if it updates the fastmap. */
unsigned
fastmap_accurate : 1
;
/* If set, `re_match_2' does not return information about
subexpressions. */
unsigned
no_sub : 1
;
/* If set, a beginning-of-line anchor doesn't match at the
beginning of the string. */
unsigned
not_bol : 1
;
/* Similarly for an end-of-line anchor. */
unsigned
not_eol : 1
;
/* If true, an anchor at a newline matches. */
unsigned
newline_anchor : 1
;
/* [[[end pattern_buffer]]] */
}
;
typedef
struct
re_pattern_buffer regex_t;
Dans le cadre de ce tutoriel, seul le champ re_nsub va nous intéresser.
III-B. regcomp▲
int
regcomp (
regex_t *
preg, const
char
*
regex, int
cflags);
Cette fonction a pour but de compiler l'expression régulière regex fournie sous forme de chaîne de caractères pour la transformer en structure de type dont l'adresse est passée en premier argument. Il est possible de modifier le comportement de cette fonction par l'intermédiaire de cflags qui peut être un OU binaire de l'une des constantes suivantes :
- REG_EXTENDED : permet d'utiliser le modèle d'expression régulière étendu plutôt que le mode basique qui est l'option par défaut ;
- REG_ICASE : permet d'ignorer la casse (minuscules/majuscules) ;
- REG_NOSUB : compile pour vérifier uniquement la concordance (vrai ou faux) ;
- REG_NEWLINE : Par défaut, le caractère de fin de ligne est un caractère normal. Avec cette option, les expressions '[^', '.' et '$' (nous reviendrons plus tard sur la signification de ces expressions) incluent le caractère de fin de ligne implicitement. L'ancre '^' reconnaît une chaîne vide après un caractère de fin de ligne.
En cas de succès, la fonction retourne 0. En cas d'échec de la compilation, retourne un code d'erreur non nul.
III-C. regexec▲
int
regexec (
const
regex_t *
preg, const
char
*
string, size_t nmatch, regmatch_t pmatch[], int
eflags);
Une fois notre expression compilée, on va pouvoir la comparer à la chaîne de caractères string à l'aide de cette fonction. Le résultat de la comparaison sera stocké dans le tableau pmatch alloué à nmatch éléments par nos soins. Il est possible de modifier le comportement de grâce à eflags :
- REG_NOTBOL : le premier caractère de la chaîne échoue toujours. Ceci n'a pas d'effet s'il s'agit du caractère de fin de ligne et que l'option REG_NEWLINE a été utilisée pour compiler l'expression régulière ;
- REG_NOTEOL : idem sauf qu'il s'agit du dernier caractère. La remarque à propos de REG_NEWLINE est également valable
retourne 0 en cas de succès ou REG_NOMATCH en cas d'échec.
III-D. regerror▲
size_t regerror (
int
errcode, const
regex_t *
preg, char
*
errbuf, size_t errbuf_size);
Cette fonction permet d'obtenir la description correspondante au code d'erreur errcode retourné par la fonction lors de la création de preg. Le message d'erreur d'au plus errbuf_size - un caractère est placé dans le tampon errbuf. retourne la taille du message d'erreur.
III-E. regfree▲
void
regfree (
regex_t *
preg);
Libère la mémoire allouée lors de la compilation.
IV. Les expressions régulières▲
La syntaxe des expressions régulières est celle décrite par le programme GNU grep.
Dans l'expression, un caractère vaut lui même (mis à part quelques métacaractères qui devront être échappés) :
a
Reconnaîtra toutes les chaînes contenant la lettre a. Il est bien sûr possible de tester la présence de plusieurs caractères placés les uns à la suite des autres :
egu
Reconnaîtra, par exemple, les chaînes « Legume » et « Expression reguliere ». Pour donner le choix entre plusieurs lettres, il suffit de mettre le groupe entre crochets :
[mt]oi
Toutes chaînes contenant la lettre 'm' ou 't' suivie des deux lettres 'o' et 'i' seront valides.
Si l'on souhaite reconnaître l'une des lettres de l'alphabet en minuscule, on pourrait écrire :
[abcdefghijklmnopqrstuvwxyz]
Mais cela peut vite s'avérer lourd dans une expression qui voudrait tester la validité d'une adresse mail. Il est donc possible de faire des regroupements en indiquant uniquement les bornes de l'ensemble séparé par un tiret :
[a-z]
Revient au même. Il existe un certain nombre d'ensembles prédéfinis dont voici la liste :
Ensemble prédéfini |
Correspondance |
---|---|
[:digit:] |
0-9 |
[:alpha:] |
A-Za-z |
[:alnum:] |
0-9A-Za-z |
[:cntrl:] |
Les caractères de contrôles (code ASCII 0177 et inférieur à 040) |
[:print:] |
Les caractères imprimables |
[:graph:] |
Idem [:print:] sans l'espace |
[:lower:] |
a-z |
[:upper:] |
A-Z |
[:punct:] |
Ni [:cntrl:] ni [:alnum:] |
[:space:] |
\n\t\r\f |
[:xdigit:] |
0-9a-fA-F nombres hexadécimaux |
Ces définitions concordent avec celles que l'on trouve dans le fichier d'en-tête ctype.h. Le point '.' permet de reconnaître n'importe quel caractère. Il est aussi possible de préciser le nombre de répétitions que l'on souhaite pour un élément :
Opérateurs |
Signification |
---|---|
? |
L'élément est répété, au plus une fois |
* |
L'élément est présent 0 ou plus de fois |
+ |
L'élément est présent au moins une fois |
{n} |
L'élément est présent exactement n fois |
{n,} |
L'élément est présent au moins n fois |
{n,m} |
L'élément est présent entre n et m fois |
Un élément est un groupe délimité par des crochets qui sont optionnels si le groupe ne comporte qu'un élément. Voici un exemple pour reconnaître si une chaîne contient trois 'a' consécutifs :
[a]{
3
}
L'opposé d'une expression est obtenu en la faisant précéder par le caractère '^'. Si l'on souhaite donner le choix entre deux expressions, il suffit de les séparer par le caractère '|'.
Ce même caractère peut être placé au début de l'expression régulière pour préciser que la chaîne à analyser doit commencer par l'élément suivant :
^[A-Z]
Précise que la chaîne doit commencer par une lettre majuscule. Le caractère '$' a le même rôle, mais cette fois en fin de chaîne.
Pour finir, voici la liste des méta caractères ainsi que la manière de les échapper :
Méta caractères |
Echappés en |
---|---|
? |
\? |
+ |
\+ |
. |
\. |
* |
\* |
{ |
\{ |
| |
\| |
( |
\( |
) |
\) |
V. Utilisation▲
Dans les exemples qui vont suivre, nous utiliserons l'expression suivante :
^www\.[-_[:alnum:]]+\.[[:lower:]]{2,4}
Cette expression régulière permet de reconnaître une adresse Internet (un peu simplement puisque l'on se contente du nom de domaine).
Pour bien comprendre une expression, il faut l'analyser par morceaux :
^
www\.
La chaîne doit commencer (^) par trois 'w' suivis d'un point (il faut bien sûr échapper le caractère '.'). on aurait très bien pu écrire :
^
[w]{
3
}
\.
Beaucoup moins lisible.
[-
_[:alnum:]]+
Nous avons affaire à un regroupement pour laisser plusieurs possibilités, la chaîne doit contenir :
- le caractère '_' ;
- le caractère '-' ;
- un chiffre ;
- une lettre majuscule ;
- une lettre minuscule.
Et ceux au moins une fois (dû au + après les crochets).
\.
Suivie d'un point.
[[:lower:]]{
2
,4
}
Suivie de 2 à 4 lettres minuscules.
Il ne faut pas oublier que notre expression sera entre guillemets, il faut donc échapper les caractères spéciaux :
"
www
\\
.[-_[:alnum:]]+
\\
.[[:lower:]]{2,4}
"
V-A. Savoir si une chaîne correspond à un motif▲
#include <stdio.h>
#include <stdlib.h>
#include <regex.h>
int
main (
void
)
{
int
err;
regex_t preg;
const
char
*
str_request =
"
www.developpez.com
"
;
const
char
*
str_regex =
"
www
\\
.[-_[:alnum:]]+
\\
.[[:lower:]]{2,4}
"
;
/* (1) */
err =
regcomp (&
preg, str_regex, REG_NOSUB |
REG_EXTENDED);
if
(
err ==
0
)
{
int
match;
/* (2) */
match =
regexec (&
preg, str_request, 0
, NULL
, 0
);
/* (3) */
regfree (&
preg);
/* (4) */
if
(
match ==
0
)
{
printf (
"
%s est une adresse internet valide
\n
"
, str_request);
}
/* (5) */
else
if
(
match ==
REG_NOMATCH)
{
printf (
"
%s n
\'
est pas une adresse internet valide
\n
"
, str_request);
}
/* (6) */
else
{
char
*
text;
size_t size;
/* (7) */
size =
regerror (
err, &
preg, NULL
, 0
);
text =
malloc (
sizeof
(*
text) *
size);
if
(
text)
{
/* (8) */
regerror (
err, &
preg, text, size);
fprintf (
stderr, "
%s
\n
"
, text);
free (
text);
}
else
{
fprintf (
stderr, "
Memoire insuffisante
\n
"
);
exit (
EXIT_FAILURE);
}
}
}
puts (
"
\n
Press any key
\n
"
);
/* Dev-cpp */
getchar (
);
return
(
EXIT_SUCCESS);
}
- On commence par compiler notre expression régulière avec l'option REG_NOSUB pour vérifier uniquement la concordance.
- Si aucune erreur ne s'est produite, on demande l'analyse de notre chaîne.
- On n'a plus besoin de l'expression compilée, on demande la libération de la mémoire.
- La chaîne est identifiée.
- On ne retrouve pas le motif de notre expression dans la chaîne analysée.
- Ou il s'est produit une erreur.
- Dans ce cas, on appelle une première fois regerror pour connaître la taille du message d'erreur.
- Et une seconde fois pour récupérer la chaîne.
V-B. Extraire une sous-chaîne correspondant à un motif▲
Toujours avec la même expression, mais cette fois, on va récupérer les concordances. Vous avez peut être remarquer que la fonction regexec, lorsque l'on lui demande remplie un tableau de type regmatch_t dont la taille est fournie par l'utilisateur. Mais comment connaître la taille du tableau? Cette information est contenue dans le membre re_nsub la structure regex_t remplie par regcomp, mais comment cette dernière peut connaître le nombre de motifs reconnus à l'avance? Tout simplement parce que chaque motif est entouré de parenthèses dans l'expression régulière, il lui suffit donc de compter le nombre de termes entre parenthèses. Il faut donc légèrement la modifier :
"
(www
\\
.[-_[:alnum:]]+
\\
.[[:lower:]]{2,4})
"
Et voici le code :
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <regex.h>
int
main (
void
)
{
int
err;
regex_t preg;
const
char
*
str_request =
"
http://www.developpez.net/forums/index.php
"
;
const
char
*
str_regex =
"
(www
\\
.[-_[:alnum:]]+
\\
.[[:lower:]]{2,4})
"
;
/* (1) */
err =
regcomp (&
preg, str_regex, REG_EXTENDED);
if
(
err ==
0
)
{
int
match;
size_t nmatch =
0
;
regmatch_t *
pmatch =
NULL
;
nmatch =
preg.re_nsub;
pmatch =
malloc (
sizeof
(*
pmatch) *
nmatch);
if
(
pmatch)
{
/* (2) */
match =
regexec (&
preg, str_request, nmatch, pmatch, 0
);
/* (3) */
regfree (&
preg);
/* (4) */
if
(
match ==
0
)
{
char
*
site =
NULL
;
int
start =
pmatch[0
].rm_so;
int
end =
pmatch[0
].rm_eo;
size_t size =
end -
start;
site =
malloc (
sizeof
(*
site) *
(
size +
1
));
if
(
site)
{
strncpy (
site, &
str_request[start], seize);
site[size] =
'
\0
'
;
printf (
"
%s
\n
"
, site);
free (
site);
}
}
/* (5) */
else
if
(
match ==
REG_NOMATCH)
{
printf (
"
%s n
\'
est pas une adresse internet valide
\n
"
, str_request);
}
/* (6) */
else
{
char
*
text;
size_t size;
/* (7) */
size =
regerror (
err, &
preg, NULL
, 0
);
text =
malloc (
sizeof
(*
text) *
size);
if
(
text)
{
/* (8) */
regerror (
err, &
preg, text, size);
fprintf (
stderr, "
%s
\n
"
, text);
free (
text);
}
else
{
fprintf (
stderr, "
Memoire insuffisante
\n
"
);
exit (
EXIT_FAILURE);
}
}
}
else
{
fprintf (
stderr, "
Memoire insuffisante
\n
"
);
exit (
EXIT_FAILURE);
}
}
puts (
"
\n
Press any key
\n
"
);
/* Dev-cpp */
getchar (
);
return
(
EXIT_SUCCESS);
}
- On commence par compiler notre expression sans l'option REG_NOSUB pour pouvoir récupérer les sous-chaînes reconnues.
- Grâce à la structure regex_t, on connaît le nombre de sous-chaînes attendues, on alloue donc un tableau de taille adéquate pour les stocker.
- On demande l'analyse de notre chaîne.
- Si la chaîne est reconnue.
- On récupère les indices de début et de fin de la sous-chaîne et on en profite pour calculer sa taille.
- On alloue un espace mémoire suffisant pour stocker notre sous-chaîne.
- On copie la chaîne d'origine de l'indice start jusqu'à seize caractères.
- Et on n'oublie pas de libérer la mémoire allouée.
VI. Conclusion▲
À présent, vous avez les connaissances nécessaires pour faire une bonne utilisation des expressions régulières en C. Si vous souhaitez en apprendre plus sur le sujet, vous pouvez consulter les pages man de la bibliothèque regex ou encore celle du programme grep; une documentation est aussi fournie avec la bibliothèque regex.
Toutefois, si vous souhaitez en faire une utilisation plus poussée, je vous conseille de vous tourner vers un langage tel que Perl.
VII. Autres documentations▲
Les expressions régulières avec l'API Regex de Java par Hugo ETIEVANT.
Utilisation des expressions régulières en .Net par MORAND Louis-Guillaume.
VIII. Remerciements▲
Merci à Gnux pour la relecture attentive de cet article.