Pourquoi printf "rétrécit-il" le tréma?

54

Si j'exécute le script simple suivant:

#!/bin/bash
printf "%-20s %s\n" "Früchte und Gemüse"   "foo"
printf "%-20s %s\n" "Milchprodukte"        "bar"
printf "%-20s %s\n" "12345678901234567890" "baz"

Il imprime:

Früchte und Gemüse foo
Milchprodukte        bar
12345678901234567890 baz

c'est-à-dire que le texte avec des trémas (tels que ü) est "réduit" d'un caractère par tréma.

Certes, je me suis trompé de réglage quelque part, mais je ne suis pas en mesure de déterminer lequel pourrait être.

Cela se produit si le codage du fichier est UTF-8.

Si je modifie son codage en latin-1, l'alignement est correct, mais les trémas sont erronés:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz
René Nyffenegger
la source
14
Vous vous attendez à ce que printf connaisse UTF-8 et d'autres jeux de caractères multi-octets?
Frostschutz
16
On dirait que c'est compter les octets plutôt que les caractères; voir echo Früchte und Gemüse | wc -c -mpour la différence.
Stephen Kitt
7
@frostschutz Zsh's printfest.
Stephen Kitt
10
Oui, je m'attends à ce que printf connaisse (au moins) UTF-8.
René Nyffenegger
12
Eh bien non. Mauvais chance. ;-)
frostschutz

Réponses:

87

Posix exige printf « s %-20scompter les 20 en termes d' octets non des caractères , même si cela fait peu de sens comme printfest d'imprimer du texte , au format (voir la discussion au sein du groupe Austin (POSIX) et les bashlistes de diffusion).

La printfconstruction de bashet la plupart des autres obus POSIX rendent hommage à cela.

zshignore cette exigence stupide (même en shémulation) et printffonctionne donc comme prévu. Idem pour les fonctions printfintégrées de fish(pas un shell semblable à POSIX).

Le ücaractère (U + 00FC), lorsqu'il est codé en UTF-8, est composé de deux octets (0xc3 et 0xbc), ce qui explique la différence.

$ printf %s 'Früchte und Gemüse' | wc -mcL
    18      20      18

Cette chaîne est composée de 18 caractères et a une largeur de 18 colonnes ( -Létant une wcextension GNU pour indiquer la largeur d'affichage de la ligne la plus large de l'entrée), mais elle est codée sur 20 octets.

Dans zshou fish, le texte serait aligné correctement.

Maintenant, il y a aussi des caractères qui ont une largeur nulle (comme des caractères combinés tels que U + 0308, la diarésie combinante) ou qui ont une double largeur, comme dans de nombreux scripts asiatiques (sans parler des caractères de contrôle comme Tab) et zshqui ne s'alignaient même pas. ceux correctement.

Exemple, dans zsh:

$ printf '%3s|\n' u ü $'u\u308' $'\u1100'
  u|
  ü|
 ü|
  ᄀ|

Dans bash:

$ printf '%3s|\n' u ü $'u\u308' $'\u1100'
  u|
 ü|
ü|
ᄀ|

ksh93a une %Lsspécification de format pour compter la largeur en termes de largeur d' affichage .

$ printf '%3Ls|\n' u ü $'u\u308' $'\u1100'
  u|
  ü|
  ü|
 ᄀ|

Cela ne fonctionne toujours pas si le texte contient des caractères de contrôle tels que TAB (comment pourrait-il? Il printffaudrait savoir à quelle distance se trouvent les taquets de tabulation dans le périphérique de sortie et à quelle position il commence à imprimer). Cela fonctionne par accident avec les caractères de retour arrière (comme dans la roffsortie où X(gras X) est écrit X\bX), bien que ksh93tous les caractères de contrôle aient une largeur de -1.

Comme autres options, vous pouvez essayer:

printf '%s\t|\n' u ü $'u\u308' $'\u1100' | expand -t3

Cela fonctionne avec certaines expandimplémentations (pas avec GNU cependant).

Sur les systèmes GNU, vous pouvez utiliser GNU awkdont le printfnombre de caractères (pas d'octets, ni de largeur d'affichage, donc toujours pas OK pour les caractères de largeur 0 ou 2, mais OK pour votre exemple):

