Warum schrumpft printf den Umlaut?

54

Wenn ich das folgende einfache Skript ausführe:

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

Es druckt:

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

Das heißt, Text mit Umlauten (z. B. ü) wird um ein Zeichen pro Umlaut "verkleinert".

Sicher habe ich irgendwo eine falsche Einstellung, aber ich kann nicht herausfinden, welche das sein könnte.

Dies tritt auf, wenn die Codierung der Datei UTF-8 ist.

Wenn ich die Codierung in Latin-1 ändere, ist die Ausrichtung korrekt, aber die Umlaute werden falsch gerendert:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz
René Nyffenegger
quelle
14
Sie erwarten, dass printf UTF-8 und andere Multibyte-Zeichensätze kennt?
Frostschutz
16
Sieht so aus, als würde es eher Bytes als Zeichen zählen. sehen echo Früchte und Gemüse | wc -c -mden Unterschied.
Stephen Kitt
7
@frostschutz Zsh's printfist.
Stephen Kitt
10
Ja, ich erwarte, dass printf (zumindest) UTF-8 kennt.
René Nyffenegger
12
Nun, das ist es nicht. Pech. ;-)
Frostschutz

Antworten:

87

POSIX verlangt printf , dass %-20sdiese 20 in Bytes und nicht in Zeichen gezählt werden , obwohl dies wenig sinnvoll printfist, da Text formatiert gedruckt wird (siehe Diskussion bei der Austin Group (POSIX) und bashMailinglisten).

Die printfeingebauten bashund die meisten anderen POSIX-Shells berücksichtigen dies .

zshIgnoriert diese dumme Anforderung (auch in der shEmulation), printffunktioniert also so, wie Sie es dort erwarten würden. Gleiches gilt für das printfBuiltin von fish(keine POSIX-ähnliche Shell).

Das üin UTF-8 codierte Zeichen (U + 00FC) besteht aus zwei Bytes (0xc3 und 0xbc), was die Diskrepanz erklärt.

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

Diese Zeichenfolge besteht aus 18 Zeichen, ist 18 Spalten breit ( -Leine GNU- wcErweiterung, die die Anzeigebreite der breitesten Zeile in der Eingabe angibt), ist jedoch in 20 Bytes codiert.

In zshoder fishwürde der Text korrekt ausgerichtet.

Es gibt jetzt auch Zeichen mit der Breite 0 (wie das Kombinieren von Zeichen wie U + 0308, das Kombinieren von Diaresis) oder mit der doppelten Breite wie in vielen asiatischen Skripten (ganz zu schweigen von Steuerzeichen wie Tabulator) und die sogar zshnicht ausgerichtet werden würden die richtig.

Beispiel in zsh:

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

In bash:

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

ksh93hat eine %LsFormatspezifikation zum Zählen der Breite in Bezug auf die Anzeigebreite .

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

Das funktioniert immer noch nicht, wenn der Text Steuerzeichen wie TAB enthält (wie könnte es sein, printfmüsste wissen, wie weit die Tabstopps im Ausgabegerät voneinander entfernt sind und an welcher Position der Druck beginnt). Es funktioniert aus Versehen mit Backspace-Zeichen (wie in der roffAusgabe, in der X(fett X) geschrieben ist als X\bX), obwohl ksh93alle Steuerzeichen eine Breite von haben -1.

Als weitere Optionen könnten Sie versuchen:

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

Das funktioniert bei einigen expandImplementierungen (allerdings nicht bei GNUs).

Auf GNU-Systemen könnten Sie GNU verwenden, awkdessen printfAnzahl in Zeichen angegeben ist (keine Bytes, keine Anzeigebreiten, also immer noch nicht OK für die Zeichen mit 0 oder 2 Breiten, aber OK für Ihr Beispiel):

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

