Introduction
Qu’est-ce qu’une expression régulière ?
Une expression régulière (traduction de l’anglais regular expression), également appelée expression rationnelle, est une chaîne de caractères servant à décrire de façon générique un ensemble de chaînes grâce à l’utilisation de caractères ayant une signification particulière. Elle est parfois aussi appelée "modèle" (traduction de l’anglais pattern).
La syntaxe utilisée pour l’écriture des expressions régulières, conçue par Ken Thompson, est dérivée de la notation utilisée par le mathématicien Stephen Kleene, qui est à l’origine du concept.
De nombreux programmes sont capables d’interpréter des expressions régulières, avec pour objectif :
-
de sélectionner des données,
-
de classer des chaînes dans certaines catégories,
-
de reconnaître des mots-clés.
Les expressions régulières sont constituées de caractères sans signification, utilisés tels quels, et de caractères ayant une signification particulière pour le programme qui va les interpréter, et donnant lieu à un traitement spécifique.
Les caractères ayant une signification particulière sont...
Qu’est-ce qu’un métacaractère ?
Un métacaractère est un caractère qui a un sens particulier pour le programme qui va le traiter. Toutefois certains peuvent être considérés comme des métacaractères dans un contexte donné, et comme des caractères standards dans un autre.
D’autre part, tout métacaractère d’un logiciel n’a pas forcément une signification particulière dans une expression régulière.
Dans le cas des interpréteurs de commandes UNIX, les caractères ; et & ont un rôle de séparateur d’instructions, et du fait de cette signification particulière sont des métacaractères pour les interpréteurs, mais ils n’ont pas de signification particulière pour la sélection des noms de fichiers, et dans ce dernier contexte sont traités comme des caractères standards.
Origine des expressions régulières
Les expressions régulières trouvent leur origine dans les travaux du mathématicien Stephen Cole Kleene, qui s’est d’abord intéressé aux fonctions récursives (ainsi qu’on peut le voir dans l’un de ses articles datant du 7 juillet 1935 : General recursive functions of natural numbers), puis ensuite aux langages réguliers (comme on peut le voir dans son document Representation of events in nerve nets and finite automata, du 15 décembre 1951). Les travaux de Kleene ont, entre autres, engendré la fameuse étoile de Kleene (*), utilisée dans la théorie des grammaires et des langages formels. Cette étoile, également appelée fermeture transitive de Kleene, ou plus simplement fermeture de Kleene, est utilisée pour indiquer un facteur de répétition (éventuellement nul) de l’élément auquel elle s’applique. On la retrouve souvent avec cette signification dans les différentes implémentations des expressions régulières.
C’est avec les travaux de Kenneth Lane Thompson (connu sous le nom Ken Thompson), l’un des principaux concepteurs du système d’exploitation UNIX, que les expressions régulières font pour la première fois leur apparition dans un logiciel.
Ken Thompson réalisa, vers la fin des années 1960, une nouvelle version (écrite en assembleur IBM 7090) de l’éditeur QED, et y intégra le concept d’expressions régulières, alors entièrement nouveau pour un éditeur de texte. Il inventa à cette occasion une manière originale de les traiter en utilisant une compilation à la volée, et déposa le 9 août 1967 un brevet qui fut publié le 2 mars 1971 sous le numéro 3.568.156 (US Patent 3,568,156, Text Matching Algorithm).
Le manuel de QED datant du 22 juin 1970 (QED Text Editor, Case 70 107-002), écrit par Dennis Ritchie et Ken Thompson, décrit en pages 3 et 4 les expressions régulières telles qu’on les retrouve dans l’éditeur ed.
Les premières expressions régulières qui ont été implémentées étaient ce que l’on appelle de nos jours des expressions régulières basiques. Elles ont ensuite évolué pour donner naissance à un autre type d’expressions régulières, appelées expressions régulières étendues.
Les expressions régulières sont maintenant très répandues : on les trouve par exemple dans de nombreuses commandes UNIX (et dans les systèmes dérivés d’UNIX : Sun OS, Xenix, HP-UX, Solaris, AIX, Linux...), dans des langages de programmation, des outils de filtrages de messages électroniques, des fichiers de configurations, etc.
Quant au nom que l’on doit attribuer à ces expressions, les avis divergent, tout du moins en ce qui concerne leur appellation en français. Faut-il les appeler expressions régulières ou expressions rationnelles ? Les termes expressions régulières font référence aux travaux du mathématicien américain Stephen Kleene sur les langages réguliers, tandis que les termes expressions rationnelles font référence...
Avertissement relatif aux expressions régulières
La syntaxe des expressions régulières diffère d’un programme à un autre, ou d’un mode à un autre (expression régulière basique ou expression régulière étendue). Parfois, elle évolue aussi au cours de l’évolution des commandes systèmes, par exemple pour mieux respecter un standard ou une norme.
La signification des métacaractères dépend du programme qui les interprète, et éventuellement d’un mode de fonctionnement sélectionné.
Avant de saisir une expression régulière, il est nécessaire de savoir comment elle sera interprétée.
Avertissement relatif aux commandes GNU/Linux
Les commandes GNU/Linux sont en évolution rapide, et apportent fréquemment des fonctionnalités ou des syntaxes nouvelles. L’adoption dans un script de fonctionnalités nouvelles et spécifiques de la dernière version d’une distribution posera des problèmes de compatibilité avec les versions plus anciennes.
Si l’on se trouve dans un contexte où des scripts (ou programmes) peuvent être utilisés sur différentes versions de systèmes d’exploitation, on aura intérêt à utiliser des syntaxes communes aux différentes versions de façon à minimiser le travail de mise en compatibilité avec les différentes plates-formes.
Les nouvelles options apportées par les récentes versions de GNU/Linux ne sont généralement pas disponibles sur les versions précédentes, et ne sont donc pas utilisables sur ces dernières versions.
Avertissement relatif à la portabilité
La syntaxe des commandes et des outils logiciels varie très souvent d’un système à un autre. Certaines options disponibles sur un système peuvent être très pratiques et efficaces, mais indisponibles sur un autre. Par exemple, une option disponible sur Linux pourra être indisponible sur Solaris, AIX ou HP-UX.
Si un même script doit être exécuté sur différentes plates-formes, il faudra veiller à utiliser une syntaxe commune à tous les systèmes cibles, généralement plus ancienne et moins pratique que l’option d’un système récent, mais présentant l’avantage d’être compréhensible par un plus grand nombre d’interpréteurs, et permettant d’écrire, de versionner et de maintenir un script unique, commun à toutes les versions, plutôt qu’une multitude de scripts, spécifiques d’une version de système.
Rappels de base concernant le shell
Le shell est un interpréteur de commandes : il lit les commandes qui lui sont passées sur son entrée standard (clavier, fichier, tube de communication), les analyse, les interprète, et exécute les commandes spécifiées.
Dans les cas courants, ce traitement peut paraître simple et sans piège, mais il faut toutefois être vigilant sur l’aspect "interprétation" du shell, en particulier sur l’interprétation de séparateurs d’arguments et des méta-caractères.
Si des chaînes de caractères contenant des espaces ou tabulations (qui sont les séparateurs par défaut du shell) doivent parvenir telles quelles à un programme, il est indispensable de l’indiquer au shell. Les quotes ont été dédiées à cet usage : supprimer la signification particulière des caractères spéciaux du shell.
Les caractères placés entre simples quotes ne sont pas interprétés, ils sont passés sans modification.
Il en est de même pour ceux placés à l’intérieur des doubles quotes, à l’exception du dollar ($), de l’antislash (\), des quotes inversées (`) et parfois du point d’exclamation (!).
Conseil important pour la qualité et la fiabilité du code
Afin d’écrire des programmes ou des scripts fiables, il est nécessaire de faire en sorte qu’ils fonctionnent quelles que soient les conditions. En particulier, les arguments qu’ils recevront devront toujours respecter la syntaxe imposée par ces scripts ou programmes.
Les problèmes rencontrés lorsqu’on analyse des dysfonctionnements, des erreurs, ou des piratages, sont parfois dus à des syntaxes trop laxistes, et mal maîtrisées, entraînant des interprétations non souhaitées, comme par exemple :
-
des métacaractères interprétés par un programme intermédiaire (comme le shell), et non par le programme à qui ils sont destinés, parce que leur interprétation n’a pas été désactivée pour le programme intermédiaire grâce à des quotes (simples ou doubles) ou des antislashes.
-
des variables contenant des séparateurs d’arguments qui vont être interprétés par un programme intermédiaire et non par le programme cible, comme ce peut être le cas avec des variables shell contenant des espaces ou tabulations, et non encadrées de doubles quotes.
Il est très important, pour écrire du code ou des commandes de qualité, de ne pas laisser de possibilité d’interprétation...
Programme d’aide à la compréhension du shell
Le shell interprète les commandes tapées au clavier ou passées dans un fichier ou via un tube de communication (pipe) et effectue parfois des traitements sur les arguments avant d’exécuter les actions (par exemple dans le cas d’évaluation de variables ou d’expressions régulières).
Il est primordial, si l’on veut éviter les erreurs d’interprétation, de fournir au shell des commandes qui fonctionnent dans tous les cas. La compréhension des mécanismes d’évaluation et de passage des paramètres est indispensable à l’écriture de commandes fiables. En particulier la signification des quotes (simples et doubles) doit être comprise et maîtrisée, afin qu’elles puissent être utilisées correctement.
Le programme args.c, donné en annexe, permet de visualiser les arguments tels qu’un programme les recevra. Il est très utile pour permettre la visualisation d’une syntaxe allégée, notamment dans le cas où une complexité entraîne une écriture relativement illisible, comme par exemple pour ce programme AWK :
awk 'function s2d(s) {
str_s = sprintf("%d", s);
cmd = "date -d '"'"'@" str_s "'"'"' '"'"'+%Y-%m-%d %H:%M:%S'"'"'";
cmd | getline;
close(cmd);
return $0;
}
{
if ($0 ~ /^#/) {
sub(/^#/, "");
printf("%s\n", s2d($0));
}
else
{ print;
printf("\n");
}
}' "$1"
Les suites de quotes ’"’"’ sont difficiles à comprendre à la première lecture. Le fait que les quotes, simples ou doubles, soient utilisées à la fois pour introduire une chaîne et pour la terminer peut induire des confusions dans l’interprétation des arguments. Pour mieux les comprendre, il faut associer les quotes par paires en leur attribuant leur signification : une quote ouvrante et une quote fermante. Lorsqu’elles sont sur la même ligne et séparées seulement de quelques caractères, l’association visuelle se fait presque instantanément, mais lorsqu’elles sont séparées de plusieurs lignes comme ce peut être le cas dans un programme AWK, les associer devient moins évident. De plus, l’exécution d’un programme par le shell plutôt que par un appel direct depuis un autre programme (avec les fonctions execl(), execv(), ou leurs dérivées) alourdit la syntaxe en nécessitant l’ajout de caractères de protection supplémentaires (quotes ou antislashes), et rend les commandes plus difficiles à lire et donc à comprendre.
Le programme args affiche les arguments tels qu’il les a reçus du shell, après substitution des métacaractères et suppression des caractères qui lui étaient spécifiquement destinés. Cela permet, grâce à une simplification de l’écriture, de mieux visualiser les caractères et leur fonction, et donc de mieux comprendre la signification des expressions compliquées.
Si l’on passe la commande AWK du script ci-dessus au programme args, il affichera les arguments suivants :
ARGC = 4
ARGV[ 1 : length = 3] = "awk"
ARGV[ 2 : length = 244] = "function s2d(s) {
str_s = sprintf("%d", s);
cmd = "date -d '@" str_s "' '+%Y-%m-%d %H:%M:%S'";
cmd | getline;
close(cmd);
return $0;
}
{
if ($0 ~ /^#/) {
sub(/^#/, "");
printf("%s\n", s2d($0));
}...