gawk 'BEGIN {for (i = 1; i < ARGC; i++) printf "%-3s|\n", ARGV[i]}
     ' u ü $'u\u308' $'\u1100'

Si la sortie est dirigée vers un terminal, vous pouvez également utiliser des séquences d'échappement de positionnement du curseur. Comme:

forward21=$(tput cuf 21)
printf '%s\r%s%s\n' \
  "Früchte und Gemüse"    "$forward21" "foo" \
  "Milchprodukte"         "$forward21" "bar" \
  "12345678901234567890"  "$forward21" "baz"
Stéphane Chazelas
la source
2
C'est inexact. Le ücaractère peut être composé de u+ ¨, ce qui correspond à 3 octets. Dans le cas de la question, il est codé en 2 caractères, mais tous ne üsont pas créés de manière égale.
Ismael Miguel
6
@IsmaelMiguel, u\u308est deux caractères ( wc -mau moins dans Unix / sens) pour un glyphe / graphem / graphem-cluster et est déjà mentionné et inclus dans cette réponse.
Stéphane Chazelas
"cela n'a pas beaucoup de sens car printf est d'imprimer du texte" Eh bien, on pourrait argumenter que printf traite les caractères C (octets); il ne devrait pas traiter les paramètres régionaux du texte et ne devrait pas avoir le fardeau de comprendre le codage (éventuellement plusieurs octets) du jeu de caractères. Mais cette ligne de défense est en contradiction avec les exigences (ISO C99) selon lesquelles la troncature d'octet "% s" ne doit pas générer de texte "non valide" (caractères tronqués). Glibc échoue même dans ce cas (il n’imprime rien). Un vrai bordel. postgresql.org/message-id/…
leonbloy
@leonbloy, cela pourrait donner un sens à C printf(3)(peu de sens après l'exigence C99 que vous mentionnez, merci pour cela), mais pas à l' printf(1)utilitaire car chaque opérateur de shell ou autre utilitaire texte traite des caractères (ou a été modifié pour traiter également des caractères comme wcqui a un -m(en -crestant octet ) ou cutqui a un -baprès -cpeut signifier autre chose que des octets).
Stéphane Chazelas
Même s'il utilisait des caractères plutôt que des octets, cela ne conviendrait toujours pas pour l'alignement des colonnes. Vous devez savoir combien de cellules terminales chaque personnage occupe, ce qui varie selon les caractères (0-2).
R ..
10

Si je modifie son codage en latin-1, l'alignement est correct, mais les trémas sont erronés:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz

En fait, non, mais votre terminal ne parle pas latin-1, et vous obtenez donc de la malbouffe plutôt que des umlauts.

Vous pouvez résoudre ce problème en utilisant iconv:

printf foo bar | iconv -f ISO8859-1 -t UTF-8

(ou exécutez tout le script shell dans iconv)

Wouter Verhelst
la source
3
Ceci est un commentaire utile mais ne répond pas à la question centrale.
gerrit
1
@ gerrit comment alors? Si printf fait ce qu'il faut pour imprimer en latin1, faites-le imprimer en latin1 et convertissez-le au format UTF-8 ultérieurement. Cela me semble être une solution adéquate à la question centrale.
Wouter Verhelst
1
La question centrale est "Pourquoi rétrécit-il le tréma", la réponse (comme dans les autres réponses) est "car il ne prend pas en charge utf-8". Il ne demande pas pourquoi les trémas sont mal rendus ou comment puis-je corriger le rendu du tréma . Dans les deux cas, votre suggestion est utile pour le sous-ensemble de utf-8 pouvant être représenté par iso8859-1 (uniquement).
gerrit
4
@WouterVerhelst, oui, cela ne peut s'appliquer qu'au texte pouvant être encodé dans un jeu de caractères à un octet.
Stéphane Chazelas
3
Moi aussi, j'ai lu la question comme suit: "comment puis-je obtenir une sortie correcte" plutôt que "je ne m'occupe pas de la sortie défectueuse, du moment que je sais pourquoi".
M. Lister