Les expressions régulières sont un domaine où il est très simple de faire des erreurs. Si vous avez le courage de lire celles que je présente ici, et si vous y découvrez des bourdes, merci de me corriger.

Délimiteurs

Un masque est entouré de délimiteurs. On a coutume d'utiliser des slash : /(toto)+/, mais en fait n'importe quel caractère non alphanumérique peut servir de délimiteur. Ainsi, si on a besoin de rechercher un slash dans une chaîne, on pourra soit le protéger d'un anti-slash, soit utiliser d'autres délimiteurs : /.+\/.+/ ou %.+/.+%

Les sous-masques

Les sous-masques, délimités par des parenthèses, ont plusieurs rôles:

  • délimiter une expression qui devra être présente un nombre donné de fois:
    • de zéro à n avec *
    • zéro ou une fois avec ?
    • au moins une fois avec +;
    • un nombre exact de fois avec {};
  • délimiter des alternatives : (toto|titi|tata);
  • capturer des sous-chaînes, par exemple avec preg_match;

Par défaut, chaque sous-masque est capturant, c'est à dire qu'il sera renvoyé dans les résultats. Imaginons, au hasard, que l'on veuille analyser une chaîne, composée d'un identifiant, éventuellement suivi d'un serveur (précédé de '@') éventuellement suivi d'un nom de ressource, précédé de '/'. On pourrait utiliser une expression du genre[1]:

$reg = '%([^@]+)(@([^/]+)(/(.+))?)?%'

On a ici cinq masques, les uns délimitant des expressions que l'on veut capturer, les autres servant juste à déterminer la présence de ces expressions. Ainsi preg_match($reg, 'toto@titi.org/tata', $res) retourne un tableau de six éléments : la chaîne totale correspondant au masque, et le contenu de chacun des sous-masques:

0 => toto@titi.org/tata
1 => toto
2 => @titi.org/tata
3 => titi.org
4 => /tata
5 => tata

On peut heureusement dire au moteur d'expression de ne pas capturer certains sous-masques. Il suffit de les faire débuter par ?: :

$reg = '%([^@]+)(?:@([^/]+)(?:/(.+))?)?%';

ne retournera que quatre éléments : la chaîne totale, et les expressions capturées.

Cerise sur le gatal, on peut nommer les expressions capturées pour y accéder non à partir de leur position dans le masque, mais par leur nom. Il suffit pour cela de débuter le sous-masque par ?P<nom>. Plus de re-numérotation des résultats si on rajoute des sous-masques, et une expression un peu plus lisible, AMHA.

$reg = '%(?P<login>[^@]+)(?:@(?P<server>[^/]+)(?:/(?P<resource>.+))?)?%';
preg_match($reg, 'toto@titi.org/tata', $res);
$login    = $res['login'];
$server   = ($res['server']?$res['server']:'default');
$resource = ($res['resource']?$res['resource']:'default');

Pour rendre plus lisibles vos expressions, vous pouvez aussi y ajouter des commentaires, encadrés de (?# et ).

Références arrières

Donner un nom aux sous-masques pour également s'avérer très utile lorsqu'on utilise des références arrières, c'est à dire lorsqu'on utilise le résultat d'un sous masque précédent. On peut faire cela soit avec un antislash suivi du numéro du sous-masque, soit en écrivant (?P=name) (depuis PHP 5.2.4, d'autres syntaxes sont disponibles mais celle-ci me semble la plus simple à retenir).

Par exemple pour capturer des adresses mail de la forme "Toto Titi <toto.titi@tata>", les deux syntaxes suivantes sont équivalentes ;

preg_match('/(\S+)\s+(\S+)\s+<(?i)\1\.(?i)\2@[^>]+>/', "Toto Titi <toto.titi@tata>", $res);
preg_match('/(?P<first>\S+)\s+(?P<last>\S+)\s+<(?i)(?P=first)\.(?i)(?P=last)@[^>]+>/', "Toto Titi <toto.titi@tata>", $res);

