Usando "while read ...", echo e printf obtienen diferentes resultados

14

De acuerdo con esta pregunta " Usar" mientras se lee ... "en un script de Linux "

echo '1 2 3 4 5 6' | while read a b c;do echo "$a, $b, $c"; done

Salir:

1, 2, 3 4 5 6

pero cuando lo reemplazo echoconprintf

echo '1 2 3 4 5 6' | while read a b c ;do printf "%d, %d, %d \n" $a $b $c; done

Salir

1, 2, 3
4, 5, 6

¿Podría alguien decirme qué hace que estos dos comandos sean diferentes? Gracias ~

usuario3094631
fuente

Respuestas:

24

No es solo echo vs printf

Primero, comprendamos qué sucede con la read a b cparte. readrealizará una división de palabras en función del valor predeterminado de IFSvariable que es space-tab-newline, y se ajustará a todo en función de eso. Si hay más datos que las variables para mantenerlo, se ajustarán las partes divididas en las primeras variables, y lo que no se puede ajustar, entrará en el último. Esto es lo que quiero decir:

bash-4.3$ read a b c <<< "one two three four"
bash-4.3$ echo $a
one
bash-4.3$ echo $b
two
bash-4.3$ echo $c
three four

Así es exactamente como se describe en bashel manual (ver la cita al final de la respuesta).

En su caso, lo que sucede es que 1 y 2 se ajustan a las variables ayb, y c toma todo lo demás, que es 3 4 5 6.

Lo que también verá muchas veces es que la gente usa while IFS= read -r line; do ... ; done < input.txtpara leer archivos de texto línea por línea. Nuevamente, IFS=está aquí por una razón para controlar la división de palabras, o más específicamente: deshabilítelo y lea una sola línea de texto en una variable. Si no estuviera allí, readestaría tratando de ajustar cada palabra individual en linevariable. Pero esa es otra historia, que te animo a estudiar más tarde, ya que while IFS= read -r variablees una estructura muy utilizada.

Comportamiento echo vs printf

echohace lo que esperarías aquí. Muestra sus variables exactamente como las readha organizado. Esto ya ha sido demostrado en discusiones anteriores.

printfes muy especial, porque seguirá ajustando variables en la cadena de formato hasta que todas se agoten. Entonces, cuando hace printf "%d, %d, %d \n" $a $b $cprintf ve la cadena de formato con 3 decimales, pero hay más argumentos que 3 (porque sus variables en realidad se expanden a 1,2,3,4,5,6 individuales). Esto puede sonar confuso, pero existe por una razón como un mejor comportamiento de lo que hace la función real printf() en lenguaje C.

Lo que también hizo aquí que afecta el resultado es que sus variables no se citan, lo que permite que el shell (no printf) descomponga las variables en 6 elementos separados. Compare esto con las citas:

bash-4.3$ read a b c <<< "1 2 3 4"
bash-4.3$ printf "%d %d %d\n" "$a" "$b" "$c"
bash: printf: 3 4: invalid number
1 2 3

Exactamente porque $cse cita la variable, ahora se reconoce como una cadena completa 3 4y no se ajusta al %dformato, que es solo un entero

Ahora haga lo mismo sin citar:

bash-4.3$ printf "%d %d %d\n" $a $b $c
1 2 3
4 0 0

printf nuevamente dice: "OK, tiene 6 elementos allí, pero el formato muestra solo 3, así que seguiré ajustando cosas y dejando en blanco lo que no pueda coincidir con la entrada real del usuario".

Y en todos estos casos no tiene que creer mi palabra. Simplemente corre strace -e trace=execvey comprueba por ti mismo qué "ve" realmente el comando

bash-4.3$ strace -e trace=execve printf "%d %d %d\n" $a $b $c
execve("/usr/bin/printf", ["printf", "%d %d %d\\n", "1", "2", "3", "4"], [/* 80 vars */]) = 0
1 2 3
4 0 0
+++ exited with 0 +++

bash-4.3$ strace -e trace=execve printf "%d %d %d\n" "$a" "$b" "$c"
execve("/usr/bin/printf", ["printf", "%d %d %d\\n", "1", "2", "3 4"], [/* 80 vars */]) = 0
1 2 printf: 3 4’: value not completely converted
3
+++ exited with 1 +++

Notas adicionales

Como Charles Duffy señaló correctamente en los comentarios, bashtiene su propio incorporado printf, que es lo que está utilizando en su comando, en stracerealidad llamará /usr/bin/printfversión, no versión de shell. Además de pequeñas diferencias, para nuestro interés en esta pregunta en particular, los especificadores de formato estándar son los mismos y el comportamiento es el mismo.

Lo que también debe tenerse en cuenta es que la printfsintaxis es mucho más portátil (y, por lo tanto, preferida) que echo, sin mencionar que la sintaxis es más familiar para C o cualquier lenguaje similar a C que tenga printf()funciones. Ver este excelente respuesta por terdon sobre el tema de printffrente echo. Si bien puede hacer que la salida se adapte a su shell específico en su versión específica de Ubuntu, si va a portar scripts en diferentes sistemas, probablemente debería preferir en printflugar de echo. Tal vez sea un administrador de sistemas principiante que trabaje con máquinas Ubuntu y CentOS, o tal vez incluso FreeBSD, quién sabe, por lo que en tales casos tendrá que tomar decisiones.

