tee + cat: use una salida varias veces y luego concatene los resultados

18

Si llamo a algún comando, por ejemplo echo, puedo usar los resultados de ese comando en varios otros comandos con tee. Ejemplo:

echo "Hello world!" | tee >(command1) >(command2) >(command3)

Con cat puedo recopilar los resultados de varios comandos. Ejemplo:

cat <(command1) <(command2) <(command3)

Me gustaría poder hacer ambas cosas al mismo tiempo, de modo que pueda usar teepara llamar a esos comandos en la salida de otra cosa (por ejemplo, lo echoque he escrito) y luego recopilar todos sus resultados en una sola salida con cat.

Es importante mantener los resultados en orden, esto significa que las líneas en la salida de command1, command2y command3no deben estar entrelazadas, sino ordenadas como están los comandos (como sucede con cat).

Puede haber mejores opciones que caty, teepero esas son las que conozco hasta ahora.

Quiero evitar el uso de archivos temporales porque el tamaño de la entrada y salida puede ser grande.

¿Cómo podría hacer esto?

PD: otro problema es que esto sucede en un bucle, lo que dificulta el manejo de archivos temporales. Este es el código actual que tengo y funciona para casos de prueba pequeños, pero crea bucles infinitos al leer y escribir desde el archivo auxiliar de alguna manera que no entiendo.

somefunction()
{
  if [ $1 -eq 1 ]
  then
    echo "Hello world!"
  else
    somefunction $(( $1 - 1 )) > auxfile
    cat <(command1 < auxfile) \
        <(command2 < auxfile) \
        <(command3 < auxfile)
  fi
}

Las lecturas y escritos en archivos auxiliares parecen superponerse, haciendo que todo explote.

Trylks
fuente
2
¿De qué tamaño estamos hablando? Sus requisitos obligan a todo a mantenerse en la memoria. Mantener los resultados en orden significa que el comando1 tiene que completarse primero (por lo que presumiblemente ha leído toda la entrada e impreso toda la salida), antes de que el comando2 y el comando3 puedan incluso comenzar a procesar (a menos que también desee recopilar su salida en la memoria al principio).
frostschutz
tienes razón, la entrada y salida de command2 y command3 son demasiado grandes para guardarlas en la memoria. Esperaba que usar swap funcionara mejor que usar archivos temporales. Otro problema que tengo es que esto sucede en un bucle, y eso hace que manejar archivos sea aún más difícil. Estoy usando un solo archivo, pero en este momento por alguna razón hay una superposición en la lectura y escritura del archivo que hace que crezca hasta el infinito. Intentaré actualizar la pregunta sin aburrirlo con demasiados detalles.
Trylks
44
Tienes que usar archivos temporales; ya sea para la entrada echo HelloWorld > file; (command1<file;command2<file;command3<file)o para la salida echo | tee cmd1 cmd2 cmd3; cat cmd1-output cmd2-output cmd3-output. Así es como funciona: tee puede bifurcar la entrada solo si todos los comandos funcionan y se procesan en paralelo. si un comando duerme (porque no desea intercalar) simplemente bloqueará todos los comandos, para evitar llenar la memoria con la entrada ...
frostschutz

Respuestas:

27

Puede usar una combinación de GNU stdbuf y peede moreutils :

echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output

orinar popen(3)esas 3 líneas de comando de shell y luego freadla entrada y fwritelas tres, que se almacenarán en un búfer de hasta 1M.

La idea es tener un buffer al menos tan grande como la entrada. De esta manera, aunque los tres comandos se inicien al mismo tiempo, solo verán la entrada entrando cuando pee pcloselos tres comandos se encuentren secuencialmente.

Sobre cada uno pclose, peevacía el búfer al comando y espera su finalización. Eso garantiza que mientras esos cmdxcomandos no comiencen a generar nada antes de que hayan recibido ninguna entrada (y no bifurquen un proceso que pueda continuar emitiendo después de que su padre haya regresado), la salida de los tres comandos no será intercalado

En efecto, es un poco como usar un archivo temporal en la memoria, con el inconveniente de que los 3 comandos se inician simultáneamente.

Para evitar iniciar los comandos al mismo tiempo, puede escribir peecomo una función de shell:

