printf e UTF-8 #180

Open
aureliojargas opened this Issue Mar 12, 2015 · 16 comments

Projects

None yet

2 participants

@aureliojargas
Member

A codificação UTF-8 é multibyte, ou seja, cada caractere pode ser composto por um ou mais bytes.

  • A letra a é composta por um byte
  • A letra á é composta por dois bytes
  • O símbolo é composto por três bytes

Confira:

$ printf 'a' | od -t x1
0000000    61                                                            
0000001
$ printf 'á' | od -t x1
0000000    c3  a1                                                        
0000002
$ printf '' | od -t x1
0000000    e2  99  a5                                                    
0000003
$

Quando você usa o printf para alinhar colunas, definindo uma largura fixa, as coisas não funcionam como se espera, pois o printf %<número>s conta bytes e não caracteres:

$ printf '|%5s|\n' a m z á ú ♥ ★      # Comportamento esperado 
|    a|
|    m|
|    z|
|    á|
|    ú|
|    ♥|
|    ★|

$ printf '|%5s|\n' a m z á ú ♥ ★      # Vida real :(
|    a|
|    m|
|    z|
|   á|
|   ú|
|  ♥|
|  ★|

Por isso a saída de algumas funções aparece desalinhada quando há letras acentuadas no resultado. Cada letra acentuada do português vale por dois :/ Exemplos de funções problemáticas:

  • zzfutebol
  • zzpais
  • zzquimica

Talvez no futuro o printf seja atualizado para levar em conta a contagem de caracteres e não de bytes, mas como Unicode é um assunto bem complexo, isso pode demorar bastante. Para ter uma ideia do tamanho do problema, veja esta resposta: http://stackoverflow.com/a/9325750/1623438

Bem, este é o problema. Abri este issue para discutirmos possíveis soluções. Lembrando que nosso universo se restringe ao português, então uma solução meia-boca somente para caracteres Latin-1 já é suficiente.

  • Como garantir uma saída alinhada das funções que hoje usam printf?
  • Como contar caracteres e não bytes?
@aureliojargas
Member

Uma solução alternativa que funciona para alguns casos, é separar os campos por tabs e usar o expand para trocar os tabs por espaços. Isso é feito na zzestado e funciona no BSD.

# trecho no final da zzestado
zzestado --formato '{sigla}\t{nome}\t{capital}\n' | expand -t 6,29

Exemplo de execução:

$ zzestado
AC    Acre                   Rio Branco
AL    Alagoas                Maceió
AP    Amapá                  Macapá
AM    Amazonas               Manaus
BA    Bahia                  Salvador
CE    Ceará                  Fortaleza
DF    Distrito Federal       Brasília
ES    Espírito Santo         Vitória
GO    Goiás                  Goiânia
MA    Maranhão               São Luís
MT    Mato Grosso            Cuiabá
MS    Mato Grosso do Sul     Campo Grande
MG    Minas Gerais           Belo Horizonte
PA    Pará                   Belém
PB    Paraíba                João Pessoa
PR    Paraná                 Curitiba
PE    Pernambuco             Recife
PI    Piauí                  Teresina
RJ    Rio de Janeiro         Rio de Janeiro
RN    Rio Grande do Norte    Natal
RS    Rio Grande do Sul      Porto Alegre
RO    Rondônia               Porto Velho
RR    Roraima                Boa Vista
SC    Santa Catarina         Florianópolis
SP    São Paulo              São Paulo
SE    Sergipe                Aracaju
TO    Tocantins              Palmas
$
@aureliojargas
Member

No BSD, o awk sofre do mesmo problema:

$ awk 'BEGIN { printf "|%5s|\n", "a" }'
|    a|
$ awk 'BEGIN { printf "|%5s|\n", "á" }'
|   á|
$ awk 'BEGIN { printf "|%5s|\n", "♥" }'
|  ♥|
$
@aureliojargas
Member

No BSD, algumas ferramentas acertam a contagem de caracteres.

