Por que o printf está "encolhendo" o trema?

54

Se eu executar o seguinte script simples:

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

Imprime:

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

ou seja, o texto com tremas (como ü) é "reduzido" por um caractere por trema.

Certamente, tenho alguma configuração errada em algum lugar, mas não consigo descobrir qual poderia ser.

Isso ocorre se a codificação do arquivo for UTF-8.

Se eu alterar sua codificação para latin-1, o alinhamento está correto, mas os tremas estão incorretos:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz
René Nyffenegger
fonte
14
Você espera que o printf esteja ciente do UTF-8 e outros conjuntos de caracteres multibyte?
Frostschutz 9/0317
16
Parece que está contando bytes em vez de caracteres; veja echo Früchte und Gemüse | wc -c -ma diferença.
Stephen Kitt
7
@frostschutz Zsh's printfé.
Stephen Kitt
10
Sim, espero que printf esteja ciente de (pelo menos) UTF-8.
René Nyffenegger 9/03/2017
12
Bem, não é. Muita sorte. ;-)
frostschutz

Respostas:

87

POSIX requer printf é %-20spara contar os 20 em termos de bytes não caracteres apesar de que faz pouco sentido como printfé imprimir texto , formatado (ver discussão no Grupo de Austin (POSIX) e bashlistas de discussão).

O printfembutido bashe a maioria dos outros cartuchos POSIX honram isso.

zshignora esse requisito bobo (mesmo em shemulação) e printffunciona como você esperaria lá. O mesmo para o printfbuilt-in fish(não é um shell semelhante ao POSIX).

O ücaractere (U + 00FC), quando codificado em UTF-8, é composto de dois bytes (0xc3 e 0xbc), o que explica a discrepância.

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

Essa cadeia é composta por 18 caracteres, tem 18 colunas de largura ( -Lsendo uma wcextensão GNU para relatar a largura de exibição da linha mais larga na entrada), mas é codificada em 20 bytes.

Em zshou fish, o texto seria alinhado corretamente.

Agora, também existem caracteres com largura 0 (como combinar caracteres como U + 0308, a diarese combinada) ou largura dupla como em muitos scripts asiáticos (para não mencionar caracteres de controle como Tab) e nem zshalinharem aqueles corretamente.

Exemplo, em zsh:

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

Em bash:

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

ksh93possui uma %Lsespecificação de formato para contar a largura em termos de largura de exibição .

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

Isso ainda não funciona se o texto contiver caracteres de controle como TAB (como poderia? printfTeria que saber a que distância as paradas de tabulação estão no dispositivo de saída e em que posição ele começa a imprimir). Ele funciona acidentalmente com caracteres de backspace (como na roffsaída em que X(negrito X) é escrito como X\bX), embora ksh93considere todos os caracteres de controle como tendo uma largura de -1.

Como outras opções, você pode tentar:

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

Isso funciona com algumas expandimplementações (embora não seja do GNU).

Nos sistemas GNU, você pode usar o GNU awkcujas printfcontagens em caracteres (não bytes, nem larguras de exibição, ainda não estão OK para os caracteres de 0 ou 2 de largura, mas sim para sua amostra):

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

Se a saída for para um terminal, você também pode usar seqüências de escape de posicionamento do cursor. Gostar:

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
fonte
2
Isso está incorreto. O ücaracter pode ser composto como u+ ¨, que é de 3 bytes. No caso da pergunta, ele é codificado como 2 caracteres, mas nem todos üsão criados igualmente.
Ismael Miguel
6
@IsmaelMiguel, u\u308tem dois caracteres ( wc -mpelo menos no Unix / sentido) para um glifo / graphem / graphem-cluster e já é mencionado e incluído nesta resposta.
Stéphane Chazelas
"isso faz pouco sentido, pois printf é imprimir texto" Bem, alguém poderia argumentar que printf lida com caracteres C (bytes); não deve lidar com localidades de texto e não deve ter o ônus de entender a codificação de charset (possivelmente multibyte). Mas essa linha de defesa entra em conflito com os requisitos (ISO C99) de que "% s" truncamento de bytes não deve resultar em textos "inválidos" (caracteres truncados). Glibc até falha nesse caso (não imprime nada). Uma verdadeira bagunça. postgresql.org/message-id/…
leonbloy
@leonbloy, isso pode dar sentido aos Cs printf(3)(pouco sentido depois do requisito C99 que você mencionou, obrigado por isso), mas não o printf(1)utilitário, pois todo operador de shell ou outro utilitário de texto lida com caracteres (ou foi modificado para também lidar com caracteres como o wcque obteve um -m(enquanto -cpermaneceu byte ) ou cutque obteve um -bdepois -cpode significar algo além de bytes).
Stéphane Chazelas
Mesmo que usasse caracteres em vez de bytes, ainda não seria adequado para alinhar colunas. Você precisa saber quantas células terminais cada caractere ocupa, o que varia de acordo com o caractere (0-2).
R ..
10

Se eu alterar sua codificação para latin-1, o alinhamento está correto, mas os tremas estão incorretos:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz

Na verdade, não, mas seu terminal não fala latim-1 e, portanto, você recebe lixo em vez de trema.

Você pode corrigir isso usando iconv:

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

(ou apenas execute todo o script do shell canalizado para iconv)

Wouter Verhelst
fonte
3
Este é um comentário útil, mas não responde à pergunta principal.
precisa saber é
11
@gerrit como assim? Se printf fizer a coisa certa ao imprimir em latin1, imprima em latin1 e converta-o em UTF-8 mais tarde? Parece uma correção adequada para a questão central para mim.
Wouter Verhelst
11
A questão principal é "Por que está encolhendo o trema", a resposta (como em outras respostas) é "porque não suporta utf-8". Não está perguntando por que os trema são processados ​​incorretamente ou como posso corrigir a renderização do trema . De qualquer forma, sua sugestão é útil para o subconjunto de utf-8 que pode ser representado como iso8859-1 (apenas).
gerrit
4
@WouterVerhelst, sim, embora isso possa se aplicar apenas ao texto que pode ser codificado em um conjunto de caracteres de byte único.
Stéphane Chazelas
3
Eu também li a pergunta como "como posso obter a saída correta" em vez de "não me importo com a saída defeituosa, desde que eu saiba o porquê".
Lister