pee() (
  input=$(cat; echo .)
  for i do
    printf %s "${input%.}" | eval "$i"
  done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out

Pero tenga en cuenta que los shells que zshno sean fallarían para la entrada binaria con caracteres NUL.

Eso evita el uso de archivos temporales, pero eso significa que toda la entrada se almacena en la memoria.

En cualquier caso, tendrá que almacenar la entrada en algún lugar, en la memoria o en un archivo temporal.

En realidad, es una pregunta bastante interesante, ya que nos muestra el límite de la idea de Unix de que varias herramientas simples cooperen en una sola tarea.

Aquí, nos gustaría que varias herramientas cooperen con la tarea:

  • un comando fuente (aquí echo)
  • un comando de despachador ( tee)
  • algunos comandos de filtro ( cmd1, cmd2, cmd3)
  • y un comando de agregación ( cat).

Sería bueno si todos pudieran correr juntos al mismo tiempo y hacer su trabajo duro en los datos que deben procesar tan pronto como estén disponibles.

En el caso de un comando de filtro, es fácil:

src | tee | cmd1 | cat

Todos los comandos se ejecutan simultáneamente, cmd1comienza a masticar datos srctan pronto como está disponible.

Ahora, con tres comandos de filtro, aún podemos hacer lo mismo: iniciarlos simultáneamente y conectarlos con tuberías:

               ┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
               ┃   ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃   ┃
               ┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔┗━━━┛
               ┃   ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃
               ┃   ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃   ┃
               ┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛

Lo que podemos hacer con relativa facilidad con tuberías con nombre :

pee() (
  mkfifo tee-cmd1 tee-cmd2 tee-cmd3 cmd1-cat cmd2-cat cmd3-cat
  { tee tee-cmd1 tee-cmd2 tee-cmd3 > /dev/null <&3 3<&- & } 3<&0
  eval "$1 < tee-cmd1 1<> cmd1-cat &"
  eval "$2 < tee-cmd2 1<> cmd2-cat &"
  eval "$3 < tee-cmd3 1<> cmd3-cat &"
  exec cat cmd1-cat cmd2-cat cmd3-cat
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'

(lo anterior } 3<&0es evitar el hecho de que &redirige stdindesde /dev/null, y usamos <>para evitar la apertura de las tuberías para bloquear hasta que el otro extremo ( cat) también se haya abierto)

O para evitar tuberías con nombre, un poco más doloroso con zshcoproc:

pee() (
  n=0 ci= co= is=() os=()
  for cmd do
    eval "coproc $cmd $ci $co"

    exec {i}<&p {o}>&p
    is+=($i) os+=($o)
    eval i$n=$i o$n=$o
    ci+=" {i$n}<&-" co+=" {o$n}>&-"
    ((n++))
  done
  coproc :
  read -p
  eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'

Ahora, la pregunta es: una vez que todos los programas se inicien y se conecten, ¿fluirán los datos?

Tenemos dos restricciones:

  • tee alimenta todas sus salidas a la misma velocidad, por lo que solo puede enviar datos a la velocidad de su tubería de salida más lenta.
  • cat solo comenzará a leer desde la segunda tubería (tubería 6 en el dibujo anterior) cuando todos los datos hayan sido leídos desde la primera (5).

Lo que eso significa es que los datos no fluirán en la tubería 6 hasta que cmd1haya terminado. Y, como en el caso de lo tr b Banterior, eso puede significar que los datos tampoco fluirán en la tubería 3, lo que significa que no fluirán en ninguna de las tuberías 2, 3 o 4 ya quetee alimentan a la velocidad más lenta de las 3.

En la práctica, esas tuberías tienen un tamaño no nulo, por lo que algunos datos lograrán pasar, y al menos en mi sistema, puedo hacer que funcione hasta:

yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c

Más allá de eso, con

yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c

Tenemos un punto muerto en el que nos encontramos en esta situación:

               ┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
               ┃   ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃   ┃
               ┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃   ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔┗━━━┛
               ┃   ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃   ┃
               ┃   ┃██████████┃cmd3┃██████████┃   ┃
               ┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛

Hemos llenado las tuberías 3 y 6 (64 kB cada una). teeha leído ese byte extra, lo ha alimentado cmd1, pero

  • ahora está bloqueado escribiendo en la tubería 3 mientras espera cmd2vaciarlo
  • cmd2no puede vaciarlo porque está bloqueado escribiendo en la tubería 6, esperando catvaciarlo
  • cat no puede vaciarlo porque está esperando hasta que no haya más entradas en la tubería 5.
  • cmd1No puedo decir que catno hay más entrada porque está esperando más entrada de tee.
  • y teeno puedo decir que cmd1no hay más entradas porque está bloqueado ... y así sucesivamente.

Tenemos un bucle de dependencia y, por lo tanto, un punto muerto.

Ahora, ¿cuál es la solución? Las tuberías más grandes 3 y 4 (lo suficientemente grandes como para contener toda srcla salida) lo harían. Podríamos hacer eso, por ejemplo, insertando pv -qB 1Gentre teey cmd2/3dónde pvpodría almacenar hasta 1G de datos en espera cmd2y cmd3leerlos. Sin embargo, eso significaría dos cosas:

  1. eso está usando potencialmente mucha memoria y, además, duplicarlo
  2. eso no cmd2logra que los 3 comandos cooperen porque en realidad solo comenzaría a procesar datos cuando cmd1 haya terminado.

Una solución al segundo problema sería hacer las tuberías 6 y 7 más grandes también. Suponiendo eso cmd2y cmd3produciendo tanta salida como consumen, eso no consumiría más memoria.

La única forma de evitar la duplicación de datos (en el primer problema) sería implementar la retención de datos en el despachador, es decir, implementar una variación teeque pueda alimentar los datos a la velocidad de salida más rápida (mantener los datos para alimentar el los más lentos a su propio ritmo). No es realmente trivial.

Entonces, al final, lo mejor que podemos obtener razonablemente sin programación es probablemente algo así como (sintaxis Zsh):

max_hold=1G
pee() (
  n=0 ci= co= is=() os=()
  for cmd do
    if ((n)); then
      eval "coproc pv -qB $max_hold $ci $co | $cmd $ci $co | pv -qB $max_hold $ci $co"
    else
      eval "coproc $cmd $ci $co"
    fi

    exec {i}<&p {o}>&p
    is+=($i) os+=($o)
    eval i$n=$i o$n=$o
    ci+=" {i$n}<&-" co+=" {o$n}>&-"
    ((n++))
  done
  coproc :
  read -p
  eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
yes abc | head -n 1000000 | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c
Stéphane Chazelas
fuente
Tienes razón, el punto muerto es el mayor problema que he encontrado hasta ahora para evitar el uso de archivos temporales. Estos archivos parecen ser bastante rápidos, sin embargo, no sé si están almacenados en caché en algún lugar, tenía miedo de los tiempos de acceso al disco, pero hasta ahora parecen razonables.
Trylks
66
Un extra +1 para el bonito arte ASCII :-)
Kurt Pfeifle
3

Lo que usted propone no se puede hacer fácilmente con ningún comando existente, y de todos modos no tiene mucho sentido. La idea general de las canalizaciones ( |en Unix / Linux) es que en cmd1 | cmd2la cmd1salida de escritura (como máximo) hasta que se llena un búfer de memoria, y luego cmd2ejecuta la lectura de datos desde el búfer (como máximo) hasta que esté vacío. Es decir, cmd1y cmd2ejecutar al mismo tiempo, nunca es necesario tener más que una cantidad limitada de datos "en vuelo" entre ellos. Si desea conectar varias entradas a una sola salida, si uno de los lectores va a la zaga de los demás, puede detener a los demás (¿cuál es el punto de correr en paralelo entonces?) O guardar la salida que el rezagado aún no ha leído (¿Cuál es el punto de no tener un archivo intermedio entonces?). mas complejo.

En mis casi 30 años de experiencia en Unix, no recuerdo ninguna situación que realmente se hubiera beneficiado de una tubería de múltiples salidas.

Hoy en día, puede combinar varias salidas en una sola secuencia, pero no de forma intercalada (¿cómo deberían las salidas cmd1y cmd2ser intercaladas? Una línea a la vez? Turnarse para escribir 10 bytes? "Párrafos" alternativos definidos de alguna manera? t escribir algo durante mucho tiempo? todo esto es complejo de manejar). Lo hacen, por ejemplo (cmd1; cmd2; cmd3) | cmd4, los programas cmd1, cmd2y cmd3se ejecutan uno tras otro, la salida se envía como entrada a cmd4.

vonbrand
fuente
3

Para su problema de superposición, en Linux (y con basho zshpero no con ksh93), puede hacerlo como:

somefunction()
(
  if [ "$1" -eq 1 ]
  then
    echo "Hello world!"
  else
    exec 3> auxfile
    rm -f auxfile
    somefunction "$(($1 - 1))" >&3 auxfile 3>&-
    exec cat <(command1 < /dev/fd/3) \
             <(command2 < /dev/fd/3) \
             <(command3 < /dev/fd/3)
  fi
)

Tenga en cuenta el uso de en (...)lugar de {...}obtener un nuevo proceso en cada iteración para que podamos tener un nuevo fd 3 apuntando a un nuevo auxfile. < /dev/fd/3es un truco para acceder a ese archivo ahora eliminado. No funcionará en sistemas que no son Linux, donde < /dev/fd/3se como dup2(3, 0)y así fd 0 estaría abierto en modo de sólo escritura con el cursor al final del archivo.

Para evitar la bifurcación de la función anidada, puede escribirla como:

somefunction()
{
  if [ "$1" -eq 1 ]
  then
    echo "Hello world!"
  else
    {
      rm -f auxfile
      somefunction "$(($1 - 1))" >&3 auxfile 3>&-
      exec cat <(command1 < /dev/fd/3) \
               <(command2 < /dev/fd/3) \
               <(command3 < /dev/fd/3)
    } 3> auxfile
  fi
}

El shell se encargaría de hacer una copia de seguridad del fd 3 en cada iteración. Sin embargo, terminarías quedando sin descriptores de archivo antes.

Aunque encontrará que es más eficiente hacerlo como:

somefunction() {
  if [ "$1" -eq 1 ]; then
    echo "Hello world!" > auxfile
  else
    somefunction "$(($1 - 1))"
    { rm -f auxfile
      cat <(command1 < /dev/fd/3) \
          <(command2 < /dev/fd/3) \
          <(command3 < /dev/fd/3) > auxfile
    } 3< auxfile
  fi
}
somefunction 12; cat auxfile

Es decir, no anide las redirecciones.

Stéphane Chazelas
fuente