$ printf "aá♥" | cut -c 2     # cut OK
á
$ printf "aá♥" | cut -c 3

$ printf "aá♥" | wc -c        # wc -c NÃO
       6
$ printf "aá♥" | wc -m        # wc -m OK (mas não sei se -m é portável)
       3
$ echo "aá♥" | sed 's/././2'  # sed OK
a.♥
$ echo "aá♥" | sed 's/././3'
aá.
$ echo "aá♥" | sed 's/././g'
...
$
@aureliojargas
Member

Dentro do universo ZZ, acredito que a zzpad seja a função ideal para concentrar os esforços de imprimir com o padding correto, incluindo UTF-8. Se conseguirmos arrumá-la, as outras funções que hoje usam printfpara alinhamento poderiam passar a usá-la.

Mas tem que arrumar, pois hoje ela sofre do mesmo problema:

$ zzpad -l 5 _ a 
____a
$ zzpad -l 5 _ á
___á
$ zzpad -l 5 _ ♥
__♥
$
@aureliojargas
Member

Consegui um exemplo funcional em sed:

$ echo a | sed -e ':loop' -e 's/^/_/' -e '/^.\{5\}/! b loop' 
____a
$ echo á | sed -e ':loop' -e 's/^/_/' -e '/^.\{5\}/! b loop' 
____á
$ echo| sed -e ':loop' -e 's/^/_/' -e '/^.\{5\}/! b loop' 
____♥
$
@aureliojargas aureliojargas added a commit that referenced this issue Mar 12, 2015
@aureliojargas aureliojargas zzpad: adicionados testes para UTF-8
A função ainda não consegue passar nestes testes. Vide issue #180
1de3e09
@aureliojargas
Member

