Anteponer la última línea de stdin a stdin completo

9

Considera este script:

tmpfile=$(mktemp)

cat <<EOS > "$tmpfile"
line 1
line 2
line 3
EOS

cat <(tail -1 "$tmpfile") "$tmpfile"

Esto funciona y produce:

line 3
line 1
line 2
line 3

Digamos que nuestra fuente de entrada, en lugar de ser un archivo real, era stdin:

cat <<EOS | # what goes here now?
line 1
line 2
line 3
EOS

¿Cómo modificamos el comando?

cat <(tail -1 "$tmpfile") "$tmpfile"

¿De modo que todavía produce la misma salida, en este contexto diferente?

NOTA: El Heredoc específico que estoy buscando, así como el uso de un Heredoc en sí mismo, es meramente ilustrativo. Cualquier respuesta aceptable debe suponer que está recibiendo datos arbitrarios a través de stdin .

Jonás
fuente
1
stdin siempre es un "archivo real" (un fifo / socket / etc también es un archivo; no todos los archivos son buscables). La respuesta a su pregunta es un trivial "usar un archivo temporal" o algún horror que cargará todo el archivo en la memoria. "¿Cómo puedo recuperar datos antiguos de una transmisión sin haberlos almacenado en ningún lado ?" No puedo tener una buena respuesta.
mosvy
1
@mosvy Esa es una respuesta perfectamente aceptable si desea agregarla.
Jonás
2
@mosvy Como Jonah ha dicho, las respuestas deben publicarse en el cuadro de respuestas. Sé que es difícil leer cualquiera de los sitios web en este momento, pero ignore el rojo que gotea lentamente sobre su visión y use el área de texto inferior.
wizzwizz4

Respuestas:

7

Tratar:

awk '{x=x $0 ORS}; END{printf "%s", $0 ORS x}'

Ejemplo

Defina una variable con nuestra entrada:

$ input="line 1
> line 2
> line 3"

Ejecute nuestro comando:

$ echo "$input" | awk '{x=x $0 ORS}; END{printf "%s", $0 ORS x}'
line 3
line 1
line 2
line 3

Alternativamente, por supuesto, podríamos usar un documento aquí:

$ cat <<EOS | awk '{x=x $0 ORS}; END{printf "%s", $0 ORS x}'
line 1
line 2
line 3
EOS
line 3
line 1
line 2
line 3

Cómo funciona

  • x=x $0 ORS

    Esto agrega cada línea de entrada a la variable x.

    En awk, ORSes el separador de registro de salida . Por defecto, es un carácter de nueva línea.

  • END{printf "%s", $0 ORS x}

    Después de haber leído todo el archivo, imprime la última línea $0, seguida del contenido de todo el archivo x.

Como esto lee toda la entrada en la memoria, no sería apropiado para entradas grandes ( por ejemplo, gigabytes).

John1024
fuente
Gracias John Entonces, ¿no es posible hacer esto de una manera análoga a mi ejemplo de archivo con nombre en el OP? Me imaginaba que el stdin se duplicaba de alguna manera ... de alguna manera tee, pero de un stdin y un archivo, estaríamos canalizando el mismo stdin en dos sustituciones de proceso diferentes. o algo que sería más o menos equivalente a eso?
Jonás
5

Si stdin apunta a un archivo que se puede buscar (como en el caso de los documentos aquí de bash (pero no todos los demás shell) que se implementan con archivos temporales), puede obtener la cola y luego buscar antes de leer el contenido completo:

Los operadores de búsqueda están disponibles en los shells zsho ksh93, o los lenguajes de script como tcl / perl / python, pero no en bash. Pero siempre puede llamar a esos intérpretes más avanzados bashsi tiene que usar bash.

ksh93 -c 'tail -n1; cat <#((0))' <<...

O

zsh -c 'zmodload zsh/system; tail -n1; sysseek 0; cat' <<...

Ahora, eso no funcionará cuando stdin apunte a archivos no buscables como una tubería o un socket. Entonces, la única opción es leer y almacenar (en la memoria o en un archivo temporal ...) toda la entrada.

Ya se han dado algunas soluciones para almacenar en la memoria.

Con un archivo temporal, con zsh, puedes hacerlo con:

seq 10 | zsh -c '{ cat =(sed \$w/dev/fd/3); } 3>&1'

Si en Linux, con basho con zshcualquier shell que use archivos temporales para documentos aquí, en realidad podría usar el archivo temporal creado por un documento aquí para almacenar la salida:

seq 10 | {
  chmod u+w /dev/fd/3 # only needed in bash5+
  cat > /dev/fd/3
  tail -n1 /dev/fd/3
  cat <&3
} 3<<EOF
EOF
Stéphane Chazelas
fuente
4
cat <<EOS | sed -ne '1{h;d;}' -e 'H;${G;p;}'
line 1
line 2
line 3
EOS

El problema con traducir esto a algo que usa tailes que tailnecesita leer todo el archivo para encontrar el final. Para usar eso en su tubería, necesita

  1. Proporcione el contenido completo del documento a tail.
  2. Proporcionar de nuevo a cat.
  3. En ese orden.

La parte difícil no es duplicar el contenido del documento (lo teehace) sino lograr que la salida tailsuceda antes de que salga el resto del documento, sin usar un archivo temporal intermedio.

El uso sed(o awk, como lo hace John1024 ) elimina el doble análisis de los datos y el problema de ordenar almacenando los datos en la memoria.

