Faire sauter les diacritiques et casser les ligatures d’une chaîne de caractères

Sommaire

Introduction

Je veux écrire un filtre qui, dans une chaîne de caractères :

En résumé, il s’agit de produire en sortie une chaîne dont les lettres appartiennent au répertoire de caractères du codage ASCII.

Les exemples qui suivent ont été testés sur Debian 12 et macOS 14. Le comportement de certaines commandes – tr et iconv – varie d’un système à l’autre : je signale les différences que j’ai pu constater et m’efforce de rechercher une solution qui soit portable.

Quels sont les caractères concernés ?

Une circulaire de 2014 émanant du ministère de la Justice et relative à l’état-civil donne une liste de « voyelles et consonnes accompagnées d’un signe diacritique connues de la langue française » ; elle mentionne également les ligatures.

Nous nous en tiendrons aux caractères de cette liste, c’est-à-dire :

àâäéèêëïîôöùûüÿçæœÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇÆŒ

À partir de la chaîne ci-dessus, le filtre doit produire la sortie suivante :

aaaeeeeiioouuuycaeoeAAAEEEEIIOOUUUYCAEOE

Avec tr : mauvaise idée

On s’attendrait à ce qu’une commande comme tr liste1 liste2 – dans laquelle l’énième caractère de liste 1 est remplacé par l’énième caractère de liste 2 – nous soit utile (voir mes notes sur la commande tr).

Or deux difficultés se présentent :

La première est que tr remplace un caractère par un seul et unique caractère : on ne peut donc s’en servir pour traiter les ligatures, car il faut pour cela substituer deux caractères (o et e par exemple) à un seul (œ). Si l’on choisi d’utiliser tr pour les diacritiques, il sera donc nécessaire d’envoyer sa sortie vers un second programme qui finira le travail. Je cherche plus simple.

Seconde difficulté : la version de tr fournie par les GNU coreutils (qui est celle installée sur les systèmes GNU/Linux) ne gère pas les caractères accentués. Ou plutôt elle ne gère pas les caractères qui sont codés sur plus d’un octet, ce qui est le cas des lettres accentuées (et des ligatures) dans le codage UTF-8. Sur ce dernier point, je renvoie là encore à mes notes sur tr.

Voici simplement deux exemples de problèmes qu’on peut rencontrer avec l’implémentation GNU de la commande tr :

$ echo 'été' | tr 'ét' 'ub'
ubbub

On attendrait ubu, on a ubbub.

Si on essaye avec tous nos caractères accentués, on génère de la poésie d’avant-garde :

$ echo 'àâäéèêëïîôöùûüÿçÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇ' | tr 'àâäéèêëïîôöùûüÿçÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇ' 'aaaeeeeiioouuuycAAAEEEEIIOOUUUYC'
CaCeCeCiCoCuCuCcCACECECICOCUCUCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

(Essayez de réciter à voix haute la sortie de cette commande, vous épaterez votre auditoire.)

La version de tr installée par défaut sur macOS, quant à elle, gère bien les caractères accentués :

$ echo 'été' | tr 'ét' 'ub'
ubu

Signalons au passage qu’elle permet d’utiliser les classes d’équivalence de POSIX :

$ echo 'aàâ' | tr '[=a=]' 'a'
aaa

(Avec la version GNU, ça ne marche pas, la même commande renvoie la sortie suivante : aàâ.)

Avec sed : cela fonctionne sur GNU/Linux et macOS

La commande y de sed effectue des translittérations à la manière de tr. Elle s’utilise sous la forme suivante : y/liste 1/liste 2/, dans laquelle liste 1 est la liste des caractères à remplacer et liste 2 celle des caractères de remplacement. De même qu’avec tr, l’énième caractère de liste 1 est remplacé par l’énième caractère de liste 2.

Bonne nouvelle, les caractères à plusieurs octets sont pris en charge, que l’on travaille sur GNU/Linux ou macOS :

$ sed 'y/àâ/aa/' <<< 'aàâä'
aaaä

La commande y de sed possède la même limitation que tr : elle remplace un caractère par un unique caractère. Pour les ligatures, on peut toutefois faire suivre la commande y par une série de commandes s, qui opèreront les substitutions voulues – par exemple s/æ/ae/g remplacera toutes les occurrences de æ par ae (le g final indiquant de remplacer toutes les occurrences de æ).

Autre difficulté posée par sed : pour écrire notre commande, il faut établir la liste de tous les caractères à rechercher, puis celle de tous les caractères de remplacement. Un oubli dans l’une ou l’autre liste, et la sortie sera faussée. Ce n’est donc pas le genre de chose qu’on peut composer à la volée dans son terminal ; il faut en faire un script.

À quoi ressemblerait ce script ? Voici une proposition :

$ cat translit-avec-sed
#!/bin/sh

avec_diacritiques=àâäéèêëïîôöùûüÿçÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇ
sans_diacritiques=aaaeeeeiioouuuycAAAEEEEIIOOUUUYC

sed "y/$avec_diacritiques/$sans_diacritiques/ 
     s/æ/ae/g
     s/Æ/AE/g
     s/œ/oe/g
     s/Œ/OE/g"

J’ai stocké les listes de caractères dans des variables : je peux ainsi m’assure d’un coup d’œil qu’elles font la même longueur, et il est plus facile de les modifier, par exemple pour les adapter à d’autres langues.

Essayons de lancer notre script :

$ resultat_attendu=aaaeeeeiioouuuycaeoeAAAEEEEIIOOUUUYCAEOE
$ resultat_obtenu="$(echo "àâäéèêëïîôöùûüÿçæœÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇÆŒ" | ./translit-avec-sed)"
$ [[ $resultat_attendu = $resultat_obtenu ]] && echo "OK"
OK

