Obtener texto del último marcador a EOF en POSIX.2

8

Tengo un texto con líneas de marcador como:

aaa
---
bbb
---
ccc

Necesito obtener un texto del último marcador (no incluido) a EOF. En este caso será

ccc

¿Hay alguna manera elegante dentro de POSIX.2? En este momento uso dos carreras: primero con nly greppara la última aparición con el número de línea respectivo. Luego extraigo el número de línea y lo uso sedpara extraer el fragmento en cuestión.

Los segmentos de texto pueden ser bastante grandes, por lo que me da miedo usar algún método de agregar texto como si agregamos el texto a un búfer, si encontramos el marcador, vaciamos el búfer, de modo que en EOF tengamos nuestro último fragmento en el buffer.

aikipooh
fuente

Respuestas:

6

A menos que sus segmentos sean realmente enormes (como en: realmente no puede ahorrar tanta RAM, presumiblemente porque este es un pequeño sistema incrustado que controla un sistema de archivos grande), una sola pasada es realmente el mejor enfoque. No solo porque será más rápido, sino más importante porque permite que la fuente sea una secuencia, desde la cual se pierden los datos leídos y no guardados. Esto es realmente un trabajo para awk, aunque sed también puede hacerlo.

sed -n -e 's/^---$//' -e 't a' \
       -e 'H' -e '$g' -e '$s/^\n//' -e '$p' -e 'b' \
       -e ':a' -e 'h'              # you are not expected to understand this
awk '{if (/^---$/) {chunk=""}      # separator ==> start new chunk
      else {chunk=chunk $0 RS}}    # append line to chunk
     END {printf "%s", chunk}'     # print last chunk (without adding a newline)

Si debe utilizar un enfoque de dos pasos, determine el desplazamiento de línea del último separador e imprima desde ese. O determine el desplazamiento de bytes e imprima a partir de eso.

</input/file tail -n +$((1 + $(</input/file         # print the last N lines, where N=…
                               grep -n -e '---' |   # list separator line numbers
                               tail -n 1 |          # take the last one
                               cut -d ':' -f 1) ))  # retain only line number