La sedsolución que propongo es

  1. 1{h;d;}, almacene la primera línea en el espacio de espera, tal cual, y salte a la siguiente línea.
  2. H, agregue entre sí la línea al espacio de espera con una nueva línea incrustada.
  3. ${G;p;}, agregue el espacio de espera a la última línea con una nueva línea incrustada e imprima los datos resultantes.

Esta es una traducción bastante literal de la solución de John1024 sed, con la advertencia de que el estándar POSIX solo garantiza que el espacio de retención sea de al menos 8192 bytes (8 KiB; pero recomienda que este búfer se asigne dinámicamente y se expanda según sea necesario, lo que tanto GNU sedy BSD lo sedestá haciendo).


Si te permites usar una tubería con nombre:

mkfifo mypipe
cat <<EOS | tee mypipe | cat <( tail -n 1 mypipe ) -
line 1
line 2
line 3
EOS
rm -f mypipe

Esto se utiliza teepara enviar los datos hacia abajo mypipey al mismo tiempo a cat. La catutilidad primero leerá la salida de tail(que lee mypipe, que teeestá escribiendo), y luego agregará la copia del documento que viene directamente tee.

Sin embargo, hay una falla grave en esto, ya que si el documento es demasiado grande (más grande que el tamaño del búfer de la tubería), teela escritura mypipey el catbloqueo se bloquearán mientras se espera que la tubería (sin nombre) se vacíe. No se vaciaría hasta que se catleyera. catno leería de él hasta que tailhubiera terminado. Y tailno terminaría hasta que teehubiera terminado. Esta es una situación clásica de punto muerto.

La variación

tee >( tail -n 1 >mypipe ) | cat mypipe -

tiene el mismo problema

Kusalananda
fuente
2
El seduno no funciona si la entrada tiene solo una línea (tal vez sed '1h;1!H;$!d;G'). También tenga en cuenta que varias sedimplementaciones tienen un límite bajo en el tamaño de su patrón y tienen espacio.
Stéphane Chazelas
La solución de tubería con nombre es el tipo de cosa que estaba buscando. La limitación es una pena. Entendí tu explicación, excepto "Y la cola no terminaría hasta que la camiseta hubiera terminado". ¿Podrías explicar por qué ese es el caso?
Jonás
2

Hay una herramienta nombrada peeen una colección de utilidades de línea de comandos generalmente empaquetadas con el nombre "moreutils" (o recuperables de su sitio web de origen ).

Si puede tenerlo en su sistema, entonces el equivalente para su ejemplo sería:

cat <<EOS | pee 'tail -1' cat 
line 1
line 2
line 3
EOS

El orden de los comandos ejecutados peees importante porque se ejecutan en la secuencia proporcionada.

LL3
fuente
1

Tratar:

cat <<EOS # | what goes here now? Nothing!
line 3
line 1
line 2
line 3
EOS

Dado que todo es información literal (un "documento aquí es"), y la diferencia entre este y la salida deseada es trivial, simplemente masajee esa información literal allí para que coincida con la salida.

Ahora supongamos que line 3proviene de algún lugar y se almacena en una variable llamada lastline:

cat <<EOS # | what goes here now? Nothing!
$lastline
line 1
line 2
$lastline
EOS

En un documento here, podemos generar texto sustituyendo variables. No solo eso, sino que podemos calcular el texto usando la sustitución de comandos:

cat <<EOS
this is template text
here we have a hex conversion: $(printf "%x" 42)
EOS

Podemos interpolar múltiples líneas:

cat <<EOS
multi line
preamble
$(for x in 3 1 2 3; do echo line $x ; done)
epilog
EOS

En general, evite el procesamiento de texto de la plantilla here doc; intenta generarlo usando código interpolado.

Kaz
fuente
1
Sinceramente, no puedo decir si esto es una broma o no. El cat <<EOS...en el OP fue solo un ejemplo de "capturar un archivo arbitrario", para hacer que la publicación sea específica y la pregunta sea clara. ¿No fue realmente obvio para usted, o simplemente pensó que sería inteligente interpretar la pregunta literalmente?
Jonás
@Jonah La pregunta dice claramente "[l] et's dice que nuestra fuente de entrada, en lugar de ser un archivo real, era stdin:". Nada sobre "archivos arbitrarios"; se trata de documentos aquí. Un documento aquí no es arbitrario. No es una entrada a su programa, sino una parte de su sintaxis que elige el programador.
Kaz
1
Creo que el contexto y las respuestas existentes dejaron en claro que ese era el caso, aunque solo sea porque su interpretación era correcta, literalmente tenía que suponer que ni yo ni ninguno de los otros carteles que respondimos nos dimos cuenta de que era posible copiar y pegar un línea de código Sin embargo, editaré la pregunta para que sea explícita.
Jonás
1
Kaz, gracias por la respuesta, pero ten en cuenta que incluso con tu edición, estás perdiendo la intención de la pregunta. Está recibiendo una entrada arbitraria de varias líneas a través de una tubería . No tienes idea de lo que será. Su tarea es generar la última línea de entrada, seguida de la entrada completa.
Jonás
1
Kaz, la entrada está ahí solo como un ejemplo. La mayoría de las personas, incluido yo mismo, considera útil tener un ejemplo de entrada real y salida esperada, en lugar de solo la pregunta abstracta. Eres el único que estaba confundido por esto.
Jonás
0

Si no te importa el pedido. Entonces esto funcionará cat lines | tee >(tail -1). Como han dicho otros. Debe leer el archivo dos veces, o guardar el archivo completo en el búfer, en el orden que solicitó.

ctrl-alt-delor
fuente