echo vs <<<, o Uso inútil de echo en Bash Award?

19

En este momento, el uso inútil del catpremio es muy conocido, y también se menciona el uso inútil deecho (no relevante para esta pregunta). Me pregunto si debería haber un "Premio al uso inútil echoen Bash": las tuberías parecen ser mucho más lentas que los heredocs y los herejes según algunas mediciones muy poco científicas:

  • Heredocs:

    for reps in 1 2 3
    do
        time for i in {1..1000}
        do
            cat <<'END'
    test string
    END
        done > /dev/null
    done
    
    real    0m1.786s
    user    0m0.212s
    sys     0m0.332s
    
    real    0m1.817s
    user    0m0.232s
    sys     0m0.332s
    
    real    0m1.846s
    user    0m0.256s
    sys     0m0.320s
  • Herejías

    for reps in 1 2 3
    do
        time for i in {1..1000}
        do
            cat <<< 'test string'
        done > /dev/null
    done
    
    real    0m1.932s
    user    0m0.280s
    sys     0m0.288s
    
    real    0m1.956s
    user    0m0.248s
    sys     0m0.348s
    
    real    0m1.968s
    user    0m0.268s
    sys     0m0.324s
  • Redireccionamiento

    for reps in 1 2 3
    do
        time for i in {1..1000}
        do
            echo 'test string' | cat
        done > /dev/null
    done
    
    real    0m3.562s
    user    0m0.416s
    sys     0m0.548s
    
    real    0m3.924s
    user    0m0.384s
    sys     0m0.604s
    
    real    0m3.343s
    user    0m0.400s
    sys     0m0.552s

En general, los heredocs y herestrings tienen aproximadamente la misma velocidad (este es solo un conjunto de datos de varias pruebas), mientras que la redirección es consistentemente un 50% más lenta. ¿Estoy malinterpretando algo, o podría usarse esto como una regla general para los comandos que leen la entrada estándar en Bash?

l0b0
fuente
44
Ciertamente le permitiré un seqpremio de " uso inútil de " ;-)
Chris Down
2
Tenga en cuenta que usted debe comparar, ya sea cat <<ENDfrente a cat <<< "test string"frente echo "test string" | cato cat <<'END'frente a cat <<< 'test string'frente echo 'test string' | catpara tener la misma cantidad de expansiones realizadas por la concha en las cuerdas.
manatwork
@ChrisDown Derp. Siempre me olvido de eso. ¡Gracias!
l0b0
@manatwork Gracias, tiempos fijos y actualizados.
l0b0
Solo una curiosidad, ¿qué pasa con esos representantes? ¿No era tu intención original hacer bucles de $ repeticiones veces $(seq $reps)?
manatwork

Respuestas:

11

Primero, concentrémonos en el rendimiento. Ejecuté puntos de referencia para un programa ligeramente diferente en un procesador x86_64 en su mayoría inactivo que ejecuta Debian squeeze.

herestring.bash, usando una herejía para pasar una línea de entrada:

#! /bin/bash
i=0
while [ $i -lt $1 ]; do
  tr a-z A-Z <<<'hello world'
  i=$((i+1))
done >/dev/null

heredoc.bash, usando un heredoc para pasar una línea de entrada:

#! /bin/bash
i=0
while [ $i -lt $1 ]; do
  tr a-z A-Z <<'EOF'
hello world
EOF
  i=$((i+1))
done >/dev/null

echo.bash, usando echoy una tubería para pasar una línea de entrada:

#! /bin/bash
i=0
while [ $i -lt $1 ]; do
  echo 'hello world' | tr a-z A-Z
  i=$((i+1))
done >/dev/null

A modo de comparación, también cronometré los scripts bajo ATT ksh93 y bajo el guión (excepto herestring.bashporque el guión no tiene herejías).

Aquí hay una mediana de tres veces:

$ time bash ./herestring.bash 10000
./herestring.bash 10000  0.32s user 0.79s system 15% cpu 7.088 total
$ time ksh ./herestring.bash 10000
ksh ./herestring.bash 10000  0.54s user 0.41s system 17% cpu 5.277 total
$ time bash ./heredoc.bash 10000
./heredoc.bash 10000  0.35s user 0.75s system 17% cpu 6.406 total
$ time ksh ./heredoc.bash 10000  
ksh ./heredoc.sh 10000  0.54s user 0.44s system 19% cpu 4.925 total
$ time sh ./heredoc.bash 10000  
./heredoc.sh 10000  0.08s user 0.58s system 12% cpu 5.313 total
$ time bash ./echo.bash 10000
./echo.bash 10000  0.36s user 1.40s system 20% cpu 8.641 total
$ time ksh ./echo.bash 10000
ksh ./echo.sh 10000  0.47s user 1.51s system 28% cpu 6.918 total
$ time sh ./echo.sh 10000
./echo.sh 10000  0.07s user 1.00s system 16% cpu 6.463 total

