Eliminar todos los duplicados consecutivos

13

Tengo un archivo que se ve así.

Move to 230.00
Hold
Hold
Hold
Hold
Hold
Hold
Move to 00.00
Hold 
Hold 
Hold 
Hold 
Hold 
FooBar
Hold 
Spam
Hold

Me gustaría que se vea así:

Move to 230.00
Hold
Move to 00.00
Hold 
FooBar
Hold
Spam
Hold

Estoy seguro de que debe haber una forma en que vim pueda hacer esto rápidamente, pero no puedo entender cómo. ¿Está esto más allá del poder de las macros y necesita vimscript?

Además, está bien si tengo que aplicar la misma macro a cada bloque de "Retenciones". No tiene que ser una sola macro que obtenga todo el archivo, aunque eso sería increíble.

James
fuente

Respuestas:

13

Creo que el siguiente comando debería funcionar:

 :%s/^\(.*\)\(\n\1\)\+$/\1/

Explicación

Usamos el comando de sustitución en todo el archivo para cambiar patterna string:

:%s/pattern/string/

Aquí patternestá ^\(.*\)\(\n\1\)\+$y stringestá \1.

pattern puede desglosarse así:

^\(subpattern1\)\(subpattern2\)\+$

^y $coinciden respectivamente un principio de línea y un final de línea.

\(y \)se usan para encerrar subpattern1para que podamos referirnos más tarde por el número especial \1.
También se usan para encerrar subpattern2para que podamos repetirlo 1 o más veces con el cuantificador\+ .

subpattern1Este es .*
.un metacarácter que coincide con cualquier carácter, excepto una nueva línea, y *es un cuantificador que coincide con el último carácter 0, 1 o más veces.
Por lo tanto, .*coincide con cualquier texto que no contenga una nueva línea.

subpattern2es \n\1
\ncoincide con una nueva línea y \1coincide con el mismo texto que coincidió dentro de la primera \(, \)que aquí estásubpattern1 .

Por patternlo tanto, puede leerse así:
un comienzo de línea ( ^) seguido de cualquier texto que no contenga una nueva línea ( .*) seguido de una nueva línea ( \n), luego el mismo texto ( \1), repitiéndose los dos últimos una o más veces ( \+), y finalmente un final de línea ( $) .

Dondequiera patternque coincida (un bloque de líneas idénticas), el comando de sustitución lo reemplaza con el stringque aquí está\1 (la primera línea del bloque).

Si desea ver qué bloques de líneas se verán afectados sin cambiar nada en su archivo, puede habilitar la hlsearchopción y agregar el nindicador de sustitución al final del comando:

:%s/^\(.*\)\(\n\1\)\+$/\1/n

Para un control más granular, también puede solicitar una confirmación antes de cambiar cada bloque de líneas agregando el cindicador de sustitución

:%s/^\(.*\)\(\n\1\)\+$/\1/c

Para obtener más información sobre la lectura comando de sustitución :help :s,
para la sustitución de las banderas :help s_flags,
para los diversos metacaracteres y cuantificadores leen :help pattern-atoms,
y expresiones regulares en vim leen este .

Editar: Comodín solucionó un problema en el comando agregando un $al final de pattern.

También BloodGain tiene una versión más corta y más legible del mismo comando.