Mais um teste, a contagem de caracteres do bash ${#var} funcionou corretamente:

$ x="aá♥"
$ echo ${#x}
3
$ bash --version
GNU bash, version 3.2.53(1)-release (x86_64-apple-darwin14)
Copyright (C) 2007 Free Software Foundation, Inc.
$

Mas não sei desde qual versão do bash isso funciona.

@aureliojargas aureliojargas added a commit that referenced this issue Mar 12, 2015
@aureliojargas aureliojargas zzpad: agora suporta UTF-8
Veja issue #180 para mais informações.
f0ac2ac
@aureliojargas
Member

@itamarnet desculpe remover o teu awk da zzpad, mas com o sed agora ela funciona corretamente com UTF-8. Pelo menos aqui na minha máquina. Confirma que na tua ficou OK também?

$ zzpad -l 5 _ a 
____a
$ zzpad -l 5 _ á
____á
$ zzpad -l 5 _ ♥
____♥
$

Agora, a zzpad pode substituir o printf para o alinhamento de colunas na saída.

E mais, como ao usar uma subshell a quebra de linha no final é removida, podemos usar esta função diretamente, várias vezes, num único echo:

$ echo "$(zzpad -l 5 _ á) $(zzpad -l 5 _ ★)"
____á ____★
$
@itamarnet
Contributor

Grande @aureliojargas não precisa se desculpar.
O uso do awk atendia uma necessidade menos ampla e foi uma solução encontrada num momento.
Ter uma melhora usando o sed não é algo para lamentar, mas a comemorar devido a ampliação da capacidade.
Se eu dominasse o sed da mesma maneira que você teria feito antes, mas não é o caso. Ao menos o que foi feito antes serviu como um ponto de partida da idéia, e a função e eu agradeço.
A noite testo e dou um feedback.
👍

@itamarnet
Contributor

Com relação ao wc -m, também não sei se é portável, mas parece que sim.
Tomo por base esse link, e apenas alguns sabores é que não suportam essa opção.
http://www.unix.com/man-page/posix/1p/wc/

Acho que podemos considerar que abrange tudo dentro do universo de ação.
Me referencio em especial a ele, pela possibilidade do retorno do tamanho da string de forma limpa e correta.
E é esse número que pode ser usado pela zzpad, auxiliando a priori zzcolunar e zzalinhar

Obs.: Pena que "wc -L" tenha uma abrangência menor nos SO's, seria uma mão na roda e economizaria muito código.

@itamarnet
Contributor

Essa nova versão do zzpad funcionou perfeitamente.
Teste realizado em várias distros e máquinas.
Show! 👍

@aureliojargas aureliojargas added the bug label Mar 13, 2015
@itamarnet
Contributor

Infelizmente no GNU Linux o cut não tem a mesma agilidade que no BSD:

$ printf "aá♥" | cut -c 2         # não funciona
$ echo "aá♥" | cut -c 3           # não funciona
�

# Alinhamento varia em função da ocorrência de caracteres acentuados
$ zzquimica | head | cut -c 1-26 | sed 's/$/~/'
N.º  Nome          Símbo~
1    Hidrogênio    H     ~
2    Hélio         He    ~
3    Lítio         Li    ~
4    Berílio       Be    ~
5    Boro          B      ~
6    Carbono       C      ~
7    Nitrogênio    N     ~
8    Oxigênio      O     ~
9    Flúor         F     ~

O wc -m conta corretamente, e o sed mantém a coerência como no BSD.
Isso justifica a mudança feita no commit f13a655

@aureliojargas
Member

Como está o locale dessa máquina?

@itamarnet
Contributor

Ops.: esqueci de informar.
Locale: pt_BR.UTF-8

@aureliojargas
Member

Que estranho... As ferramentas GNU em geral são mais avançadas, eram pra suportar melhor o UTF-8. Eu estava tendo problemas também num Debian, até que conferi e o locale não estava definido corretamente em todas as variáveis. Foi só arrumar e daí funcionou.

Dá uma olhada como ficou:

$ locale
LANG=pt_BR.utf8
LANGUAGE=
LC_CTYPE="pt_BR.utf8"
LC_NUMERIC="pt_BR.utf8"
LC_TIME="pt_BR.utf8"
LC_COLLATE="pt_BR.utf8"
LC_MONETARY="pt_BR.utf8"
LC_MESSAGES="pt_BR.utf8"
LC_PAPER="pt_BR.utf8"
LC_NAME="pt_BR.utf8"
LC_ADDRESS="pt_BR.utf8"
LC_TELEPHONE="pt_BR.utf8"
LC_MEASUREMENT="pt_BR.utf8"
LC_IDENTIFICATION="pt_BR.utf8"
LC_ALL=pt_BR.utf8
$

Eu estou desconfiado que possa estar faltando alguma coisa no teu locale.

@itamarnet
Contributor

Pois é Aurélio, deve ser algo que eu ainda não percebi.
Variei a configuração do locale, e o cut sempre teve o mesmo problema.
veja minha última saída do locale.

$ locale
LANG=pt_BR.utf8
LANGUAGE=
LC_CTYPE="pt_BR.utf8"
LC_NUMERIC=pt_BR.utf8
LC_TIME=pt_BR.utf8
LC_COLLATE="pt_BR.utf8"
LC_MONETARY=pt_BR.utf8
LC_MESSAGES="pt_BR.utf8"
LC_PAPER=pt_BR.utf8
LC_NAME=pt_BR.utf8
LC_ADDRESS=pt_BR.utf8
LC_TELEPHONE=pt_BR.utf8
LC_MEASUREMENT=pt_BR.utf8
LC_IDENTIFICATION=pt_BR.utf8
LC_ALL=

Mesmo com LC_ALL=pt_BR.utf8, o problema manteve-se
E antes no lugar do "utf8" estava "UTF-8" e o comportamento não mudou.
Estou sem idéias! Alguma sugestão?

@aureliojargas
Member

É... Está tudo certo com teu locale também. Agora lascou :)

Então deve ser isso mesmo. Algumas ferramentas do Linux ainda estão quebradas no UTF-8 e temos que contornar :/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment