CyberSDF

Reprenez la main sur votre numérique

Accents dans un script bash : compiler la locale UTF-8 avec locale-gen

un poste de travail sobre vu de trois quarts, ecran eteint, clavier mecanique, carnet ouvert

Un é affiché « Ã© » ou remplacé par un point d’interrogation dans un script bash vient d’une locale fr_FR.UTF-8 que la machine n’a jamais compilée. Sur Debian 12, locale-gen construit cette locale à partir des lignes de /etc/locale.gen, et bash signale son absence par un avertissement « cannot change locale » au démarrage. La page locale(7) des pages de manuel Debian range ces réglages par catégories, LC_CTYPE pour les octets et LC_COLLATE pour le tri. La rubrique Linux réunit le reste de l’outillage d’un terminal.

Sommaire
  1. locale -a liste les locales compilées, pas celles que le système sait produire
  2. La locale se déclare en une ligne dans /etc/locale.gen
  3. locale-gen compile les couples déclarés et reconstruit l’archive
  4. update-locale écrit LANG dans /etc/default/locale
  5. bash: warning: setlocale: cannot change locale (fr_FR.UTF-8)
  6. La locale dit comment lire les octets, pas comment le fichier est enregistré
  7. LC_ALL passe avant tout, LANG sert de valeur par défaut
  8. Générer la locale ne tranche ni le choix du jeu ni la portée de LC_ALL
  9. Sources

locale -a liste les locales compilées, pas celles que le système sait produire

La commande locale -a affiche la liste des locales disponibles, et la page locale(1) du paquet manpages 6.03-2 pour Debian 12 définit cette liste comme « a list of all available locales ». La documentation Ubuntu précise que cette liste est celle des locales déjà générées. Un nom présent dans /usr/share/i18n/locales mais non compilé n’y figure pas. Le fichier /usr/share/i18n/SUPPORTED, cité par locale-gen(8), énumère les couples locale et charset que le système peut produire.

locale
locale -a | grep -i '^fr_FR'

Ce que vous devez voir : la seconde commande ne renvoie rien tant que fr_FR.UTF-8 n’est pas compilée. Dans la sortie de locale, les valeurs posées dans l’environnement s’affichent sans guillemets et les valeurs déduites entre guillemets, une distinction que porte locale(1) ; la ligne LC_ALL= reste vide tant que LC_ALL n’est pas posée. Ces deux contrôles font partie des commandes de base d’un terminal Linux.

La locale se déclare en une ligne dans /etc/locale.gen

Le fichier /etc/locale.gen énumère les locales à compiler, une par ligne, au format <locale> <charset> décrit par locale.gen(5). Les lignes précédées d’un # sont ignorées, comme le rappelle la section FILES de locale-gen(8) dans le paquet locales 2.36-9+deb12u10 de Debian 12. Pour le français, la ligne à activer s’écrit fr_FR.UTF-8 UTF-8. Le paquet locales ne livre que les gabarits, la même page précisant que seules les locales demandées sont compilées ; sans /etc/locale.gen, apt install locales le pose.

grep -n 'fr_FR.UTF-8' /etc/locale.gen

Ce que vous devez voir : une seule ligne, précédée du # et de son numéro. Retirer ce # suffit, à la main dans un éditeur ou par la commande suivante.

sudo sed -i 's/^#\s*fr_FR.UTF-8 UTF-8/fr_FR.UTF-8 UTF-8/' /etc/locale.gen
grep -n '^fr_FR.UTF-8' /etc/locale.gen

Ce que vous devez voir : la même ligne, sans #, et rien d’autre de modifié. La sélection par dpkg-reconfigure locales produit le même fichier, la page locale-gen(8) citant « via dpkg package configuration ».

locale-gen compile les couples déclarés et reconstruit l’archive

L’outil locale-gen lit /etc/locale.gen et appelle localedef pour chaque profil retenu, comme le décrit la page locale-gen(8). Les locales produites vont dans /usr/lib/locale, d’après locale.gen(5). Sans option, la commande remplace l’archive /usr/lib/locale/locale-archive ; l’option –keep-existing existe pour l’éviter, sa description demandant de « do not remove /usr/lib/locale/locale-archive, only compiling new locales ». L’exécution se fait avec les droits de root.

