Użyj komendy ex, aby sprawdzić, czy dwie linie są identyczne?

9

Patrzyłem na to pytanie, a potem zastanawiałem się, jak zaimplementować moją odpowiedź, która używa sed wyłącznie POSIX ex .

Sztuczka polega na tym, że chociaż sedmogę porównać przestrzeń wstrzymania z przestrzenią wzorów, aby sprawdzić, czy są one dokładnie równoważne (z G;/^\(.*\)\n\1$/{do something}), nie znam sposobu na wykonanie takiego testu ex.

Wiem, że w Vimie mogłem Yobrócić pierwszą linię, a następnie napisać, :2,$g/<C-r>0/dby prawie zrobić to, co określam - ale jeśli pierwszy wiersz zawiera cokolwiek innego niż bardzo prosty tekst alfanumeryczny, staje się to rzeczywiście przypadkiem, ponieważ linia jest zrzucana jako wyrażenie regularne , nie tylko ciąg znaków do porównania. (A jeśli pierwszy wiersz zawiera ukośnik, reszta wiersza zostanie zinterpretowana jako polecenie!)

Więc jeśli chcę usunąć wszystkie wiersze, myfilektóre są identyczne z pierwszym wierszem - ale nie usunąć pierwszego - jak mogę to zrobić za pomocą ex? Jeśli o to chodzi, jak mogę to zrobić za pomocą vi?

Czy istnieje sposób POSIX na usunięcie linii, jeśli dokładnie pasuje do innej linii?

Być może coś w rodzaju tej wymyślonej składni:

:2,$g/**lines equal to "0**/d
Dzika karta
źródło
3
Możesz zbudować komendę, ale wymagałoby to trochę vimscript i prawdopodobnie nie byłby to POSIX:execute '2,$g/\V' . escape(getline(1), '\') . '/d'
owy
1
@saginaw, dzięki. Jak dotąd jedynym podejściem POSIX, które mi się przydarzyło, jest po prostu użycie sedjako filtru od wewnątrz exi uruchomienie całej mojej sedodpowiedzi na całym buforze ... który oczywiście działałby (i w rzeczywistości jest przenośny w przeciwieństwie do sed -i).
Wildcard
Masz rację i uważam, że twoje początkowe podejście jest <C-r>0bardzo dobre. Nie jestem pewien, czy lepiej radzisz sobie tylko z poleceniami Ex, ponieważ musisz chronić znaki specjalne. Bez ograniczenia zgodnego z POSIX myślę, że użyłbyś bardzo nomagicznego przełącznika, \Va następnie chroniłbyś odwrotny ukośnik (ponieważ zachowuje on swoje specjalne znaczenie nawet przy pomocy \V) za pomocą escape()funkcji, której drugi argument jest ciągiem zawierającym wszystkie znaki, które chcesz uciec / zabezpieczyć .
saginaw
Jednak w poprzednim poleceniu zapomniałem również chronić ukośnik, ponieważ ma on również specjalne znaczenie dla polecenia globalnego, jest to separator wzorców. Prawidłowe polecenie prawdopodobnie wyglądałoby mniej więcej tak: :execute '2,$g/\V' . escape(getline(1), '\/') . '/d'Lub możesz użyć innego znaku dla separatora wzorców, takiego jak średnik. W takim przypadku nie trzeba chronić ukośnika we wzorcu. Dałoby to coś takiego::execute '2,$g;\V' . escape(getline(1), '\') . ';d'
saginaw
1
Uważam, że twoje drugie podejście jest sedrównież bardzo dobre. Dzięki Vimowi często przekazujesz określone zadania specjalne innym programom i sedprawdopodobnie jest to dobry przykład. Nawiasem mówiąc, nie musisz uruchamiać sedcałego bufora. Jeśli chcesz uruchomić go tylko na części bufora, możesz podać zakres. Na przykład, jeśli chcesz filtrować tylko te linie pomiędzy 50 a 100, można wpisać: :50,100!<your sed command>.
saginaw

Odpowiedzi:

3

Wigor

W Vimie możesz dopasować dowolny znak, w tym znak nowej linii \_.. Możesz użyć tego do skonstruowania wzoru pasującego do całej linii, dowolnej ilości rzeczy, a następnie tej samej linii:

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

Teraz chcesz usunąć wszystkie wiersze w pliku, które pasują do pierwszego, nie włączając pierwszego. Podstawienie do usunięcia ostatniego wiersza pasującego do pierwszego to:

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

Możesz użyć, :globalaby upewnić się, że podstawienie jest powtarzane wystarczająco dużo razy, aby usunąć wszystkie linie:

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

POSIX ex

@saginaw pokazuje lepszy sposób na to w Vimie w komentarzu do twojego pytania, ale możemy dostosować powyższą technikę dla POSIX ex.

Aby to zrobić w sposób zgodny z POSIX, musisz wyłączyć dopasowanie wielu linii, ale nadal możesz korzystać z odwołań wstecznych. Wymaga to dodatkowej pracy:

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

Oto podział:

: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

Więc jeśli bieżąca linia jest duplikatem linii 1, rejestr x będzie zawierał d. Wykonanie go spowoduje usunięcie bieżącej linii. Jeśli nie jest duplikatem, będzie zawierał bzdury z prefiksem, z "którym po wykonaniu jest to brak operacji, ponieważ " rozpoczyna komentarz. Nie wiem, czy jest to najładniejszy sposób, aby to osiągnąć, to tylko pierwszy, który przyszedł mi do głowy!

Zdarza się, że nie można usunąć pierwszego wiersza, ponieważ proces kopiowania tymczasowo zmienia wiersz 1. Jeśli to nie był przypadek można poprzedzić :gz 2,$zakresu zamiast.

Testowane w Vimie i ex-vi wersji 4.0.

EDYTOWAĆ

I prostszy sposób, który pozwala uniknąć znaków specjalnych w celu utworzenia wzorca wyszukiwania (z 'nomagic'zestawem), buduje :globalpolecenie, a następnie je wykonuje:

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

Nie możesz tego zrobić jako liniowiec, ponieważ miałbyś zagnieżdżone :global, co jest niedozwolone.

Antony
źródło
2

Wydaje się, że jedynym sposobem na to, aby to zrobić w POSIX, jest użycie zewnętrznego filtra, takiego jak sed.

Na przykład, aby usunąć 17 linię pliku tylko wtedy, gdy jest dokładnie identyczna z linią 5, a poza tym pozostawić ją bez zmian, możesz wykonać następujące czynności:

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

(Możesz uruchomić sedcały bufor tutaj lub możesz uruchomić go tylko w wierszach 5-17, ale w pierwszym przypadku robisz niepotrzebne filtrowanie - nic wielkiego - a w tym drugim przypadku będziesz musiał użyć cyfry 1 i 13 w twoim sedpoleceniu zamiast 5 i 17. Mylące.)

Ponieważ sedwykonuje tylko jedno przejście do przodu, nie ma łatwego sposobu na cofnięcie i usunięcie piątej linii tylko wtedy, gdy jest identyczna z 17-tą linią. Próbowałem przez chwilę z ciekawości ... to trudne .


Przełom - możesz to zrobić w następujący sposób:

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

To właściwie bardziej ogólna metoda. Można go również użyć, aby dać ten sam wynik jak pierwsze polecenie (i usunąć 17 linię tylko, jeśli jest identyczna z 5 linią) w następujący sposób:

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

W przypadku szerszych zastosowań, takich jak usuwanie wszystkich wierszy pliku, które są identyczne z wierszem 37, pozostawiając nienaruszoną linię 37, możesz wykonać następujące czynności:

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

Zawarcie tu jest do sprawdzania, czy dwa wiersze są identyczne, najlepszym narzędziem jest sed , nie ex. Ale jak wspomniała DevSolar w komentarzu , nie jest to błąd viani ex- są one zaprojektowane do pracy z narzędziami uniksowymi; to jest główna siła.

Dzika karta
źródło
Znacznie trudniejsze jest: wstawienie linii na końcu pliku, tylko jeśli linia nie istnieje już gdzieś w pliku.
Wildcard
Powinno to być wykonalne przy podejściu podobnym do mojej odpowiedzi. Nie sądzę jednak, żeby był to jeden liniowiec!
Antony