¿Usar ex-comando para verificar si dos líneas son idénticas?

9

Estaba mirando esta pregunta y luego me preguntaba cómo podría implementar mi respuesta, que usa sed puramente POSIX ex .

El truco es que, si sedbien puedo comparar el espacio de espera con el espacio del patrón para ver si son exactamente equivalentes (con G;/^\(.*\)\n\1$/{do something}), no conozco ninguna forma de hacer tal prueba ex.

Sé que en Vim podría Ymarcar la primera línea y luego escribir :2,$g/<C-r>0/dpara hacer casi lo que estoy especificando, pero si la primera línea contiene algo que no sea un texto alfanumérico muy directo, esto se convierte realmente en un cambio, ya que la línea se está volcando como una expresión regular. , no solo una cadena para comparar. (¡Y si la primera línea contiene una barra diagonal, el resto de la línea se interpretará como un comando!)

Entonces, si deseo eliminar todas las líneas myfileque son idénticas a la primera línea, pero no eliminar la primera línea, ¿cómo podría hacerlo usando ex? Para el caso, ¿cómo podría hacerlo usando vi?

¿Hay alguna forma POSIX de eliminar una línea si coincide exactamente con otra línea?

Quizás algo como esta sintaxis imaginaria:

:2,$g/**lines equal to "0**/d
Comodín
fuente
3
Podría construir el comando, pero necesitaría un poco de vimscript y probablemente no sería una forma POSIX::execute '2,$g/\V' . escape(getline(1), '\') . '/d'
saginaw
1
@saginaw, gracias. Hasta ahora, el único enfoque POSIX que se me ha ocurrido es usarlo sedcomo un filtro desde adentro exy ejecutar toda mi sedrespuesta en todo el búfer ... lo que funcionaría, por supuesto (y en realidad es portátil a diferencia sed -i).
Comodín el
Tienes razón y creo que tu enfoque inicial es <C-r>0muy bueno. No estoy seguro de que pueda hacerlo mejor solo con los comandos Ex porque debe proteger los caracteres especiales. Sin la restricción compatible con POSIX, creo que usaría el interruptor muy nomagic \Vy luego protegería la barra invertida (porque mantiene su significado especial incluso con \V) con la escape()función cuyo segundo argumento es una cadena que contiene todos los caracteres que desea escapar / proteger .
saginaw
Sin embargo, en el comando anterior también olvidé proteger la barra diagonal, porque también tiene un significado especial para el comando global, es el delimitador de patrón. Entonces, el comando correcto probablemente sería algo así como: :execute '2,$g/\V' . escape(getline(1), '\/') . '/d'O podría usar otro carácter para el delimitador de patrón como un punto y coma. En este caso, no necesitaría proteger una barra diagonal en el patrón. Daría algo como::execute '2,$g;\V' . escape(getline(1), '\') . ';d'
saginaw
1
Encuentro su segundo acercamiento con sedtambién muy bueno. Con Vim, a menudo delega ciertas tareas especiales a otros programas, y sedprobablemente sea un buen ejemplo de eso. Por cierto, no tiene que ejecutar sedtodo su búfer. Si desea ejecutarlo solo en una parte del búfer, puede asignar un rango. Por ejemplo, si desea filtrar sólo las líneas entre 50 y 100, podría escribir: :50,100!<your sed command>.
saginaw

Respuestas:

3

Empuje

En Vim puedes combinar cualquier personaje, incluida la nueva línea con \_.. Puede usar esto para construir un patrón que coincida con una línea completa, cualquier cantidad de cosas y luego esa misma línea:

/\(^.*$\)\_.*\n\1$/

Ahora desea eliminar todas las líneas de un archivo que coincidan con la primera, sin incluir la primera. La sustitución para eliminar la última línea que coincide con la primera es:

:1 s/\(^.*$\)\_.*\zs\n\1$//

Puede usar :globalpara asegurarse de que la sustitución se repita suficientes veces para eliminar todas las líneas:

:g/^/ 1s/\(^.*$\)\_.*\zs\n\1$//

POSIX ex

@saginaw muestra una forma más ordenada de hacer esto en Vim en un comentario a su pregunta, pero podemos adaptar la técnica anterior para POSIX ex.

Para hacer esto de una manera compatible con POSIX, debe rechazar la coincidencia de varias líneas, pero aún puede hacer uso de referencias posteriores. Esto requiere un poco de trabajo extra:

:g/^/ t- | s/^/@@@/ | 1t- | s/^/"/ | j! | s/^"\(.*\)@@@\1$/d/ | d x | @x

Aquí está el desglose:

:g/^/                   for each line

t- |                    copy it above

s/^/@@@/ |              prefix it with something unique (@@@)
                        (do a search in the buffer first to make
                        sure it really is unique)

1t- |                   copy the first line above this one

s/^/"/ |                prefix with "

j! |                    join those two lines (no spaces)

s/^"\(.*\)@@@\1$/d/ |   if the part after the " and before the @@@
                        matches the part after the @@@, replace the line
                        with d

d x |                   delete the line into register x

@x                      execute it

Entonces, si la línea actual es un duplicado de la línea 1, el registro x contendrá d. Al ejecutarlo se eliminará la línea actual. Si no es un duplicado, contendrá un sinsentido con el prefijo "que, cuando se ejecuta, es un no-op, ya que " comienza un comentario. No sé si esta es la mejor manera de lograr esto, ¡es solo la primera que se me ocurrió!

Sucede que la primera línea no se puede eliminar porque el proceso de copia cambia temporalmente lo que es la línea 1. Si este no hubiera sido el caso, podría prefijarlo :gcon un 2,$rango.

Probado en Vim y ex-vi versión 4.0.

EDITAR

Y una forma más simple, que escapa de caracteres especiales para crear un patrón de búsqueda (con 'nomagic'set), crea un :globalcomando y luego lo ejecuta:

:set nomagic
:1t1 | .g/^/ s#\[$^\/]#\\\&#g | s#\.\*#2,$g/^\&$/d# | d x
:@x
:set magic

Sin embargo, no puede hacer esto como una sola línea, ya que tendría un anidado :global, que no está permitido.

Antonio
fuente
2

Parece que la única forma POSIX de hacer esto es usar un filtro externo, como sed.

Por ejemplo, para eliminar la línea 17 de su archivo solo si es exactamente idéntica a la línea 5 y dejarla sin cambios, puede hacer lo siguiente:

:1,17!sed '5h;17{G;/^\(.*\)\n\1$/d;s/\n.*$//;}'

(Puede ejecutar sedtodo el búfer aquí, o puede ejecutarlo solo en las líneas 5-17, pero en el primer caso está haciendo un filtrado innecesario, no es gran cosa, y en el último caso tendría que usar el números 1 y 13 en su sedcomando en lugar de 5 y 17. Confuso).

Dado que sedsolo hace un solo pase hacia adelante, no hay una manera fácil de hacer lo contrario y eliminar la quinta línea solo si es idéntica a la 17a línea. Lo intenté por un momento como punto de curiosidad ... es complicado .


Avance : puede hacerlo así:

:17t 5
:5,5+!sed '1N;/^\(.*\)\n\1$/d;s/\n.*$//'

Este es en realidad el método más general. También se puede usar para dar el mismo resultado que el primer comando (y eliminar la línea 17 solo si es idéntica a la línea 5) de la siguiente manera:

:5t 17
:17,17+!sed '1N;/^\(.*\)\n\1$/d;s/\n.*$//'

Para usos más amplios, como eliminar todas las líneas del archivo que son idénticas a la línea 37, y dejar la línea 37 intacta, puede hacer lo siguiente:

:37,$!sed '1{h;n;};G;/^\(.*\)\n\1$/d;s/\n.*$//'
:37t 0
:1,37!sed '1{h;d;};G;/^\(.*\)\n\1$/d;s/\n.*$//'

La conclusión aquí es, para verificar si dos líneas son idénticas, la mejor herramienta es sed no ex. Pero como DevSolar aludió en un comentario , esto no es un fracaso vio exestán diseñados para funcionar con herramientas Unix; Esa es una gran fortaleza.

Comodín
fuente
Mucho, mucho más difícil es: insertar una línea al final de un archivo, solo si la línea no existe en algún lugar del archivo.
Comodín el
Eso debería ser factible con un enfoque similar a mi respuesta. ¡Sin embargo, no creo que sea una frase!
Antony