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:

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

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"

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:

Fr�chte und Gem�se   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)


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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.