Use ex-command para verificar se duas linhas são idênticas?

9

Eu estava olhando para essa pergunta e depois me perguntei como poderia implementar minha resposta que usa sed usando puramente POSIX ex .

O truque é que, enquanto sedeu posso comparar o espaço de espera com o espaço do padrão para ver se eles são exatamente equivalentes (com G;/^\(.*\)\n\1$/{do something}), não conheço nenhuma maneira de fazer esse teste ex.

Eu sei que no Vim eu poderia Ycriar a primeira linha e digitar :2,$g/<C-r>0/dpara quase fazer o que estou especificando - mas se a primeira linha contiver algo além de um texto alfanumérico muito simples, isso se tornará arriscado, pois a linha está sendo inserida como uma expressão regular , não apenas uma sequência para comparação. (E se a primeira linha contiver uma barra, o restante da linha será interpretado como um comando!)

Portanto, se eu quiser excluir todas as linhas myfileidênticas à primeira linha - mas não excluir a primeira linha - como eu poderia fazer isso usando ex? Por falar nisso, como eu poderia fazer isso usando vi?

Existe uma maneira POSIX de excluir uma linha se ela corresponder exatamente a outra linha?

Talvez algo como esta sintaxe imaginária:

:2,$g/**lines equal to "0**/d
Curinga
fonte
3
Você poderia construir o comando, mas seria necessário um pouco de vimscript e provavelmente não seria uma maneira POSIX::execute '2,$g/\V' . escape(getline(1), '\') . '/d'
Saginaw
11
@saginaw, obrigado. Até agora a única POSIX abordagem que me ocorreu é usar apenas sedcomo um filtro de dentro ex, e executar toda a minha sedresposta sobre todo o tampão ... que iria trabalhar, é claro (e é realmente portátil ao contrário sed -i).
Curinga
Você está certo e acho a sua abordagem inicial <C-r>0muito boa. Não tenho certeza de que você poderia fazer melhor apenas com os comandos Ex, porque você precisa proteger caracteres especiais. Sem a restrição compatível com POSIX, acho que você usaria a opção muito nomagic \Ve protegeria a barra invertida (porque mantém seu significado especial mesmo \V) com a escape()função cujo segundo argumento é uma string que contém todos os caracteres dos quais você deseja escapar / proteger .
Saginaw
No entanto, no comando anterior, eu também esqueci de proteger a barra, porque também tem um significado especial para o comando global, é o delimitador de padrões. Portanto, o comando correto provavelmente seria algo como: :execute '2,$g/\V' . escape(getline(1), '\/') . '/d'Ou você poderia usar outro caractere para o delimitador de padrão como um ponto e vírgula. Nesse caso, você não precisaria proteger uma barra no padrão. Isso daria algo como::execute '2,$g;\V' . escape(getline(1), '\') . ';d'
Saginaw
11
Acho sua segunda abordagem sedtambém muito boa. Com o Vim, você frequentemente delega certas tarefas especiais para outros programas e sedé provavelmente um bom exemplo disso. A propósito, você não precisa executar sedtodo o seu buffer. Se você deseja executá-lo apenas em uma parte do buffer, pode dar um intervalo. Por exemplo, se você deseja filtrar apenas as linhas entre 50 e 100, você pode digitar: :50,100!<your sed command>.
Saginaw

Respostas:

3

Vim

No Vim, você pode combinar qualquer caractere, incluindo nova linha, com \_.. Você pode usar isso para construir um padrão que corresponda a uma linha inteira, qualquer quantidade de material e, em seguida, a mesma linha:

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

Agora você deseja excluir todas as linhas em um arquivo que correspondem à primeira, sem incluir a primeira. A substituição para excluir a última linha que corresponde à primeira é:

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

Você pode usar :globalpara se certificar de que a substituição seja repetida várias vezes para excluir todas as linhas:

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

POSIX ex

@saginaw mostra uma maneira mais clara de fazer isso no Vim em um comentário à sua pergunta, mas podemos adaptar a técnica acima para o POSIX ex.

Para fazer isso de uma maneira compatível com POSIX, é necessário desabilitar a correspondência de várias linhas, mas você ainda pode fazer uso de referências anteriores. Isso requer algum trabalho extra:

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

Aqui está o detalhamento:

: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

Portanto, se a linha atual for uma duplicata da linha 1, o registro x conterá d. A execução excluirá a linha atual. Se não for uma duplicata, ela conterá um absurdo prefixado com o "qual, quando executado, não funcionará, pois " inicia um comentário. Não sei se essa é a maneira mais legal de fazer isso, é apenas a primeira que me veio à mente!

Acontece que a primeira linha não pode ser excluída porque o processo de cópia altera temporariamente o que é a linha 1. Se não fosse esse o caso, você poderia prefixar o :gcom um 2,$intervalo.

Testado no Vim e ex-vi versão 4.0.

EDITAR

E uma maneira mais simples, que escapa caracteres especiais para criar um padrão de pesquisa (com 'nomagic'conjunto), cria um :globalcomando e o executa:

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

Você não pode fazer isso como uma linha, pois você teria um aninhado :global, o que não é permitido.

Antony
fonte
2

Parece que a única maneira do POSIX de fazer isso é usar um filtro externo, como sed.

Por exemplo, para excluir a 17ª linha do seu arquivo apenas se for exatamente idêntica à 5ª linha e, de outro modo, deixá-lo inalterado, você pode fazer o seguinte:

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

(Você pode executar sedo buffer inteiro aqui, ou somente nas linhas 5-17, mas no primeiro caso, você está fazendo uma filtragem desnecessária - não é grande coisa - e, no último caso, teria que usar o números 1 e 13 em seu sedcomando, em vez de 5 e 17. Confuso.)

Como sedapenas uma única passagem para frente, não há maneira fácil de fazer o inverso e excluir a 5ª linha apenas se for idêntica à 17ª linha. Eu tentei por um tempo como um ponto de curiosidade ... é complicado .


Inovação - Você pode fazer o seguinte:

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

Este é realmente o método mais geral. Da mesma forma, pode ser usado para fornecer o mesmo resultado que o primeiro comando (e excluir a 17ª linha apenas se for idêntica à 5ª linha) da seguinte forma:

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

Para usos mais amplos, como excluir todas as linhas do arquivo idênticas à linha 37 e deixar intacta a linha 37, você pode fazer o seguinte:

: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.*$//'

A conclusão aqui é que, para verificar se duas linhas são idênticas, a melhor ferramenta é sed não ex. Mas como o DevSolar mencionou em um comentário , isso não é um fracasso viou ex- eles foram projetados para funcionar com as ferramentas do Unix; essa é uma grande força.

Curinga
fonte
Muito, muito mais difícil é: inserir uma linha no final de um arquivo, apenas se a linha ainda não existir em algum lugar do arquivo.
Curinga
Isso deve ser possível com uma abordagem semelhante à minha resposta. Eu não acho que seria uma frase única!
Antony