La commande tr (et ses limites)
Sommaire
Introduction
La commande tr
peut :
- remplacer un caractère par un autre ;
- supprimer des caractères ;
- condenser des suites de caractères.
Bien que d’un maniement simple, elle tend toutefois des pièges à son utilisateur : notamment parce que la version installée sur les systèmes GNU/Linux (celle des GNU Core Utilities) ne gère pas les caractères codés sur plusieurs octets – ce qui est par exemple le cas caractères accentués.
Remplacer un caractère par un autre
La commande prend deux listes de caractères comme arguments
Dans les commandes de la forme tr liste1 liste2
, tr
remplace toutes les occurrences de l’énième caractère de liste1
par l’énième caractère de liste2
.
Voici quelques exemples, en commençant par le cas le plus simple, celui dans lequel liste1
et liste2
se composent d’un seul caractère :
$ printf 'abc123\n' | tr 'a' 'd'
dbc123
On étoffe les listes :
$ printf 'abc123\n' | tr 'abc' 'def'
def123
Le tiret indique un intervalle de lettres ou de chiffres :
$ printf 'abc123\n' | tr a-z A-Z
ABC123
$ printf 'abc123\n' | tr 'a-c1-3' 'd-fx-z'
defxyz
On peut aussi utiliser les classes de caractères POSIX :
$ printf 'abc123\n' | tr '[:lower:]' '[:upper:]'
ABC123
$ printf 'abc123\n' | tr '[:lower:]1-9' '[:upper:]a-i'
ABCabc
Au passage, tr
peut être utilisé pour décoder le chiffre de César – ici avec une distance de 3 :
$ printf 'pxirq\n' | tr 'a-z' 'd-za-c'
salut
Cas de listes de caractères de taille différente
Un comportement par défaut sur lequel on ne saurait compter
Si liste1
contient moins d’éléments que liste2
, alors tous les éléments de liste1
dont le rang est supérieur au nombre d’éléments de liste2
sont remplacés par le dernier élément de liste2
:
$ printf 'abcde\n' | tr a-z 1-2
12222
Ce comportement, vérifié avec les commandes tr
de GNU, de macOS et d’OpenBSD, n’est toutefois pas requis par la norme POSIX – on ne saurait donc le tenir pour garanti.
L’expression [x*n] définie par POSIX
Il est toutefois possible d’obtenir ce même résultat en utilisant l’expression [x*n]
définie par POSIX, dans laquelle x
est le caractère à répéter, et n
le nombre de répétitions :
$ printf 'abcde\n' | tr a-z 1[2*4]
12222
$ printf 'abcde\n' | tr a-z 1[2*3]z
1222z
Si n
n’est pas spécifié (forme [x*]
), alors tr
ajoute autant d’occurrences du caractère de remplacement qu’il faut pour que la deuxième liste fasse la taille de la première :
$ printf 'abcde\n' | tr a-z 1[2*]
12222
La forme [x*n]
peut-être utilisée ailleurs qu’en fin de liste :
$ printf 'abcde\n' | tr a-z 1[2*2]z
122zz
$ printf 'abcde\n' | tr a-z 1[2*2][a*1]z
122az
$ printf 'abcde\n' | tr a-z 1[2*2][a*2]z
122aa
$ printf 'abcde\n' | tr a-z '[2*3]z'
222zz
Mais attention, si l’on utilise une expression de la forme [x*]
, les caractères qui la suivent ne sont pas pris en compte (testé sur GNU/Linux et OpenBSD) :
$ printf 'abcde\n' | tr a-z 1[2*]z
12222
L’option -t de la version GNU de tr
L’implémentation GNU de tr
propose une option -t
(ou --truncate-set-1
) qui permet, si la deuxième liste de caractères est plus petite que la première, de tronquer la première liste afin que les deux soient de longeur égale :
$ printf 'abcde\n' | tr -t a-z 1-2
12cde
On peut utiliser le complément d’une liste
L’option -c
sélectionne l’ensemble complémentaire de celui de la première liste passée en paramètre à tr
(c’est-à-dire que le programme opérera sur tous les caractères qui ne sont pas indiqués dans liste1
) :
$ printf 'abc123\n' | tr -c '\n' '.'
......
$ printf 'Quoi? Non!\n' | tr -c '[:alnum:]\n' '-'
Quoi--Non-
Il est utile d’ajouter le saut de ligne (indiqué par la séquence d’échappement \n
) dans liste1
, sans quoi toutes les lignes de l’entrée n’en forment plus qu’une sur la sortie. Comparer :
$ printf 'abc\ndef\n123\n' | tr -c '[:alpha:]\n' '.'
abc
def
...
$ printf 'abc\ndef\n123\n' | tr -c '[:alpha:]' '.'
abc.def.....$
(Le $
à la fin de la dernière ligne de l’exemple est l’invite de commande qui, faute de saut de ligne à la fin de la sortie de commande, réapparaît sur la même ligne que celle-ci.)
Supprimer des caractères
Dans la forme tr -d liste
, l’option -d
supprime tous les caractères lus qui figurent dans liste
:
$ printf 'abc123\n' | tr -d 'abc'
123
$ printf 'abc123\n' | tr -d '[:alpha:]'
123
$ printf 'abc123\n' | tr -d '1-3'
abc
$ printf 'abc123\n' | tr -d '[:digit:]bc'
a
On peut combiner cette option avec -c
, mais il est préférable, là encore d’ajouter le caractère de fin de ligne :
$ printf 'abc123\n' | tr -dc '[:alpha:]\n'
abc
Condenser des suites de caractères
Condenser seulement
Dans sa forme tr -s liste
, la commande remplace toute suite de chacun des caractères de liste
par une seule occurrence dudit caractère :
$ printf "aaabbbccc\n" | tr -s 'a'
abbbccc
$ printf "aaabbbccc\n" | tr -s 'abc'
abc
On peut s’en servir pour supprimer des lignes vides :
$ printf "abc\n\n123\n"
abc
123
$ printf "abc\n\n123\n" | tr -s '\n'
abc
123
Remplacer puis condenser
Dans sa forme tr -s liste caractère
, tr
effectue deux actions successives :
- remplacer : il remplace d’abord par
caractère
tous les caractères contenus dansliste
; - condenser : puis il remplace chaque séquence de
caractère
par une seule occurrence dudit caractère.
$ printf "abc123\n" | tr 'abc' '-'
---123
$ printf "abc123\n" | tr 'abc' '-' | tr -s '-'
-123
$ printf "abc123\n" | tr -s 'abc' '-'
-123
En somme, c’est une forme raccourcie pour un enchaînement de commandes telles que : tr liste caractère | tr -s caractère
.
J’ajoute deux exemples, qui montrent bien que tr
effectue le remplacement des caractères avant de condenser, et que la condensation s’opère sur toute la chaîne, non pas sur les parties qui ont fait l’objet du remplacement :
$ printf "abc123\n" | tr -s 'a-c2-3' 'z'
z1z
$ printf "abc123\n" | tr -s 'a-c2-3' '1'
1
Dans la dernière ligne, on obtient 1
et non pas 111
.
Points épineux
Attention aux crochets
J’ai souvent écrit – par erreur – des lignes contenant des intervalles ou des listes de caractères indiqués entre crochets.
$ printf 'abc\n' | tr '[a-z]' '[A-Z]'
ABC
Or les crochets sont inutiles :
$ printf 'abc\n' | tr 'a-z' 'A-Z'
ABC
Ma façon d’écrire la commande aboutit seulement à remplacer un crochet ouvrant (premier élément de la liste [a-z]
) par un crochet ouvrant (premier élément de la liste [A-Z]
) – et pareil pour le crochet fermant, dernier élément des listes.
Je subissais probablement l’influence de la façon dont on indique listes de caractères ou intervalles dans les expansions de nom de fichiers du shell (par exemple : echo t[a-z0-9]*
) ou dans les expressions régulières (par exemple : grep '[a-z0-9]'
).
L’erreur est sans conséquence dans le cas d’un remplacement de caractères, le résultat étant celui attendu. Mais si l’on fait usage de l’option ‑d
, alors on supprime des crochets en voulant supprimer des lettres. Comparer :
$ printf 'abc[…].\n' | tr -d '[a-z]'
….
$ printf 'abc[…].\n' | tr -d 'a-z'
[…].
Caractères codés sur plusieurs octets : les limites de tr
Un codage de caractères informatique est une mise en correspondance de caractères d’une part, et d’octets d’autre part.
Mon shell (mais c’est aujourd’hui la norme) utilise le codage UTF-8, dans lequel les caractères sont codés sur un nombre variable d’octets. Par exemple :
- le
a
minuscule est codé sur l’octet01100001
; - le
é
minuscule sur la suite d’octets11000011
,10101001
; - l’émoji ☕ sur les trois octets
11100010
,10011000
,10010101
; - et l’émoji 😂 sur les quatre octets
11110000
,10011111
,10011000
,10000010
.
En UTF-8, les caractères codés sur un seul octet sont ceux du répertoire du codage ASCII : chiffres, lettres de l’alphabet latin sans accent, un nombre réduit de symboles et de signes de ponctuation.
Comme on va le voir, la version GNU de tr
ne gère pas les caractères codés sur plusieurs octets – il faut donc renoncer à s’en servir dans bon nombre de cas. Je ne me risquerais pas à expliquer à quoi cela tient. Le lecteur curieux d’en savoir plus peut consulter la page « GNU Coreutils - Multibyte/unicode support » sur le site crashcourse.housegordon.org
. Assaf Gordon, son auteur, a travaillé voici quelques années sur une possible prise en charge des caractères à plusieurs octets par la commande tr
de GNU ; si cela n’a pas abouti, il a toutefois mis en ligne sa documentation sur le sujet.
Voici des exemples tirés de l’utilisation de la version de tr
fournie par GNU, avec des parallèles avec celle de macOS, qui fonctionne bien.
Commençons par des remplacements simples de caractères accentués. Sur GNU/Linux, tr
échoue :
$ echo 'été roi' | tr 'ét' 'ub'
ubbub roi
Sur macOS, c’est bon :
$ echo 'été roi' | tr 'ét' 'ub'
ubu roi
Les lettres accentuées et les ligatures ne figurent pas dans l’intervalle a-z
, ni avec GNU tr
ni sur macOS. C’est, me semble-t-il, le comportement attendu en UTF-8 : l’intervalle a-z
(points de code U+0061
à U+007A
) ne contient que les 26 lettres de l’alphabet.
$ echo 'aéœ' | tr -d a-z
éœ
$ printf 'À l’été\n' | tr a-z A-Z
À L’éTé
Mais dans la version GNU de tr
, diacritiques et ligatures ne figurent pas non plus dans dans les classes de caractères POSIX telles que [:alpha:]
, [:lower:]
ou [:print:]
:
$ echo 'aéœ' | tr -d [:alpha:]
éœ
$ echo 'aéœ' | tr -d [:print:]
éœ
$ printf 'À l’été\n' | tr [:lower:] [:upper:]
À L’éTé
Sur macOS, les classes POSIX donnent le résultat attendu :
$ echo 'aéœ' | tr -d [:alpha:]
$ printf 'À l’été\n' | tr [:lower:] [:upper:]
À L’ÉTÉ
Si je veux remplacer un é
ou l’apostrophe typographique par un tiret, j’obtiens deux et trois tirets avec la version GNU de tr
– comme si la commande comptait comme un caractère distinct chacun des deux octets qui composent le é
ou des trois octets qui composent l’apostrophe :
$ echo é | tr é -
--
$ echo ’ | tr ’ -
---
$ echo '😂' | tr '😂' 'a'
aaaa
$ echo '😂' | tr '😂' 'abcd'
abcd
Sur macOS, aucun problème :
$ echo ’ | tr ’ -
-
$ echo é | tr é -
-
$ echo '😂' | tr '😂' 'a'
a
$ echo '😂' | tr '😂' 'abcd'
a
Un autre exemple, avec GNU d’abord :
$ for i in z é ☕ 😂 ; do echo "$i abc" | tr "$i" '☕' ; done
abc
abc
☕ abc
☕ abc
On attendrait quatre fois la ligne ☕ abc
, mais ce n’est pas le cas. Sur macOS, c’est bon :
$ for i in z é ☕ 😂 ; do echo "$i abc" | tr "$i" '☕' ; done
☕ abc
☕ abc
☕ abc
☕ abc
Conclusion consolatoire adressée aux utilisateurs de tr sur un système GNU/Linux
Si tr
ne peut donc pas faire grand-chose pour les utilisateurs de systèmes GNU/Linux, ces derniers peuvent toutefois se tourner vers sed
, et notamment vers les commandes s
et y
. Car le sed
de GNU gère bien les caractères à plusieurs octets – mais cela implique de se mettre aux expressions régulières (ce qui est de toute façon un des meilleurs services qu’on puisse se rendre).
Quelques exemples sans explications :
$ printf 'été roi\n' | sed 'y/ét/ub/'
ubu roi
$ printf 'À l’été\n' | sed 's/[[:lower:]]//g'
À ’
$ printf 'À l’été\n' | sed 's/.*/\U&/g'
À L’ÉTÉ
$ printf 'À l’été\n' | sed 's/.*/\U&/g' | sed 's/.*/\L&/g'
à l’été
(Attention, les formes \U&/
et \L&/
sont propres au sed
de GNU.)
Et enfin :
$ for i in z é ☕ 😂 ; do echo "$i abc" | sed "y/$i/☕/" ; done
☕ abc
☕ abc
☕ abc
☕ abc