Saginaw
fuente
1
Agradable; Sin $embargo, su comando necesita un en él. De lo contrario, hará cosas inesperadas con una línea que comienza con un texto idéntico a la línea anterior, pero tiene algunos otros caracteres finales. También tenga en cuenta que el comando básico que proporcionó es funcionalmente equivalente a mi respuesta de :%!uniq, pero los indicadores de resaltado y confirmación son agradables.
Comodín el
Tienes razón, acabo de comprobar y si una de las líneas duplicadas contiene un carácter final diferente, el comando no se comporta como se esperaba. No sé cómo solucionarlo, el átomo \ncoincide con un final de línea y debería evitarlo, pero no lo hace. Traté de agregar un $justo después .*sin éxito. Voy a intentar solucionarlo, pero si no puedo, tal vez elimine mi respuesta o agregue una advertencia al final. Gracias por señalar este problema.
Saginaw
1
Prueba:%s/^\(.*\)\(\n\1\)\+$/\1/
comodín, el
1
Debe considerar que $coincide con el final de la cadena , no con el final de la línea. Esto técnicamente no es cierto, pero cuando coloca caracteres después de él, salvo algunas excepciones, coincide con un literal en $lugar de algo especial. Por lo tanto, usar \nes mejor para coincidencias de varias líneas. (Ver :help /$)
Comodín el
Creo que tiene razón en que \npuede usarse en cualquier lugar dentro de la expresión regular, mientras $que probablemente debería usarse solo al final. Solo para hacer una diferencia entre los dos, he editado la respuesta escribiendo que \ncoincide con una nueva línea (que instintivamente te hace pensar que todavía hay algo de texto después) mientras que $coincide con un final de línea (que te hace pensar que no hay nada izquierda).
Saginaw
10

Intenta lo siguiente:

:%s;\v^(.*)(\n\1)+$;\1;

Al igual que con la respuesta de saginaw , esta usa el comando Vim: sustituto. Sin embargo, aprovecha algunas características adicionales para mejorar la legibilidad:

  1. Vim nos permite usar cualquier carácter ASCII no alfanumérico, excepto la barra diagonal inversa ( \ ), la comilla doble ( " ) o la barra vertical ( | ) para dividir nuestro texto de coincidencia / reemplazo / banderas. Aquí, seleccioné punto y coma ( ; ), pero puede elige otro.
  2. Vim proporciona configuraciones "mágicas" para expresiones regulares, de modo que los caracteres se interpretan por sus significados especiales en lugar de requerir un escape de barra invertida. Esto es útil para reducir la verbosidad y porque es más consistente que el valor predeterminado "nomagic". Comenzar con \vsignifica "muy mágico", o todos los caracteres, excepto los alfanuméricos ( A-z0-9 ) y el guión bajo ( _ ) tienen un significado especial.

El significado de los componentes son:

% para todo el archivo

S sustituto

; comenzar cadena de sustitución

\ v "muy mágico"

^ principio de línea

(. *) 0 o más de cualquier carácter (grupo 1)

(\ n \ 1) + nueva línea seguida de (texto de coincidencia del grupo 1), 1 o más veces (grupo 2)

$ final de línea (o en este caso, piensa que el siguiente carácter debe ser una nueva línea )

; comenzar a reemplazar la cadena

\ 1 grupo 1 texto de coincidencia

; fin del comando o inicio de banderas

Ganancia de sangre
fuente
1
Realmente me gusta tu respuesta, porque es más legible pero también porque me hizo comprender mejor la diferencia entre \ny $. \nagrega algo al patrón: la nueva línea de caracteres que le dice a vim que el siguiente texto está en una nueva línea. Mientras $que no agrega nada al patrón, simplemente prohíbe que se haga una coincidencia si el siguiente carácter fuera del patrón no es una nueva línea. Al menos, es lo que he entendido al leer su respuesta y :help zero-width.
saginaw
Y lo mismo debe ser cierto para ^, no agrega nada al patrón, solo evita que se haga una coincidencia si el carácter anterior fuera del patrón no es una nueva línea ...
saginaw
@saginaw Lo tienes exactamente bien, y esa es una buena explicación. En las expresiones regulares, algunos caracteres pueden considerarse como caracteres de control . Por ejemplo, +significa "repetir la expresión anterior (carácter o grupo) 1 o más veces", pero no coincide con nada en sí mismo. El ^medio "no puede comenzar en el medio de la cadena" y $significa "no puede terminar en el medio de la cadena". Observe que no dije "línea", sino "cadena" allí. Vim trata cada línea como una cadena de forma predeterminada, y ahí es donde \nentra. Le dice a Vim que consuma una nueva línea para intentar hacer que esta coincidencia.
Bloodgain
8

Si desea eliminar TODAS las líneas idénticas adyacentes, no solo Hold, puede hacerlo extremadamente fácilmente con un filtro externo desde dentro vim:

:%!uniq (en un entorno Unix).

Si quieres hacerlo directamente en vim , en realidad es muy complicado. Creo que hay una manera, pero para el caso general es muy complicado hacerlo 100% funcional y aún no he resuelto todos los errores.

Sin embargo, para este caso específico , dado que puede ver visualmente que la siguiente línea que no está duplicada no comienza con el mismo carácter, puede usar:

:+,./^[^H]/-d

El +significa la línea después de la línea actual. Los . se refiere a la línea actual. El /^[^H]/-significa la línea anterior (- ) la siguiente línea que no se inicia con H.

Entonces d es eliminar.

Comodín
fuente
3
Si bien los comandos Vim sustitutos y globales son buenos ejercicios, llamar a ellos uniq(ya sea desde vim o usando el shell) es cómo resolvería esto. Por un lado, estoy bastante seguro de uniqque manejará líneas en blanco / todos los espacios como equivalentes (no lo probé), pero eso sería mucho más difícil de capturar con una expresión regular. También significa no "reinventar la rueda" mientras trato de hacer el trabajo.
Bloodgain
2
La capacidad de alimentar texto a través de herramientas externas es la razón por la que generalmente recomiendo Vim y Cygwin en Windows. Vim y Shell simplemente pertenecen juntos.
DevSolar
2

Una respuesta basada en Vim:

:%s/\(^.*\n\)\1\{1,}/\1

= Reemplace cada línea seguida por sí misma al menos una vez , con esa misma línea.

VanLaser
fuente
2

Uno más, suponiendo Vim 7.4.218 o posterior:

function! s:Uniq(line1, line2)
    let cursor = getcurpos()
    let lines = uniq(getline(a:line1, a:line2))
    if setline(a:line1, lines) == 0 && len(lines) <= a:line2 - a:line1
        silent execute (a:line1 + len(lines)) . ',' . a:line2 . 'd _'
    endif
    call setpos('.', cursor)
endfunction

command! -range=% Uniq call <SID>Uniq(<line1>, <line2>)

Sin embargo, esto no es necesariamente mejor que las otras soluciones.

Sato Katsura
fuente
2

Aquí hay una solución basada en un viejo (2003) vim (golf) de Preben Gulberg y Piet Delport.

  • Sus raíces yacen en %g/^\v(.*)\n\1$/d
  • A diferencia de las otras soluciones, se ha encapsulado en una función, por lo que no modifica el registro de búsqueda ni el registro sin nombre.
  • Y también se ha encapsulado en un comando para simplificar su uso:
    • :Uniq(equivalente a :%Uniq),
    • :1,Uniq (desde el inicio del búfer hasta la línea actual),
    • seleccionar visualmente líneas + golpe :Uniq<cr>(expandido por vim en:'<,'>Uniq )
    • etc ( :h range)

Aquí está el código:

command! -range=% -nargs=0 Uniq <line1>,<line2>call s:EmuleUniq()

function! s:EmuleUniq() range
  let l1 = a:firstline
  let l2 = a:lastline
  if l1 < l2
    " Note the "-" to avoid spilling over the end of the range
    " Note also the use of ":delete", along with the black hole register "_"
    silent exe l1.','l2.'-g/^\(.*\)\n\1$/d _'

    call histdel('search', -1)          " necessary
    " let @/ = histget('search', -1)    " useless within a function
  endif
endfunction

Nota: sus primeros intentos fueron:

" Version1 from: Preben 'Peppe' Guldberg <peppe {at} xs4all {dot} nl>
" silent exe l1 . ',' . (l2 - 1) . 's/^\(.*\)\%(\n\%<' . (l2 + 1)
      " \ . 'l\1$\)\+/\1/e'

" Version from: Piet Delport <pjd {at} 303.za {dot} net>
" silent exe l1.','l2.'g/^\%<'.l2.'l\(.*\)\n\1$/d'
Luc Hermitte
fuente