Excluir todas as duplicatas consecutivas


13

Eu tenho um arquivo que se parece com isso.

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

Eu gostaria que fosse assim:

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

Tenho certeza de que deve haver uma maneira de o vim fazer isso rapidamente, mas não consigo entender como. Isso está além do poder das macros e precisa do vimscript?

Além disso, tudo bem se eu tiver que aplicar a mesma macro a cada bloco de "Retenções". Não precisa ser uma única macro que obtenha o arquivo inteiro, embora isso seja incrível.

Respostas:


13

Eu acho que o seguinte comando deve funcionar:

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

Explicação:

Usamos o comando de substituição em todo o arquivo para mudar patternpara string:

:%s/pattern/string/

Aqui patternestá ^\(.*\)\(\n\1\)\+$e stringé \1.

pattern pode ser dividido assim:

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

^e $correspondem, respectivamente, ao início da linha e ao final da linha.

\(e \)são usados ​​para incluir, subpattern1para que possamos nos referir a ele mais tarde pelo número especial \1.
Eles também são usados ​​para delimitar, subpattern2para que possamos repeti-lo 1 ou mais vezes com o quantificador \+.

subpattern1is .*
.é um metacaractere que corresponde a qualquer caractere, exceto a nova linha, e *é um quantificador que corresponde ao último caractere 0, 1 ou mais vezes.
Portanto, .*corresponde a qualquer texto que não contenha nova linha.

subpattern2é \n\1
\ncorresponde a uma nova linha e \1corresponde ao mesmo texto que foi correspondido dentro da primeira \(, \)que aqui é subpattern1.

Então, patternpode ser lida assim:
um início de linha ( ^) seguido por qualquer texto contendo nenhuma linha nova ( .*) seguido por uma nova linha ( \n), em seguida, o mesmo texto ( \1), os dois últimos sendo repetido uma ou mais vezes ( \+), e finalmente um fim de linha ( $) .

Onde quer que patternseja correspondido (um bloco de linhas idênticas), o comando de substituição o substitui pelo stringqual está aqui \1(a primeira linha do bloco).

Se você deseja ver quais blocos de linhas serão afetados sem alterar nada no seu arquivo, é possível ativar a hlsearchopção e adicionar o nsinalizador de substituição no final do comando:

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

Para um controle mais granular, você também pode solicitar uma confirmação antes de alterar cada bloco de linhas adicionando o csinalizador de substituição:

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

Para obter mais informações sobre o comando de substituição :help :s, leia ,
para os sinalizadores de substituição :help s_flags,
para os vários metacaracteres e quantificadores :help pattern-atoms,
e para expressões regulares no vim, leia isso .

Editar: o curinga corrigiu um problema no comando adicionando um $no final de pattern.

Também o BloodGain possui uma versão mais curta e legível do mesmo comando.


1
Agradável; seu comando precisa $disso, no entanto. Caso contrário, ele fará coisas inesperadas com uma linha que começa com texto idêntico à linha anterior, mas possui outros caracteres à direita. Observe também que o comando básico que você deu é funcionalmente equivalente à minha resposta :%!uniq, mas os sinalizadores de destaque e confirmação são bons.
Wildcard

Você está certo, acabei de verificar e se uma das linhas duplicadas contiver um caractere final diferente, o comando não se comportará como o esperado. Não sei como consertar, o átomo \ncorresponde ao final da linha e deve evitar isso, mas não. Eu tentei adicionar um $pouco depois .*sem sucesso. Vou tentar corrigi-lo, mas se não puder, talvez eu exclua minha resposta ou adicione um aviso no final. Obrigado por apontar este problema.
Saginaw

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

1
Você deve considerar que $corresponde ao final da string , não ao final da linha. Tecnicamente, isso não é verdade - mas quando você coloca caracteres após outras exceções, ele corresponde a um literal em $vez de qualquer coisa especial. Portanto, usar \né melhor para correspondências com várias linhas. (Veja :help /$)
Curinga

Eu acho que você está certo em que \npode ser usado em qualquer lugar dentro do regex, enquanto $provavelmente deve ser usado apenas no final. Apenas para fazer a diferença entre os dois, editei a resposta escrevendo que \ncorresponde a uma nova linha (o que instintivamente faz você pensar que ainda há algum texto depois) enquanto $corresponde a um final de linha (o que faz você pensar que não há nada esquerda).
Saginaw

10

Tente o seguinte:

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