On peut le modifier pour qu’il soit exécuté directement par sed :

#!/usr/bin/sed -f

y/àâäéèêëïîôöùûüÿçÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇ/aaaeeeeiioouuuycAAAEEEEIIOOUUUYC/

s/æ/ae/g
s/Æ/AE/g
s/œ/oe/g
s/Œ/OE/g

Avec iconv : cela fonctionne sur GNU/Linux mais pas sur macOS

Le programme iconv convertit du texte d’un codage de caractères à un autre. Par exemple, pour passer en UTF-8 un fichier codé en UTF-16, on lancera la commande suivante, dans laquelle l’option -f indique le codage d’entrée et -t celui de sortie :

$ iconv -f UTF-16 -t UTF-8 < fichier-utf-16 > fichier utf-8
$ file fichier-utf-*
fichier-utf-16: Unicode text, UTF-16, little-endian text
fichier-utf-8:  Unicode text, UTF-8 text

Comme indiqué dans l’introduction, nous souhaitons que les lettres écrites en sortie de commande appartiennent toutes au répertoire du codage ASCII. Essayons donc d’utiliser iconv avec les options -f UTF-8 et -t ASCII :

$ iconv -f UTF-8 -t ASCII <<< 'àâäéèêëïîôöùûüÿçæœÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇÆŒ'
iconv: séquence d'échappement non permise à la position 0

La commande renvoie une erreur, à laquelle le manuel indique qu’il est possible de remédier :

Lorsque la chaîne //TRANSLIT est ajoutée à encodage-cible, la translitération est activée. Cela signifie que lorsqu’un caractère ne peut pas être représenté dans le jeu de caractères cible, il pourra être approximé par un ou plusieurs caractères de forme similaire. Les caractères qui sont en-dehors du jeu de caractères cible et pour lesquels la translitération n’est pas possible sont remplacés par un point d’interrogation (?) dans l’affichage.

Essayons :

$ iconv -f UTF-8 -t ASCII//TRANSLIT <<< 'àâäéèêëïîôöùûüÿçæœÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇÆŒ'
aaaeeeeiioouuuycaeoeAAAEEEEIIOOUUUYCAEOE

Cela fonctionne. Nous avons donc une commande plutôt simple à écrire, qui traite d’un coup diacritiques et ligatures, et qui fait le travail pour d’autres caractères que ceux qui figurent dans la circulaire de 2014.

Pour simplifier davantage, on peut en faire un alias :

$ echo "alias ,2ascii='iconv -f UTF-8 -t ASCII//TRANSLIT'" >> ~/.bash_aliases 
$ tail -n1 ~/.bash_aliases 
alias ,2ascii='iconv -f UTF-8 -t ASCII//TRANSLIT'
$ . ~/.bash_aliases
$ echo 'à' | ,2ascii 
a

Malheureusement la version d’iconv installée sur macOS n’offre pas la possibilité d’ajouter la chaîne //TRANSLIT à l’encodage-cible.

$ iconv -f UTF-8 -t ASCII <<< 'àâäéèêëïîôöùûüÿçæœÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇÆŒ'
iconv: iconv(): Illegal byte sequence
$ iconv -f UTF-8 -t ASCII//TRANSLITT <<< 'àâäéèêëïîôöùûüÿçæœÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇÆŒ'
???"O`U"UAE`UAE^U"O
iconv: warning: invalid characters: 32

Avec le module unicodedata de Python : cela fonctionne sur GNU/Linux et macOS

Voici enfin un script en Python qui apporte une solution à ce problème, et qui devrait fonctionner sur GNU/Linux et sur macOS :

#! /usr/bin/env python3

import sys
import unicodedata

def remove_diacritics(str):
    nfd_str = unicodedata.normalize("NFD", str)
    # unicode 'Mn' category = mark,nonspacing
    letters_without_diacritics = [
        c for c in nfd_str
        if unicodedata.category(c) != 'Mn'
     ]
    return unicodedata.normalize(
        "NFC","".join(letters_without_diacritics))

def decompose_ligatures(str):
    return (str.replace("œ","oe")
               .replace("Œ","OE")
               .replace("æ","ae")
               .replace("Æ","AE")
           )

lines = sys.stdin.readlines()

for line in lines:
    print(decompose_ligatures(
          remove_diacritics(line)), end='')

L’indentation du programme peut paraître étrange, mais j’ai essayé de garder des lignes courtes pour la présentation sur cette page.

Le code repose sur le module unicodedata, bien pratique pour manipuler des caractères Unicode.

Il fait appel à deux fonctions distinctes, l’une pour les diacritiques et l’autre pour les ligatures.

Je n’ai pas trouvé comment scinder les ligatures, sinon par un codage en dur – le programme doit donc être adapté si le texte contient d’autres ligatures que les quatre du français.

En revanche le traitement des diacritiques est propre :

L’avantage du script en Python sur celui que je proposais avec sed est qu’il traite a priori tous les diacritiques, tandis que sed ne traite que les caractères qu’on lui indique. Reste qu’il est plus long à écrire – n’étant pas à l’aise en programmation, j’apprécie le shell pour sa souplesse, et même Python me paraît un peu lourd.

Conclusion

Nous avons éliminé tr pour cette tâche.

Le filtre écrit avec sed est portable, mais il faut lui indiquer tous les caractères à remplacer. Un oubli demeure possible.

Les programmes fondés sur iconv et sur Python sont plus universels :