Почему 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

то есть текст с умлаутами (например, ü) сокращается на один символ за умлаут.

Конечно, у меня где-то неправильные настройки, но я не могу понять, какой это может быть.

Это происходит, если кодировка файла UTF-8.

Если я изменю его кодировку на latin-1, выравнивание будет правильным, но умлауты будут отображаться неправильно:

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's 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 ширины, но в порядке для вашего образца):

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

Если вывод идет на терминал, вы также можете использовать escape-последовательности позиционирования курсора. Подобно:

forward21=$(tput cuf 21)
printf '%s\r%s%s\n' \
  "Früchte und Gemüse"    "$forward21" "foo" \
  "Milchprodukte"         "$forward21" "bar" \
  "12345678901234567890"  "$forward21" "baz"
Стефан Шазелас
источник
2
Это неверно. Символ üможет быть составлен как u+ ¨, что составляет 3 байта. В случае вопроса он закодирован как 2 символа, но не все üсозданы одинаково.
Исмаэль Мигель
6
@IsmaelMiguel u\u308- это два символа ( wc -mпо крайней мере, в Unix / смысле) для одного глифа / графема / графем-кластера, и он уже упоминается и включен в этот ответ.
Стефан
«это не имеет большого смысла, так как 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).
R ..
10

Если я изменю его кодировку на latin-1, выравнивание будет правильным, но умлауты будут отображаться неправильно:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz

На самом деле, нет, но ваш терминал не говорит по-латыни, и поэтому вы получаете мусор, а не умлаутс.

Вы можете исправить это, используя iconv:

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

(или просто запустите весь сценарий оболочки по иконке)

Воутер Верхелст
источник
3
Это полезный комментарий, но он не отвечает на основной вопрос.
Gerrit
1
@gerrit как так? Если printf делает правильные вещи при печати на латинице, то печатает ли она на латинице 1 и конвертирует ли она в UTF-8 позже? Похоже, правильное решение для основного вопроса для меня.
Воутер Верхелст
1
Основной вопрос: «Почему это сокращается?», Ответ (как и в других ответах) «потому что он не поддерживает utf-8». Не спрашивается, почему умлауты отображаются неправильно или как я могу исправить умлаут . В любом случае, ваше предложение полезно для подмножества utf-8, которое может быть представлено как iso8859-1 (только).
gerrit
4
@WouterVerhelst, да, хотя это может применяться только к тексту, который может быть закодирован в однобайтовой кодировке.
Стефан
3
Я также прочитал вопрос как «как я могу получить правильный вывод», а не «я не возражаю против ошибочного вывода, если я знаю почему».
Мистер Листер