</input/file tail -n +$(</input/file awk '/^---$/ {n=NR+1} END {print n}')
</input/file tail -c +$(</input/file LC_CTYPE=C awk '
    {pos+=length($0 RS)}        # pos contains the current byte offset in the file
    /^---$/ {last=pos}          # last contains the byte offset after the last separator
    END {print last+1}          # print characters from last (+1 because tail counts from 1)
')

Anexo: Si tiene más de POSIX, aquí hay una versión simple de un paso que se basa en una extensión común a awk que permite que el separador de registros RSsea ​​una expresión regular (POSIX solo permite un solo carácter). No es completamente correcto: si el archivo termina con un separador de registros, imprime el fragmento antes del último separador de registros en lugar de un registro vacío. La segunda versión que usa RTevita ese defecto, pero RTes específica de GNU awk.

awk -vRS='(^|\n)---+($|\n)' 'END{printf $0}'
gawk -vRS='(^|\n)---+($|\n)' 'END{if (RT == "") printf $0}'
Gilles 'SO- deja de ser malvado'
fuente
@Gilles: sedfunciona bien, pero no puedo awkejecutar el ejemplo; se cuelga ... y obtengo un error en el 3er ejemplo: cut -f ':' -t 1 ... corte: opción no válida - 't'
Peter.O
@ fred.bear: No tengo idea de cómo sucedió eso: probé todos mis fragmentos, pero de alguna manera estropeé la edición posterior a copiar y pegar en el cutejemplo. No veo nada malo con el awkejemplo, ¿qué versión de awk estás usando y cuál es tu entrada de prueba?
Gilles 'SO- deja de ser malvado'
... en realidad la awkversión está trabajando .. es sobresalen teniendo un muy largo tiempo en un archivo de gran tamaño .. la sedversión procesada el mismo archivo en 0.470s .. Mis datos de prueba es muy ponderada ... sólo dos trozos con un solitario '---' tres líneas desde el final de 1 millón de líneas ...
Peter.O
@Gilles .. (Creo que debería dejar de probar a las 3 AM. De alguna manera probé los tres awks de "dos pasadas" como una sola unidad :( ... Ahora he probado cada uno individualmente y el segundo es muy rápido en 0,204 segundos ... Howerver, el primer awk de "dos pasos" solo emite: " (entrada estándar) " (el -l parece ser el culpable) ... en cuanto al tercer awk de "dos pasos", no no muestre nada ... pero el segundo "dos pasos" es el más rápido de todos los métodos presentados (POSIX o no
:)
@ fred.bear: fijo y fijo. Mi control de calidad no es muy bueno para estos fragmentos cortos: normalmente copio y pego desde una línea de comandos, formateo, luego noto un error e intento corregirlo en línea en lugar de reformatearlo. Tengo curiosidad por ver si el recuento de caracteres es más eficiente que el recuento de líneas (métodos de dos pasos de segundo frente a tercero).
Gilles 'SO- deja de ser malvado'
3

Una estrategia de dos pases parece ser lo correcto. En lugar de sed lo usaría awk(1). Los dos pases podrían verse así:

$ LINE=`awk '/^---$/{n=NR}END{print n}' file`

para obtener el número de línea. Y luego repita todo el texto que comienza desde ese número de línea con:

$ awk "NR>$LINE" file

Esto no debería requerir un almacenamiento en búfer excesivo.

Mackie Messer
fuente
y luego se pueden combinar:awk -v line=$(awk '/^---$/{n=NR}END{print n}' file) 'NR>line' file
Glenn Jackman
Al ver que he estado probando las otras presentaciones, ahora también he probado el fragmento de arriba de "Glen Jackman". Tarda 0.352 segundos (con el mismo archivo de datos mencionado en mi respuesta) ... Estoy empezando a recibir el mensaje de que awk puede ser más rápido de lo que originalmente creía posible (pensé que sed era tan bueno como lo era, pero parece ser un caso de "caballos de carreras") ...
Peter.O
Muy interesante ver todos estos scripts comparados. Buen trabajo Fred.
Mackie Messer
Las soluciones más rápidas usan tac y tail que en realidad leen el archivo de entrada al revés. Ahora bien, si sólo se podían leer los awk revés de archivo de entrada ...
Mackie Messer
3
lnum=$(($(sed -n '/^---$/=' file | sed '$!d') +1)); sed -n "${lnum},$ p" file 

Los primeros sednúmeros de línea de salida de las líneas "---" ...
El segundo sedextrae el último número de la salida del primer sed ...
Agregue 1 a ese número para obtener el inicio de su bloque "ccc" ...
El tercero salidas 'sed' desde el inicio del bloque "ccc" a EOF

Actualización (con información modificada sobre los métodos de Gilles)

Bueno, me preguntaba cómo se desempeñaría Glenn Jackman tac , así que probé las tres respuestas (al momento de escribir) ... El archivo de prueba contenía 1 millón de líneas (de sus propios números de línea).
Todas las respuestas hicieron lo que se esperaba ...

Aquí están los tiempos ...


Gilles sed (pase único)

# real    0m0.470s
# user    0m0.448s
# sys     0m0.020s

Gilles awk (pase único)

# very slow, but my data had a very large data block which awk needed to cache.

Gilles 'dos pasos' (primer método)

# real    0m0.048s
# user    0m0.052s
# sys     0m0.008s

Gilles 'dos pasos' (segundo método) ... muy rápido

# real    0m0.204s
# user    0m0.196s
# sys     0m0.008s

Gilles 'dos pasos' (tercer método)

# real    0m0.774s
# user    0m0.688s
# sys     0m0.012s

Gilles 'gawk' (método RT) ... muy rápido , pero no es POSIX.

# real    0m0.221s
# user    0m0.200s
# sys     0m0.020s

Glenn Jackman ... muy rápido , pero no es POSIX.

# real    0m0.022s
# user    0m0.000s
# sys     0m0.036s

fred.bear

# real    0m0.464s
# user    0m0.432s
# sys     0m0.052s

Mackie Messer

# real    0m0.856s
# user    0m0.832s
# sys     0m0.028s
Peter.O
fuente
Por curiosidad, ¿cuál de mis dos versiones de prueba probaste y qué versión de awk usaste?
Gilles 'SO- deja de ser malvado'
@Gilles: utilicé GNU Awk 3.1.6 (en Ubuntu 10.04 con 4 GB de RAM). Todas las pruebas tienen 1 millón de líneas en el primer "fragmento", luego un "marcador" seguido de 2 líneas de "datos" ... Tomó 15.540 segundos procesar un archivo más pequeño de 100,000 líneas, pero para las 1,000,000 de líneas, estoy ejecutándolo ahora, y han pasado más de 25 minutos hasta ahora. Está usando un núcleo al 100% ... eliminándolo ahora ... Aquí hay algunas pruebas incrementales más: líneas = 100000 (0m16.026s) - líneas = 200000 (2m29.990s) - líneas = 300000 (5m23. 393s) - líneas = 400000 (11m9.938s)
Peter.O
Vaya ... En mi comentario anterior, me perdí tu referencia awk de "dos pasadas". El detalle anterior es para el awk de "un solo paso" ... La versión awk es correcta ... He hecho más comentarios sobre las diferentes versiones de "dos pases" en su respuesta (y modificó los resultados de tiempo anteriores)
Peter.O
2

Utilice " tac " que genera las líneas de un archivo de principio a fin:

tac afile | awk '/---/ {exit} {print}' | tac
Glenn Jackman
fuente
tacno es POSIX, es específico de Linux (está en GNU coreutils y en algunas instalaciones de busybox).
Gilles 'SO- deja de ser malvado'
0

Podrías usar ed

ed -s infile <<\IN
.t.
1,?===?d
$d
,p
q
IN

Cómo funciona: tduplica la línea actual ( .), que siempre es la última línea cuando se edinicia (en caso de que el delimitador esté presente en la última línea), 1,?===?delimina todas las líneas hasta la coincidencia anterior incluida ( edaún está en la última línea) ) luego $delimina la última línea (duplicada), ,pimprime el búfer de texto (reemplace con wpara editar el archivo en su lugar) y finalmente se qcierra ed.


Si sabe que hay al menos un delimitador en la entrada (y no me importa si también está impreso), entonces

sed 'H;/===/h;$!d;x' infile

Sería el más corto.
Cómo funciona: agrega todas las líneas al Hbúfer antiguo, sobrescribe el hbúfer antiguo cuando encuentra una coincidencia, delige todas las líneas excepto la última $cuando xcambia los búferes (y las impresiones automáticas).

don_crissti
fuente