(j'utilise ici en plus une option interne, (?i) indiquant que le sous-masque suivant doit être insensible à la casse. Les chaînes "Toto Titi <Toto.Titi@tata>", "Toto Titi <toto.titi@tata>" ou "Toto Titi <TOTO.TITI@tata>" vérifieront donc toutes le masque.

Masques conditionnels

Il est possible de choisir d'utiliser un masque uniquement si une condition est remplie, voire d'en utiliser un autre si elle n'est pas remplie. La syntaxe générale est la suivante:

  • (?(condition)masque)
  • (?(condition)masque|masque sinon)

La condition elle même peut être de trois types :

  • une référence arrière;
  • une assertion;
  • un appel récursif. Dans ce cas, la syntaxe est de la forme (?(Rn)masque) avec n le numéro du sous-masque à appeler récursivement. On peut aussi appeler récursivement les sous-masques par leur nom avec (R&name). Pour ne pas agrâver mon mal de crâne, je ne traiterai pas de récursivité ici, allez regarder la doc si ça vous intéresse.

Dans le cas d'une référence arrière, la condition sera vraie si la dite référence a capturé quelque chose. L'exemple le plus classique est la capture de balises fermantes si une balise ouvrante était présente. Pour capturer une adresse mail, éventuellement encadrée de < et > on pourra utiliser

  • /(<)?[^>\s]+(?(1)>)/ : le (?(1)>) signifie qu'on ne capture le ">" final que s'il y en avait un au début
  • ou /(?P<OPEN><)?[^>\s]+(?(<OPEN>)>)/

Les assertions elles sont des masques non capturant, c'est à dire qu'elle utilisent la même syntaxe que les masques, mais ne consomment pas les caractères qu'elles testent. On distingue cinq types d'assertions :

  • les assertions "simples" : par exemple ^ sera vraie si le caractère suivant est le premier de la ligne. On utilise aussi $ pour la fin de ligne, \b pour une limite de mot (début ou fin), \B, \A, \Z, \z;
  • les assertions vrai si les caractères suivants remplissent la condition : (?=). Par exemple \w+(?=;) ramènera les mots suivis d'un point virgule, mais celui-ci ne sera pas inclus;
  • les assertions fausses si les caractères suivants remplissent la condition : (?!). Par exemple /\w+\b(?!;)/ pour les mots qui ne sont pas suivis d'un point-virgule. Notez l'utilisation du \b pour marquer la limite de mot;
  • les assertions vrai si les caractères précédents remplissent la condition : (?<=) : /(?<=-)\d+/ trouve les nombres négatifs et ramène leur valeur absolue;
  • les assertions fausses si les caractères précédents remplissent la condition : (?<!). /(?<!-)\b\d+/ trouve les nombres positifs (avec encore une fois l'utilisation de \b, sinon l'expression ramène les chiffres non précédés de -, et non les nombres;

Et on en arrive donc à nos masques conditionnels. Imaginons que l'on veuille tester une chaîne composée d'un mot, une date et un mot, séparés par ':', sachant que la date peut être au format JJ.MM.AAAA ou AAAA.MM.JJ . On pourra utiliser l'expression suivante :

/\S+:((?=\d{4})\d{4}\.\d{2}\.\d{2}|\d{2}\.\d{2}\.\d{4}):\S+/

Elle est bien de la forme ((?condition)oui|non), avec comme condition que la deuxième partie de la chaîne commence par quatre chiffres suivis d'un point. Si c'est le cas on utilise le masque d{4}\.\d{2}\.\d{2}, sinon \d{2}\.\d{2}\.\d{4}.

Correction suite au commentaire de TiTerm : mon exemple est bidon, la condition ne sert à rien puisque le moteur peut choisir de lui-même entre les 2 alternatives. c'est à dire que /\S+:(\d{4}\.\d{2}\.\d{2}|\d{2}\.\d{2}\.\d{4}):\S+/ fera la même chose. Bon, je sèche sur un exemple fonctionnel.

Voilà, je laisse pour l'instant de côté les masques récursifs et la possibilité de définir des masques pour une utilisation ultérieure. Il est utile de savoir que ça existe, mais j'avoue ne pas assez en maîtriser l'usage pour m'y aventurer.

Note : ce billet était sponsorisé par l'aspirine Lama Lalatêt.

Notes

[1] composer un billet sur les expressions régulières dans un éditeur utilisant des expressions régulières pour formater le texte, c'est du bonheur ;)