Conclusiones:

  • Un heredoc es más rápido que una herejía.
  • echoy una tubería es notable, pero no dramáticamente más rápida. (Tenga en cuenta que este es un programa de juguetes: en un programa real, la mayor parte del tiempo de procesamiento sería en lo que sea que la trllamada representa aquí).
  • Si quieres velocidad, abandona bash y llama a dash o incluso mejor a ksh. Las características de Bash no compensan su relativa lentitud, pero ksh tiene características y velocidad.

Más allá del rendimiento, también hay claridad y portabilidad. <<<es una extensión ksh93 / bash / zsh que es menos conocida que echo … |o <<. No funciona en ksh88 / pdksh o en POSIX sh.

El único lugar donde <<<podría decirse que es significativamente más claro es dentro de un heredoc:

foo=$(tr a-z A-Z <<<'hello world')

vs

foo=$(tr a-z A-Z <<'EOF'
hello world
EOF
)

(La mayoría de los depósitos no pueden hacer frente al cierre del paréntesis al final de la línea que contiene <<EOF).

Gilles 'SO- deja de ser malvado'
fuente
2
Tenga en cuenta que <<<proviene de rc, y fue llevado por primera vez al mundo de shell de Bourne zsh. rcno agregó una nueva línea final, sino todo bash, zshy ksh93AFAICT. rcusa una tubería allí, while bash, zshy ksh93usa un archivo temporal como heredocs.
Stéphane Chazelas
@StephaneChazelas Vaya, debería haberlo comprobado, gracias. Esto lo hace <<<aún menos útil.
Gilles 'SO- deja de ser malvado'
También tenga en cuenta que zshtiene una optimización =(<<<foo)para crear efectivamente un archivo temporal que contiene "foo" que no implica bifurcar un proceso como lo =(echo foo)haría.
Stéphane Chazelas
También mencionaré que pdksh no incluye <<<.
kurtm
@kurtm “No funciona en ksh88 / pdksh o en POSIX sh.”
Gilles 'SO- estar parada mal'
6

Otra razón para usar heredocs (si no tenía suficiente) es que el eco puede fallar si la transmisión no se consume. Considere tener la pipefailopción bash ' :

set -o pipefail
foo=yawn
echo $foo | /bin/true ; echo $?  # returns 0

/bin/trueno consume su entrada estándar, pero echo yawncompleta sin embargo. Sin embargo, si se le pide a echo que imprima muchos datos, no se completará hasta que se truehaya completado:

foo=$(cat /etc/passwd)
# foo now has a fair amount of data

echo $foo | /bin/true ; echo $?  # returns 0 sometimes 141
echo $foo$foo$foo$foo | /bin/true ; echo $?  # returns mostly 141

141 es SIGPIPE (128 + 13) (se agrega 128 porque bash lo hace de acuerdo con bash (1):

Cuando un comando termina en una señal fatal N, bash usa el valor de 128 + N como el estado de salida.

Los heredocs no tienen este problema:

/bin/true <<< $foo$foo$foo$foo ; echo $?  # returns 0 always
mogsie
fuente
Gracias, este matiz en particular estaba causando que mi script fallara intermitentemente en lugares aleatorios, más aún en los servidores de Amazon que en los hosts kvm, pero aún fallaba de vez en cuando, en lugares aparentemente imposibles de fallar. ¡Es bueno que el valor pipefailpredeterminado sea estar apagado!
mogsie
2
Oh, habilito pipefailen la parte superior de cada script. ¡Dame todos los errores!
l0b0
@ l0b0 Hmmm. Tal vez agregue pipefail a j.mp/safebash. ¡Estoy a favor de obtener todos los errores durante el desarrollo!
Bruno Bronosky
2

Una razón por la que es posible que desee usar echo es para ejercer cierto control sobre el carácter de nueva línea que se agrega al final de heredocs y hererestrings:

Tres caracteres footienen longitud 3:

$ echo -n foo | wc -c
3

Sin embargo, una herejía de tres caracteres tiene cuatro caracteres:

$ wc -c <<< foo
4

Un heredoc de tres caracteres también:

$ wc -c << EOF
foo
EOF
4

El cuarto personaje es un 0x0apersonaje de nueva línea .

De alguna manera, esto encaja mágicamente con la forma en que bash elimina estos caracteres de nueva línea al tomar la salida de un subconjunto:

Aquí hay un comando que devuelve cuatro caracteres: fooy \n. El '\ n' se agrega por echo, siempre agrega un carácter de nueva línea a menos que especifique la -nopción:

$ echo foo
foo
$ echo foo | wc -c
4

Sin embargo, al asignar esto a una variable, se elimina la nueva línea final agregada por echo:

$ foo=$(echo foo)
$ echo "$foo" # here, echo adds a newline too.
foo

Entonces, si mezcla archivos y variables y los usa en los cálculos (por ejemplo, no puede usar heredocs o herestrings, ya que agregarán una nueva línea.

foo=abc
echo -n 'abc' > something.txt
if [ $(wc -c <<< "$foo") -eq $(wc -c < something.txt) ] ; then
  echo "yeah, they're the same!"
else
  echo "foo and bar have different lengths (except, maybe not)"
fi

Si cambia la declaración if para leer

if [ $(echo -n "$foo" | wc -c) -eq $(wc -c < something.txt) ] ; then

entonces la prueba pasa.

mogsie
fuente