Wenn die Ausgabe an ein Terminal gesendet wird, können Sie auch Escape-Sequenzen für die Cursorpositionierung verwenden. Mögen:

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
quelle
2
Das ist falsch. Die ücaracter kann als zusammengesetzt sein , u+ ¨, das 3 Byte. Im Fall der Frage wird sie als 2 Zeichen codiert, aber nicht alle üwerden gleich erstellt.
Ismael Miguel
6
@IsmaelMiguel, u\u308besteht aus zwei Zeichen ( wc -mmindestens in Unix / sense) für eine Glyphe / Graphem / Graphem-Cluster und ist bereits erwähnt und in dieser Antwort enthalten.
Stéphane Chazelas
"Das macht wenig Sinn, da printf das Drucken von Text ist" Nun, man könnte argumentieren, dass printf C-Zeichen (Bytes) behandelt; Es sollte sich nicht um Textländereinstellungen handeln und es sollte nicht die Last haben, die (möglicherweise Multibyte-) Zeichensatzkodierung zu verstehen. Diese Verteidigungslinie steht jedoch im Widerspruch zu den Anforderungen (ISO C99), wonach das Abschneiden von "% s" -Bytes nicht zu "ungültigen" Texten (abgeschnittenen Zeichen) führen darf. Glibc schlägt in diesem Fall sogar fehl (es wird nichts gedruckt). Ein echtes Durcheinander. postgresql.org/message-id/…
leonbloy
@leonbloy, das könnte Sinn für Cs machen printf(3)(wenig Sinn nach dieser C99-Anforderung, die Sie erwähnen, danke dafür), aber nicht das printf(1)Dienstprogramm, da jeder Shell-Operator oder anderes Textdienstprogramm sich mit Zeichen befasst (oder geändert wurde, um sich auch mit Zeichen zu befassen) wie wcdas bekam ein -m(während Byte-c blieb ) oder das bekam ein nach könnte etwas anderes als Bytes bedeuten). cut-b-c
Stéphane Chazelas
Selbst wenn Zeichen anstelle von Bytes verwendet würden, wäre dies nicht zum Ausrichten von Spalten geeignet. Sie müssen wissen, wie viele Terminalzellen jedes Zeichen belegt, je nach Zeichen (0-2).
R ..
10

Wenn ich die Codierung in Latin-1 ändere, ist die Ausrichtung korrekt, aber die Umlaute werden falsch gerendert:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz

Eigentlich nein, aber Ihr Terminal spricht kein lateinisch-1, und Sie erhalten daher eher Junk als Umlaute.

Sie können dies beheben, indem Sie iconv verwenden:

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

(Oder führen Sie einfach das gesamte Shell-Skript aus, das in iconv eingebunden ist.)

Wouter Verhelst
quelle
3
Dies ist ein nützlicher Kommentar, der jedoch die Kernfrage nicht beantwortet.
Gerrit
1
@gerrit wie so? Wenn printf beim Drucken in latin1 das Richtige tut, muss es dann in latin1 gedruckt und später in UTF-8 konvertiert werden? Scheint mir eine richtige Lösung für die Kernfrage zu sein.
Wouter Verhelst
1
Die Kernfrage lautet "Warum schrumpft der Umlaut?", Die Antwort lautet (wie in anderen Antworten) "weil utf-8 nicht unterstützt wird". Es wird nicht gefragt, warum die Umlaute falsch gerendert werden oder wie ich das Umlaut-Rendering korrigieren kann . In beiden Fällen ist Ihr Vorschlag für die Teilmenge von utf-8 nützlich, die (nur) als iso8859-1 dargestellt werden kann.
Gerrit
4
@WouterVerhelst, aber das gilt nur für Text, der in einem Einzelbyte-Zeichensatz codiert werden kann.
Stéphane Chazelas
3
Ich habe auch die Frage gelesen: "Wie kann ich die Ausgabe richtig machen?" Und nicht "Mir macht die fehlerhafte Ausgabe nichts aus, solange ich weiß warum".
Mr Lister