Dlaczego printf „kurczy się”?

54

Jeśli wykonam następujący prosty skrypt:

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

Drukuje:

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

to znaczy tekst z umlautami (np. ü) jest „zmniejszany” o jeden znak na umlaut.

Oczywiście, mam gdzieś jakieś złe ustawienie, ale nie jestem w stanie ustalić, który to może być.

Dzieje się tak, jeśli kodowanie pliku to UTF-8.

Jeśli zmienię kodowanie na latin-1, wyrównanie jest prawidłowe, ale umlauty są renderowane nieprawidłowo:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz
René Nyffenegger
źródło
14
Oczekujesz, że printf będzie świadomy UTF-8 i innych wielobajtowych zestawów znaków?
frostschutz
16
Wygląda na to, że liczy więcej bajtów niż znaków; zobacz echo Früchte und Gemüse | wc -c -mróżnicę.
Stephen Kitt,
7
@frostschutz Zsh's printfis.
Stephen Kitt
10
Tak, spodziewam się, że printf będzie świadomy (przynajmniej) UTF-8.
René Nyffenegger,
12
Cóż, nie jest. Pech. ;-)
frostschutz

Odpowiedzi:

87

POSIX wymaga printf , %-20saby policzyć te 20 w kategoriach bajtów, a nie znaków, chociaż nie ma to większego sensu, jak printfdrukowanie tekstu , formatowanie (patrz dyskusja w Austin Group (POSIX) i bashlisty mailingowe).

Uwzględniają to printfwbudowane bashi większość innych powłok POSIX.

zshignoruje to głupie wymaganie (nawet w shemulacji), więc printfdziała tak, jak można się tam spodziewać. To samo dotyczy printfwbudowanej fish(nie powłoki podobnej do POSIX).

üZnak (U + 00FC), w którym zakodowane UTF-8 składa się z dwóch bajtów (0xc3 i 0xbc), co wyjaśnia różnicę.

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

Łańcuch ten składa się z 18 znaków, ma 18 kolumn szerokości ( -Ljest wcrozszerzeniem GNU raportującym szerokość wyświetlania najszerszej linii na wejściu), ale jest zakodowany na 20 bajtach.

W zshlub fishtekst zostałby wyrównany poprawnie.

Teraz są też znaki, które mają szerokość 0 (jak łączenie znaków, takich jak U + 0308, łączenie diurezy) lub mają podwójną szerokość, jak w wielu skryptach azjatyckich (nie wspominając o znakach kontrolnych, takich jak Tab), a nawet zshnie wyrównywałyby te poprawnie.

Przykład w zsh:

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

W bash:

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

ksh93ma %Lsspecyfikację formatu, aby policzyć szerokość pod względem szerokości wyświetlania .

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

To nadal nie działa, jeśli tekst zawiera znaki sterujące, takie jak TAB (jak to możliwe? printfMusiałby wiedzieć, jak daleko od siebie są tabulatory w urządzeniu wyjściowym i w jakiej pozycji zaczyna drukować). Działa przypadkowo ze znakami backspace (jak na roffwyjściu, gdzie zapisano X(pogrubienie X) jako X\bX), chociaż ksh93uważa, że ​​wszystkie znaki sterujące mają szerokość -1.

Jako inne opcje możesz spróbować:

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

Działa to z niektórymi expandimplementacjami (choć nie GNU).

W systemach GNU możesz użyć GNU, awkktórego printfliczenie jest w znakach (nie bajtach, nie szerokościach wyświetlania, więc nadal nie jest OK dla znaków o szerokości 0 lub 2 szerokości, ale OK dla twojej próbki):

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

Jeśli dane wyjściowe trafiają do terminala, możesz także użyć sekwencji ucieczki pozycjonowania kursora. Lubić:

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
źródło
2
To jest niepoprawne Znak ümoże być złożony jako u+ ¨, czyli 3 bajty. W przypadku pytania jest zakodowane jako 2 znaki, ale nie wszystkie üsą tworzone jednakowo.
Ismael Miguel
6
@ IsmaelMiguel, u\u308to dwa znaki ( wc -mprzynajmniej w Uniksie / sensie) dla jednego glifu / graphem / graphem-klastra i jest już wspomniany i zawarty w tej odpowiedzi.
Stéphane Chazelas,
„to nie ma sensu, ponieważ printf polega na drukowaniu tekstu” Można argumentować, że printf zajmuje się znakami C (bajtami); nie powinien zajmować się ustawieniami tekstu i nie powinien być obciążony zrozumieniem kodowania zestawu znaków (być może wielobajtowego). Ale ta linia obrony jest sprzeczna z wymaganiami (ISO C99), że obcięcie bajtu „% s” nie powinno skutkować „nieprawidłowymi” tekstami (obciętymi znakami). Glibc nawet zawodzi w tym przypadku (nic nie drukuje). Prawdziwy bałagan. postgresql.org/message-id/…
leonbloy
@leonbloy, może to mieć sens dla C printf(3)(nie ma sensu po tym wymaganiu C99, o którym wspominasz, dziękuję za to), ale nie printf(1)narzędzie, ponieważ każdy operator powłoki lub inne narzędzie tekstowe radzi sobie ze znakami (lub zostały zmodyfikowane, aby również traktować znaki np. wcktóry dostał bajt-m (podczas gdy -czostał bajt ) lub cutktóry otrzymał -bpóźniej, -cmoże oznaczać coś innego niż bajty).
Stéphane Chazelas,
Nawet gdyby używał znaków, a nie bajtów, nadal nie byłby odpowiedni do wyrównywania kolumn. Musisz wiedzieć, ile komórek terminalnych zajmuje każda postać, która różni się w zależności od postaci (0-2).
R ..
10

Jeśli zmienię kodowanie na latin-1, wyrównanie jest prawidłowe, ale umlauty są renderowane nieprawidłowo:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz

Właściwie nie, ale twój terminal nie mówi po łacinie-1, dlatego dostajesz śmieci zamiast umlautów.

Możesz to naprawić za pomocą iconv:

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

(lub po prostu uruchom cały skrypt powłoki przesłany do iconv)

Wouter Verhelst
źródło
3
To przydatny komentarz, ale nie odpowiada na podstawowe pytanie.
gerrit
1
@gerrit jak to? Jeśli printf robi to dobrze podczas drukowania w Latin1, to czy ma to wydrukować w Latin1 i przekonwertować go na UTF-8 później? Wydaje mi się, że jest to poprawna odpowiedź na kluczowe pytanie.
Wouter Verhelst,
1
Podstawowe pytanie brzmi: „Dlaczego umlaut umlaut”, odpowiedź (podobnie jak w innych odpowiedziach) brzmi „ponieważ nie obsługuje utf-8”. Nie pyta, dlaczego umlauty są renderowane źle i jak mogę naprawić renderowanie umlaut . Tak czy inaczej, twoja sugestia jest przydatna dla podzbioru utf-8, który może być reprezentowany jako iso8859-1 (tylko).
gerrit
4
@WouterVerhelst, tak, chociaż może to dotyczyć tylko tekstu, który można zakodować w zestawie znaków jednobajtowych.
Stéphane Chazelas,
3
Ja też czytam pytanie jako „w jaki sposób mogę uzyskać prawidłowy wynik”, a nie „Nie przeszkadza mi błędny wynik, o ile wiem, dlaczego”.
Pan Lister,