Como na resposta de saginaw , isso usa o comando substituto de Vim. No entanto, ele aproveita alguns recursos extras para melhorar a legibilidade:

  1. O Vim nos permite usar qualquer caractere ASCII não alfanumérico, exceto barra invertida ( \ ), aspas duplas ( " ) ou pipe ( | ) para dividir nosso texto de correspondência / substituição / sinalizadores. Aqui, selecionei ponto e vírgula ( ; ), mas você pode escolha outro.
  2. O Vim fornece configurações "mágicas" para expressões regulares, para que os caracteres sejam interpretados por seus significados especiais, em vez de exigir uma fuga de barra invertida. Isso é útil para reduzir a verbosidade e porque é mais consistente que o padrão "nomagic". Começando com \vsignifica "muito mágico", ou todos os caracteres, exceto alfanuméricos ( A-z0-9 ) e sublinhado ( _ ) têm um significado especial.

O significado dos componentes são:

% para o arquivo inteiro

s substituto

; começar a sequência substituta

\ v "muito mágico"

^ início da linha

(. *) 0 ou mais de qualquer caractere (grupo 1)

(\ n \ 1) + nova linha seguida por (texto de correspondência do grupo 1), 1 ou mais vezes (grupo 2)

$ fim de linha (ou, neste caso, pense que o próximo caractere deve ser uma nova linha )

; começar a substituir string

\ 1 texto de correspondência do grupo 1

; sinalizadores de fim de comando ou início


1
Eu realmente gosto da sua resposta, porque é mais legível, mas também porque me fez entender melhor a diferença entre \ne $. \nadiciona algo ao padrão: a nova linha de caractere que informa ao vim que o texto a seguir está em uma nova linha. Considerando $que não adiciona nada ao padrão, simplesmente proíbe uma correspondência se o próximo caractere fora do padrão não for uma nova linha. Pelo menos, é o que eu entendi lendo sua resposta e :help zero-width.
Saginaw

E o mesmo deve ser verdade para ^, ele não acrescenta nada ao padrão, ele simplesmente impede uma partida a ser feita se o exterior caractere anterior do padrão não é uma nova linha ...
Saginaw

@saginaw Você está certo, e essa é uma boa explicação. Em expressões regulares, alguns caracteres podem ser considerados caracteres de controle . Por exemplo, +significa "repita a expressão anterior (caractere ou grupo) 1 ou mais vezes", mas não corresponde a nada. Os ^meios "não podem começar no meio da cadeia" e os $meios "não podem terminar no meio da cadeia". Observe que eu não disse "linha", mas "corda" lá. O Vim trata cada linha como uma string por padrão - e é aí que \nentra. Diz ao Vim para consumir uma nova linha para tentar fazer essa correspondência.
Bloodgain 6/11/15

8

Se você deseja remover TODAS as linhas idênticas adjacentes, não apenas Hold, você pode fazê-lo extremamente facilmente com um filtro externo de dentro vim:

:%!uniq (em um ambiente Unix).

Se você quiser fazer isso diretamente vim, é realmente muito complicado. Eu acho que existe uma maneira, mas para o caso geral é muito complicado torná-lo 100% funcional e ainda não resolvi todos os bugs.

No entanto, neste caso específico , como você pode ver visualmente que a próxima linha que não é duplicada não começa com o mesmo caractere, você pode usar:

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

O +significa a linha após a linha atual. O . refere-se à linha atual. O /^[^H]/-significa que a linha antes de ( -) a próxima linha que não começa com H.

Então d é excluir.


3
Enquanto os comandos substitutos e globais do Vim são bons exercícios, chamar uniq(de dentro do vim ou usando o shell) é como eu resolveria isso. Por um lado, tenho certeza de uniqque manipulará linhas que estão em branco / todos os espaços como equivalentes (não o testaram), mas isso seria muito mais difícil de capturar com um regex. Isso também significa não "reinventar a roda" enquanto estou tentando fazer o trabalho.
Bloodgain 6/11

2
A capacidade de alimentar texto através de ferramentas externas é o motivo pelo qual eu normalmente recomendo o Vim e o Cygwin no Windows. Vim e shell simplesmente pertencem um ao outro.
DevSolar

2

Uma resposta baseada no Vim:

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

= Substitua cada linha seguida por ela mesma pelo menos uma vez , pela mesma linha.


2

Mais um, assumindo o Vim 7.4.218 ou 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>)

Isso não é necessariamente melhor que as outras soluções.


2

Aqui está uma solução baseada em um antigo (2003) vim (golf) de Preben Gulberg e Piet Delport.

  • Suas raízes estão em %g/^\v(.*)\n\1$/d
  • Ao contrário das outras soluções, ele foi encapsulado em uma função e, portanto, não modifica o registro de pesquisa nem o registro sem nome.
  • E também foi encapsulado em um comando para simplificar seu uso:
    • :Uniq(equivalente a :%Uniq),
    • :1,Uniq (desde o início do buffer até a linha atual),
    • visualmente selecione linhas + hit :Uniq<cr>(expandido pelo vim into :'<,'>Uniq)
    • etc ( :h range)

Aqui está o 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: suas primeiras tentativas foram:

" 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'
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.