Por que printf está “encolhendo” umlaut?

52

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 umlauts (como ü ) é "reduzido" por um caractere por umlaut.

Certamente, tenho algumas configurações erradas em algum lugar, mas não consigo descobrir qual delas poderia ser.

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

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

Fr�chte und Gem�se   foo
Milchprodukte        bar
12345678901234567890 baz
    
por René Nyffenegger 09.03.2017 / 12:44

2 respostas

87

POSIX requer printf %-20s para contar aqueles 20 em termos de bytes não caracteres mesmo que isso faça pouco sentido pois printf é imprimir texto , formatado ( veja a discussão no Austin Group (POSIX) e bash listas de discussão).

O printf incorporado em bash e a maioria das outras caixas POSIX honram isso.

zsh ignora esse requisito bobo (mesmo em sh emulação), então printf funciona como esperado. O mesmo para o printf incorporado de fish (não um shell parecido com 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 string é feita de 18 caracteres, tem 18 colunas de largura ( -L sendo uma extensão GNU wc para relatar a largura de exibição da linha mais ampla na entrada), mas é codificada em 20 bytes.

Em zsh ou fish , o texto seria alinhado corretamente.

Agora, também há caracteres com 0-width (como combinar caracteres como U + 0308, a combinação de diaresis) ou ter largura dupla como em muitos scripts asiáticos (sem mencionar os caracteres de controle como Tab) e até zsh não os alinharia corretamente.

Exemplo, em zsh :

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

Em bash :

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

ksh93 tem uma especificação de formato %Ls para contar a largura em termos de largura exibição .

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

Que ainda não funciona se o texto contiver caracteres de controle como TAB (como poderia? printf teria que saber a que distância as paradas de tabulação estão no dispositivo de saída e qual posição começa a imprimir em). Ele funciona acidentalmente com caracteres de retrocesso (como na roff output em que X (negrito X ) é gravado como X\bX ), já que ksh93 considera 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 implementações de expand (não do GNU).

Em sistemas GNU, você pode usar o GNU awk cujo printf conta em caracteres (não bytes, não exibir larguras, portanto ainda não está OK para os caracteres de 0 ou 2 de largura, mas OK 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 poderá usar sequências de escape de posicionamento do cursor. Como:

forward21=$(tput cuf 21)
printf '%s\r%s%s\n' \
  "Früchte und Gemüse"    "$forward21" "foo" \
  "Milchprodukte"         "$forward21" "bar" \
  "12345678901234567890"  "$forward21" "baz"
    
por 09.03.2017 / 12:58
10

If I change its encoding to latin-1, the alignment is correct, but the umlauts are rendered wrong:

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

Na verdade, não, mas o seu terminal não fala latin-1 e, portanto, você recebe lixo eletrônico em vez de umlauts.

Você pode corrigir isso usando iconv:

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

(ou apenas execute todo o script de shell canalizado em iconv)

    
por 09.03.2017 / 13:36