Cita del manual bash, sección SHELL BUILTIN COMMANDS

leer [-ers] [-a aname] [-d delim] [-i texto] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [nombre ... ]

Se lee una línea de la entrada estándar, o del descriptor de archivo fd suministrado como argumento para la opción -u, y la primera palabra se asigna al primer nombre, la segunda palabra al segundo nombre, y así sucesivamente, con las sobras. palabras y sus separadores intermedios asignados al apellido. Si se leen menos palabras de la secuencia de entrada que nombres, a los nombres restantes se les asignan valores vacíos. Los caracteres en IFS se usan para dividir la línea en palabras usando las mismas reglas que el shell usa para la expansión (descrita anteriormente en Separación de palabras).

Sergiy Kolodyazhnyy
fuente
2
Una diferencia notable entre el stracecaso y el otro strace printfes el uso /usr/bin/printf, mientras que printfdirectamente en bash está usando el shell incorporado con el mismo nombre. No siempre serán idénticos; por ejemplo, la instancia de bash tiene especificadores de formato %qy, en nuevas versiones, $()Tpara formateo de hora.
Charles Duffy
7

Esto es solo una sugerencia y no pretende reemplazar la respuesta de Sergiy. Creo que Sergiy escribió una gran respuesta sobre por qué son diferentes en la impresión. Cómo se asigna la variable en la lectura con el resto en la $cvariable como 3 4 5 6después 1y 2se asignan a ay b. echono dividirá la variable por ti donde lo printfharás con el %ds.

Sin embargo, puedes hacer que básicamente te den las mismas respuestas manipulando el eco de los números al comienzo del comando:

En /bin/bashpuedes usar:

echo -e "1 2 3 \n4 5 6"

En /bin/shsolo puedes usar:

echo "1 2 3 \n4 5 6"

Bash usa -e para habilitar los caracteres \ escape donde sh no lo necesita ya que ya está habilitado. \nhace que cree una nueva línea, por lo que ahora la línea de eco se divide en dos líneas separadas que ahora se pueden usar dos veces para su declaración de bucle de eco:

:~$ echo -e "1 2 3 \n4 5 6" | while read a b c; do echo "$a, $b, $c"; done
1, 2, 3
4, 5, 6

Lo que a su vez produce la misma salida con el uso del printfcomando:

:~$ echo -e "1 2 3 \n4 5 6" | while read a b c ;do printf "%d, %d, %d \n" $a $b $c; done
1, 2, 3 
4, 5, 6 

En sh

$ echo "1 2 3 \n4 5 6" | while read a b c; do echo "$a, $b, $c"; done
1, 2, 3
4, 5, 6
$ echo "1 2 3 \n4 5 6" | while read a b c ;do printf "%d, %d, %d \n" $a $b $c; done
1, 2, 3 
4, 5, 6 

¡Espero que esto ayude!

Terrance
fuente
echo -eno es solo una extensión de POSIX, sino cualquiera echoque no se emita -een su salida dado que la entrada en realidad está desafiando / rompiendo la especificación de letras negras. (bash no es compatible de manera predeterminada, pero cumple con el estándar cuando se configuran ambos posixy los xpg_echoindicadores; este último se puede hacer predeterminado en tiempo de compilación, lo que significa que no todas las versiones de bash funcionarán echo -ecomo espera aquí).
Charles Duffy
Consulte las secciones USO DE LA APLICACIÓN y FUNDAMENTOS de la especificación POSIX echoen pubs.opengroup.org/onlinepubs/9699919799/utilities/echo.html , que describe explícitamente que echoel comportamiento de ese no está especificado en presencia de -nbarras diagonales inversas o en cualquier lugar en la entrada, y aconseje que printfser utilizado en su lugar.
Charles Duffy
@CharlesDuffy ¿Alguna vez escribí en el mío que espero que esto funcione en todas las versiones de bash? ¿Y qué problema tienes con mi respuesta? Si cree que tiene una mejor solución, escríbala como respuesta en lugar de tratar de demostrar que la gente está equivocada. He pasado por este camino muchas veces con personas como tú, así que por favor detén estos comentarios como este. Eso es todo lo que tengo que decir.
Terrance
3
@CharlesDuffy en Ask Ubuntu a veces escribimos respuestas que no son transportables a otras distribuciones de Linux. Como su representante aquí es solo 103 puntos, es posible que no lo haya notado. Sin embargo, su representante en Stack Overflow es de 123.3K puntos, por lo que obviamente sabe un montón de cosas sobre los sistemas * NIX en general. Creo que usted, Terrace y Serg están bien, pero están mirando el hilo desde diferentes ángulos. Me hago eco (sin juego de palabras) del sentimiento de que escribas una respuesta que sea * NIX portable, o al menos portátil en las distribuciones de Linux.
WinEunuuchs2Unix
2
@CharlesDuffy Si bien estoy de acuerdo con su opinión, las respuestas deben hacerse portátiles, después de todo, este es un sitio orientado a Ubuntu, por lo tanto, los usuarios deben asumir las herramientas específicas de Ubuntu. Entonces la advertencia está algo implícita. No entremos en discusiones demasiado largas. Tienes razón en que se deben considerar otros shells, y también se debe considerar la lectura de novatos para aprender de las respuestas. Un breve comentario probablemente sería suficiente para hacer el punto.
Sergiy Kolodyazhnyy