sudo locale-gen
locale -a | grep -i '^fr_FR'

Ce que vous devez voir : la seconde commande renvoie maintenant une ligne qui commence par fr_FR. L’écriture du jeu de caractères peut y prendre la forme UTF-8 ou utf8, locale(7) décrivant la recherche des données compilées sous les deux orthographes. La même page nomme l’archive /usr/lib/locale/locale-archive, et le wiki Arch donne localedef –list-archive comme contrôle de son contenu.

update-locale écrit LANG dans /etc/default/locale

La commande update-locale modifie /etc/default/locale, le fichier des réglages de locale valables pour tout le système. Sa page de manuel accepte une liste d’affectations var=locale : une variable citée sans valeur est retirée du fichier, et l’option –reset efface celles qui ne figurent pas sur la ligne de commande. La documentation Ubuntu rappelle qu’une variable d’environnement est locale au processus : un changement n’atteint pas une session déjà ouverte.

sudo update-locale LANG=fr_FR.UTF-8
grep LANG /etc/default/locale

Ce que vous devez voir : la ligne LANG du fichier, sans autre ajout. Après une nouvelle connexion, la commande locale affiche LANG=fr_FR.UTF-8 sans guillemets, puisque la valeur vient de l’environnement, et la ligne LC_ALL= reste vide. Le contrôle de l’encodage effectif passe par locale -c charmap, que locale(1) donne pour afficher le jeu de caractères courant : la réponse doit être UTF-8, précédée du nom de la catégorie LC_CTYPE.

bash: warning: setlocale: cannot change locale (fr_FR.UTF-8)

bash applique les variables de locale héritées par un appel à setlocale() au démarrage, et le fichier locale.c de sa source avertit quand cet appel échoue. Le message part sur la sortie d’erreur, précédé du nom du shell et du mot warning ajouté par error.c ; il nomme la variable fautive et la valeur qu’elle portait. Quand l’appel a renseigné errno, la source accole en plus le texte de l’erreur système, comme « No such file or directory ».

bash: warning: setlocale: LC_ALL: cannot change locale (fr_FR.UTF-8)
bash: warning: setlocale: LC_ALL: cannot change locale (fr_FR.UTF-8): No such file or directory

La variable nommée dans le message dépend de l’endroit qui porte la valeur. Quand LC_ALL porte le nom de la locale, l’avertissement vise LC_ALL ; quand c’est LANG, il vise la première catégorie appliquée, LC_CTYPE le plus souvent, avec la valeur de LANG entre parenthèses. La cause ne change pas : aucune locale compilée ne porte ce nom et setlocale() rend NULL. Générer la locale règle l’avertissement à la connexion suivante ; sinon, il faut corriger le fichier qui pose la variable, puis rouvrir la session.

La locale dit comment lire les octets, pas comment le fichier est enregistré

Une locale fr_FR.UTF-8 ne convertit aucun fichier : elle indique aux programmes comment interpréter les octets qu’ils lisent, et un script enregistré en ISO-8859-1 garde son octet E9 à la place du é. La page file(1) du paquet 1:5.44-3 pour Debian 12 explique que l’outil distingue les textes ASCII, ISO-8859-x et UTF-8 et annonce le jeu de caractères reconnu. La conversion des octets relève d’iconv(1), avec l’encodage d’entrée en option -f et l’encodage de sortie en option -t.

file mon_script.sh
iconv -f ISO-8859-1 -t UTF-8 mon_script.sh > mon_script.utf8.sh

La réponse de file doit contenir Unicode et UTF-8 pour un script utilisable tel quel. Une conversion réussie laisse intact tout ce qui était déjà en ASCII, puisque ce jeu de caractères est un sous-ensemble de l’UTF-8. La question se règle en amont, à l’écriture d’un script shell, avec un éditeur réglé sur l’UTF-8.

LC_ALL passe avant tout, LANG sert de valeur par défaut

