Mengapa printf “menyusut” umlaut?

54

Jika saya menjalankan skrip sederhana berikut:

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

Mencetak:

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

yaitu, teks dengan umlaut (seperti ü) adalah "menyusut" oleh satu karakter per umlaut.

Tentu saja, saya memiliki beberapa pengaturan yang salah di suatu tempat, tetapi saya tidak dapat menemukan yang mana.

Ini terjadi jika penyandian file adalah UTF-8.

Jika saya mengubah penyandiannya ke latin-1, perataannya benar, tetapi umlaut yang ditampilkan salah:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz
René Nyffenegger
sumber
14
Anda berharap printf mengetahui UTF-8 dan rangkaian karakter multibyte lainnya?
frostschutz
16
Sepertinya itu menghitung byte daripada karakter; lihat echo Früchte und Gemüse | wc -c -mperbedaannya.
Stephen Kitt
7
@frostschutz Zsh printfadalah.
Stephen Kitt
10
Ya, saya berharap printf mengetahui (setidaknya) UTF-8.
René Nyffenegger
12
Ya tidak. Keberuntungan yang cukup. ;-)
frostschutz

Jawaban:

87

POSIX membutuhkan printf 's %-20suntuk menghitung orang-orang 20 dalam hal byte tidak karakter meskipun itu masuk akal sebagai printfadalah untuk mencetak teks , diformat (lihat diskusi di Austin Grup (POSIX) dan bashmailing list).

Kerangka printfbawaan bashdan sebagian besar kerang POSIX lainnya menghormatinya.

zshmengabaikan persyaratan konyol (bahkan dalam shpersaingan) sehingga printfberfungsi seperti yang Anda harapkan di sana. Sama untuk printfbuiltin dari fish(bukan shell seperti POSIX).

The ükarakter (U + 00FC), ketika dikodekan dalam UTF-8 terbuat dari dua byte (0xc3 dan 0xbc), yang menjelaskan perbedaan tersebut.

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

String itu terdiri dari 18 karakter, lebar 18 kolom ( -Lmenjadi wcekstensi GNU untuk melaporkan lebar tampilan garis terluas dalam input) tetapi dikodekan pada 20 byte.

Di zshatau fish, teks akan disejajarkan dengan benar.

Sekarang, ada juga karakter yang memiliki 0-lebar (seperti menggabungkan karakter seperti U + 0308, yang menggabungkan diaresis) atau memiliki lebar ganda seperti di banyak skrip Asiatik (belum lagi karakter kontrol seperti Tab) dan bahkan zshtidak akan menyelaraskan mereka dengan benar.

Contoh, di zsh:

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

Dalam bash:

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

ksh93memiliki %Lsspesifikasi format untuk menghitung lebar dalam hal tampilan lebar.

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

Itu masih tidak berfungsi jika teks berisi karakter kontrol seperti TAB (bagaimana mungkin? printfHarus tahu seberapa jauh jarak tab berhenti di perangkat output dan di mana ia mulai mencetak). Ia bekerja secara tidak sengaja dengan karakter backspace (seperti dalam roffoutput di mana X(dicetak tebal X) ditulis X\bX) meskipun ksh93menganggap semua karakter kontrol memiliki lebar -1.

Sebagai opsi lain, Anda dapat mencoba:

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

Itu bekerja dengan beberapa expandimplementasi (bukan GNU sekalipun).

Pada sistem GNU, Anda bisa menggunakan GNU awkyang printfmenghitung dalam karakter (bukan byte, bukan lebar layar, jadi masih tidak OK untuk karakter 0 lebar atau 2 lebar, tapi OK untuk sampel Anda):

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

Jika output masuk ke terminal, Anda juga dapat menggunakan urutan pelarian posisi kursor. Suka:

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
sumber
2
Itu tidak benar. The ücaracter dapat disusun sebagai u+ ¨, yang merupakan 3 byte. Dalam kasus pertanyaan, ini dikodekan sebagai 2 karakter, tetapi tidak semua üdibuat sama.
Ismael Miguel
6
@IsmaelMiguel, u\u308adalah dua karakter ( wc -msetidaknya dalam Unix / sense) untuk satu glyph / graphem / graphem-cluster dan sudah disebutkan dan dimasukkan dalam jawaban ini.
Stéphane Chazelas
"Itu tidak masuk akal karena printf adalah untuk mencetak teks" Ya, orang bisa berpendapat bahwa printf berkaitan dengan karakter C (byte); seharusnya tidak berurusan dengan lokal teks, dan seharusnya tidak memiliki beban memahami pengkodean charset (mungkin multibyte). Tetapi garis pertahanan ini bertentangan dengan persyaratan (ISO C99) bahwa pemotongan byte "% s" tidak boleh menghasilkan teks "tidak valid" (karakter terpotong). Glibc bahkan gagal dalam kasus itu (tidak mencetak apa-apa). Kekacauan yang nyata. postgresql.org/message-id/…
leonbloy
@leonbloy, itu mungkin masuk akal C printf(3)(sedikit masuk akal setelah persyaratan C99 yang Anda sebutkan, terima kasih untuk itu), tetapi bukan printf(1)utilitas karena setiap operator shell atau utilitas teks lainnya berurusan dengan karakter (atau dimodifikasi untuk juga berurusan dengan karakter seperti wcyang mendapat -m(sementara byte-c tetap ) atau yang mendapat after bisa berarti sesuatu yang lain daripada byte). cut-b-c
Stéphane Chazelas
Bahkan jika itu menggunakan karakter daripada byte, itu masih tidak akan cocok untuk meluruskan kolom. Anda perlu tahu berapa banyak sel terminal yang ditempati setiap karakter, yang bervariasi berdasarkan karakter (0-2).
R ..
10

Jika saya mengubah penyandiannya ke latin-1, perataannya benar, tetapi umlaut yang ditampilkan salah:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz

Sebenarnya, tidak, tetapi terminal Anda tidak berbicara bahasa latin-1, dan karena itu Anda mendapatkan sampah daripada umlaut.

Anda dapat memperbaikinya dengan menggunakan ikonv:

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

(atau jalankan saja skrip shell yang disalurkan ke iconv)

Wouter Verhelst
sumber
3
Ini adalah komentar yang bermanfaat tetapi tidak menjawab pertanyaan inti.
gerrit
1
@gerit bagaimana? Jika printf melakukan hal yang benar saat mencetak dalam bahasa latin1, lalu cetak dalam bahasa latin1 dan mengubahnya menjadi UTF-8 nanti? Sepertinya perbaikan yang tepat untuk pertanyaan inti bagi saya.
Wouter Verhelst
1
Pertanyaan intinya adalah "Mengapa menyusut umlaut", jawabannya (seperti pada jawaban lain) adalah "karena tidak mendukung utf-8". Itu tidak bertanya mengapa umlaut dirender salah atau bagaimana saya bisa memperbaiki rendering umlaut . Either way, saran Anda berguna untuk himpunan bagian utf-8 yang dapat direpresentasikan sebagai iso8859-1 (hanya).
gerrit
4
@WouterVerhelst, ya itu hanya berlaku untuk teks yang dapat dikodekan dalam charset byte tunggal.
Stéphane Chazelas
3
Saya juga membaca pertanyaan sebagai "bagaimana saya bisa mendapatkan output dengan benar" daripada "Saya tidak keberatan dengan output yang salah, selama saya tahu mengapa".
Tn. Lister