¿Por qué printf "encoge" la diéresis?

54

Si ejecuto el siguiente script simple:

#!/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

es decir, el texto con diéresis (como ü) es "reducido" por un carácter por diéresis.

Ciertamente, tengo una configuración incorrecta en alguna parte, pero no soy capaz de averiguar cuál podría ser.

Esto ocurre si la codificación del archivo es UTF-8.

Si cambio su codificación a latin-1, la alineación es correcta, pero las diéresis son incorrectas:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz
René Nyffenegger
fuente
14
¿Espera que printf esté al tanto de UTF-8 y otros conjuntos multibyte?
frostschutz
16
Parece que está contando bytes en lugar de caracteres; Mira echo Früchte und Gemüse | wc -c -mpor la diferencia.
Stephen Kitt
77
@frostschutz Zsh's printfes.
Stephen Kitt
10
Sí, espero que printf tenga en cuenta (al menos) UTF-8.
René Nyffenegger el
12
Pues no lo es. Mala suerte ;-)
frostschutz

Respuestas:

87

POSIX requiere printf Es %-20spara contar los 20 en términos de bytes no personajes a pesar de que tiene poco sentido como printfes imprimir texto , formateado (véase la discusión en el Grupo Austin (POSIX) y bashlistas de correo).

La printfconstrucción de bashy la mayoría de los otros proyectiles POSIX honran eso.

zshignora ese requisito tonto (incluso en la shemulación), por lo que printffunciona como es de esperar allí. Lo mismo para la printfconstrucción de fish(no un shell POSIX).

El ücarácter (U + 00FC), cuando se codifica en UTF-8, está formado por dos bytes (0xc3 y 0xbc), lo que explica la discrepancia.

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

Esa cadena está hecha de 18 caracteres, tiene 18 columnas de ancho ( -Lsiendo una wcextensión GNU para informar el ancho de visualización de la línea más ancha en la entrada) pero está codificada en 20 bytes.

En zsho fish, el texto se alinearía correctamente.

Ahora, también hay caracteres que tienen ancho 0 (como combinar caracteres como U + 0308, la diéresis combinada) o tienen doble ancho como en muchos scripts asiáticos (sin mencionar caracteres de control como Tab) e incluso zshno se alinearían esos correctamente.

Ejemplo, en zsh:

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

En bash:

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

ksh93tiene una %Lsespecificación de formato para contar el ancho en términos de ancho de pantalla .

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

Eso todavía no funciona si el texto contiene caracteres de control como TAB (¿cómo podría? printfTendría que saber qué tan separados están los tabuladores en el dispositivo de salida y en qué posición comienza a imprimir). Funciona por accidente con caracteres de retroceso (como en la roffsalida donde X(negrita X) se escribe como X\bX) aunque ksh93considera que todos los caracteres de control tienen un ancho de -1.

Como otras opciones, puedes probar:

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

Eso funciona con algunas expandimplementaciones (aunque no con GNU).

En los sistemas GNU, puede usar GNU awkcuyos printfrecuentos en caracteres (no bytes, no anchos de visualización, por lo que aún no está bien para los caracteres de 0 o 2 anchos, pero está bien para su muestra):

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

Si la salida va a un terminal, también puede usar secuencias de escape de posicionamiento del cursor. Me gusta:

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
fuente
2
Eso es incorrecto. El ücaracter se puede componer como u+ ¨, que es de 3 bytes. En el caso de la pregunta, se codifica como 2 caracteres, pero no todos üse crean por igual.
Ismael Miguel
66
@IsmaelMiguel, u\u308son dos caracteres (en Unix / wc -msentido al menos) para un glifo / gráfico / grupo de gráficos y ya se menciona e incluye en esta respuesta.
Stéphane Chazelas
"eso tiene poco sentido ya que printf es imprimir texto" Bueno, uno podría argumentar que printf trata con caracteres C (bytes); no debería tratar con configuraciones regionales de texto, y no debería tener la carga de comprender la codificación de charset (posiblemente multibyte). Pero esta línea de defensa entra en conflicto con los requisitos (ISO C99) de que el truncamiento de bytes "% s" no debe dar como resultado textos "inválidos" (caracteres truncados). Glibc incluso falla en ese caso (no imprime nada). Un verdadero desastre. postgresql.org/message-id/…
leonbloy
@leonbloy, eso podría tener sentido para las C printf(3)(poco sentido después de ese requisito de C99 que estás mencionando, gracias por eso), pero no la printf(1)utilidad ya que cada operador de shell u otra utilidad de texto trata con caracteres (o se modificaron para tratar también con caracteres como wccuál obtuvo un -m(mientras -cpermaneció en byte ) o cutque obtuvo un -bdespués -cpodría significar algo más que bytes).
Stéphane Chazelas
Incluso si usara caracteres en lugar de bytes, no sería adecuado para alinear columnas. Necesita saber cuántas celdas terminales ocupa cada carácter, que varía según el carácter (0-2).
R ..
10

Si cambio su codificación a latin-1, la alineación es correcta, pero las diéresis son incorrectas:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz

En realidad, no, pero su terminal no habla latín-1 y, por lo tanto, obtiene basura en lugar de diéresis.

Puedes arreglar esto usando iconv:

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

(o simplemente ejecute todo el script de shell canalizado en iconv)

Wouter Verhelst
fuente
3
Este es un comentario útil pero no responde la pregunta central.
gerrit
1
@gerrit ¿cómo es eso? Si printf hace lo correcto cuando imprime en latin1, ¿tiene que imprimir en latin1 y convertirlo a UTF-8 más tarde? Parece una solución adecuada para la pregunta central para mí.
Wouter Verhelst
1
La pregunta central es "¿Por qué está reduciendo la diéresis?", La respuesta (como en otras respuestas) es "porque no admite utf-8". No se pregunta por qué las diéresis están mal representadas o cómo puedo solucionar el renderizado de diéresis . De cualquier manera, su sugerencia es útil para el subconjunto de utf-8 que se puede representar como iso8859-1 (solo).
gerrit
44
@WouterVerhelst, sí, aunque eso solo puede aplicarse al texto que puede codificarse en un conjunto de caracteres de un solo byte.
Stéphane Chazelas
3
Yo también leí la pregunta como "¿cómo puedo obtener la salida correcta" en lugar de "No me importa la salida defectuosa, siempre que sepa por qué".
Sr. Lister