La page locale(7) énumère l’ordre que setlocale() applique quand aucun nom ne lui est fourni : LC_ALL d’abord, puis la variable de la catégorie concernée, puis LANG. Le code de bash reprend cet ordre dans locale.c, avec un commentaire qui le rattache à POSIX.2, et la fonction get_locale_var() y cherche la valeur dans cette séquence avant de se rabattre sur la chaîne vide. Une variable de catégorie posée à vide est ignorée, et LC_ALL posé à vide rend la main aux autres.

Les quatre variables de locale qu’un script rencontre, d’après locale(7) et la source de bash.
Variable Ce qu’elle gouverne Rang d’application
LC_ALL toutes les catégories à la fois 1, avant toutes les autres
LC_CTYPE interprétation des octets, classifications et classes de caractères 2
LC_COLLATE règles de tri et d’expressions régulières 2
LC_TIME formats de date et d’heure 2
LANG les catégories qu’aucune variable LC_ ne fixe 3, valeur par défaut

Le tri est la catégorie où l’écart se remarque le plus vite. Le wiki Arch donne l’exemple de LC_COLLATE=C.UTF-8, qui fait remonter en tête de ls les fichiers dont le nom commence par un point : le même répertoire ne se lit pas dans le même ordre selon cette seule variable. Un script qui appelle sort ou ls dépend de la même règle dans ces programmes, et comprendre bin/bash aide à distinguer ce qui vient du shell et ce qui vient d’eux.

Générer la locale ne tranche ni le choix du jeu ni la portée de LC_ALL

Faut-il générer C.UTF-8, fr_FR.UTF-8, ou les deux ?

La bibliothèque glibc a reçu une locale C.UTF-8 dans sa version 2.35, et sa note de version précise qu’elle « is not built into glibc, and must be installed » : elle se génère comme les autres. Une locale C.UTF-8 suffit à un script qui affiche et compare des caractères UTF-8. Les formats français de date et de tri viennent de fr_FR.UTF-8, dont le nom porte le code territoire FR que le wiki Arch décrit comme un code ISO 3166. Ajoutez les deux lignes dans /etc/locale.gen.

Un script qui pose LC_ALL=C perd-il les accents ?

LC_ALL l’emporte sur LANG et sur toutes les variables de catégorie, comme l’énumère locale(7), donc un script qui l’exporte garde ce réglage même sur une machine configurée en fr_FR.UTF-8. La conséquence la plus visible touche le tri, puisque LC_COLLATE repasse sur les règles de la locale C. Pour figer une seule commande, mieux vaut la préfixer, LC_ALL=C sort -u, et laisser le reste du script sous la locale française.

Que change l’option –keep-existing de locale-gen ?

Sans option, locale-gen supprime et reconstruit /usr/lib/locale/locale-archive, l’archive qui contient les locales compilées. La page locale-gen(8) décrit –keep-existing comme l’inverse : « do not remove /usr/lib/locale/locale-archive, only compiling new locales ». Sur une machine où beaucoup de locales sont déjà générées, l’option évite de les recompiler toutes.

Comment vérifier qu’un fichier de script est bien en UTF-8 ?

La commande file nomme le jeu de caractères quand le fichier est un texte, une distinction que file(1) documente pour l’ASCII, l’ISO-8859-x et l’UTF-8. Si la réponse ne contient pas UTF-8, la commande iconv -f ISO-8859-1 -t UTF-8 mon_script.sh > mon_script.utf8.sh en produit une copie convertie. Sans -f ni -t, iconv(1) tire les deux encodages de la locale courante.

Pourquoi fr_FR.UTF-8 et fr_FR.utf8 désignent-ils la même locale ?

La page locale(7) donne l’ordre de recherche des données compilées pour en_GB.UTF-8 : en_GB.UTF-8, puis en_GB.utf8, puis en_GB, puis en.UTF-8, en.utf8 et en. L’écriture du jeu de caractères dans le nom n’a donc pas besoin de correspondre à celle du fichier compilé. C’est pourquoi locale -a peut afficher fr_FR.utf8 quand /etc/locale.gen contient fr_FR.UTF-8.

Sources