Чому printf “скорочується” умулат?


54

Якщо я виконую такий простий скрипт:

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

Він друкує:

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

тобто текст з umlauts (наприклад, ü) "стискається" на один символ на umlaut.

Звичайно, я маю десь неправильну установку, але я не в змозі зрозуміти, який з них міг би бути.

Це відбувається, якщо кодування файлу - UTF-8.

Якщо я зміню його кодування на латинську-1, вирівнювання є правильним, але umlauts відображаються неправильно:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz

14
Ви очікуєте, що printf буде в курсі UTF-8 та інших багатобайтових діаграм?
frostschutz

16
Схоже, це підрахунок байтів, а не символів; бачити echo Früchte und Gemüse | wc -c -mрізницю.
Стівен Кітт

7
@frostschutz Zsh printfє.
Стівен Кітт

10
Так, я очікую, що printf буде в курсі (принаймні) UTF-8.
Рене Ніффенеггер

12
Ну, це не так. Жорстка удача. ;-)
frostschutz

Відповіді:


87

POSIX вимагає printf , %-20sщоб лічильники рахували ці 20 в байтах, а не символи, хоча це мало сенсу, як printfдрукувати текст , відформатований (див. Обговорення в Austin Group (POSIX) та bashсписки розсилки).

printfВбудований в bashі більшість інших POSIX оболонки честь цього.

zshігнорує, що нерозумна вимога (навіть в shемуляції) printfпрацює так, як ви там очікували. Те саме для printfвбудованої fish(не POSIX-оболонки).

üСимвол (U + 00FC), коли кодується в UTF-8 складається з двох байтів (0xC3 і 0xbc), що пояснює розходження.

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

Цей рядок складається з 18 символів, шириною 18 стовпців ( -Lє wcрозширенням GNU для повідомлення про ширину відображення найширшого рядка на вході), але кодується на 20 байтах.

В zshабо fishтекст буде вирівняно правильно.

Тепер також є символи, які мають 0-ширину (наприклад, комбінування символів, таких як U + 0308, комбінуючий діарез) або мають подвійну ширину, як у багатьох азіатських сценаріях (не кажучи вже про контрольні символи типу Tab) і навіть zshне вирівнюють ці належним чином.

Приклад zsh:

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

В bash:

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

ksh93має %Lsспецифікацію формату для підрахунку ширини з точки зору ширини відображення .

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

Це все ще не працює, якщо текст містить контрольні символи, такі як TAB (як він міг printfби знати, наскільки далеко розташовані зупинки вкладки у пристрої виводу та в якому положенні він починає друкувати). Це працює випадково з символами зворотної області (як, наприклад, у roffвисновку, де X(жирним шрифтом X) написано як X\bX), хоча, як і ksh93всі керуючі символи, розглядаються як з шириною -1.

Як інші варіанти, ви можете спробувати:

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

Це працює з деякими expandреалізаціями (не GNU).

У системах GNU ви можете використовувати GNU awk, printfкількість яких рахується в символах (не байти, не ширина дисплея, тому все одно не в порядку для символів 0-ширини або 2-ширини, але OK для вашого зразка):

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

Якщо висновок переходить до терміналу, ви також можете використовувати послідовності введення позиціонування курсору. Подібно до:

forward21=$(tput cuf 21)
printf '%s\r%s%s\n' \
  "Früchte und Gemüse"    "$forward21" "foo" \
  "Milchprodukte"         "$forward21" "bar" \
  "12345678901234567890"  "$forward21" "baz"

2
Це неправильно. üCarácter може складатися , як u+ ¨, який є 3 байта. У випадку запитання він кодується як 2 символи, але не всі üстворюються однаково.
Ісмаїл Мігель

6
@IsmaelMiguel - u\u308це два символиwc -mпринаймні сенсі Unix ) для одного кластера gliph / graphem / graphem і вже згадуються та включаються до цієї відповіді.
Стефан Шазелас

"це мало сенсу, оскільки printf - це друкувати текст" Ну, можна стверджувати, що printf має справу з символами C (байтами); він не повинен мати справу з локальними текстами, і він не повинен нести тягар розуміння (можливо, багатобайтового) кодування шаблонів. Але ця лінія захисту суперечить вимогам (ISO C99), що байтове врізання "% s" не повинно призводити до "недійсних" текстів (усічені символи). У цьому випадку Glibc навіть не вдається (він нічого не друкує). Справжній безлад. postgresql.org/message-id/…
leonbloy

@leonbloy, що може мати сенс C printf(3)(мало сенсу після цієї вимоги C99, яку ви згадуєте, спасибі за це), але не printf(1)утиліта, оскільки кожен оператор оболонки чи інша текстова утиліта мають справу з символами (або були модифіковані також для роботи з символами наприклад, wcякий отримав -m(поки -cзалишився байт ) або cutщо отримав -bпісля -cможе означати щось інше, ніж байти).
Стефан Шазелас

Навіть якщо він використовував символи, а не байти, він все одно не підходить для вирівнювання стовпців. Потрібно знати, скільки кінцевих комірок займає кожен символ, що залежить від символів (0-2).
Р ..

10

Якщо я зміню його кодування на латинську-1, вирівнювання є правильним, але umlauts відображаються неправильно:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz

Насправді, ні, але ваш термінал не розмовляє з латинською-1, і тому ви отримуєте мотлох, а не умлаут.

Ви можете виправити це за допомогою iconv:

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

(або просто запустіть весь скрипт оболонки, перекладений у iconv)


3
Це корисний коментар, але не відповідає основного питання.
Герріт

1
@gerrit як так? Якщо printf робить правильно, коли друкує латиницею1, то чи повинен він друкувати латинією1 і пізніше перетворити її на UTF-8? Мені здається, належне виправлення основного питання для мене.
Wouter Verhelst

1
Основне питання: "Чому він скорочується умлаутом", відповідь (як і в інших відповідях) "тому, що він не підтримує utf-8". Це не запитання, чому Umlauts відображаються неправильно, або як я можу виправити ренделінг . У будь-якому випадку, ваша пропозиція корисна для підмножини utf-8, яка може бути представлена ​​як iso8859-1 (лише).
Герріт

4
@WouterVerhelst, так, але це може стосуватися лише тексту, який можна закодувати в однобайтовій діаграмі.
Стефан Шазелас

3
Я занадто читав питання як "як я можу отримати правильний результат", а не "Я не заперечую за несправним результатом, доки я знаю, чому".
Містер Лістер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.