Filtruj lub potokuj określone sekcje pliku

14

Mam plik wejściowy z niektórymi sekcjami, które są oznaczone znacznikami początkowym i końcowym, na przykład:

line A
line B
@@inline-code-start
line X
line Y
line Z
@@inline-code-end
line C
line D

Chcę zastosować transformację do tego pliku, tak aby wiersze X, Y, Z były filtrowane przez niektóre polecenia ( nlna przykład), ale reszta linii przechodzi przez niezmienione. Zauważ, że nl(linie liczbowe) akumulują stan w poprzek linii, więc nie jest to transformacja statyczna, która jest stosowana do każdej z linii X, Y, Z. ( Edycja : wskazano, że nlmoże działać w trybie, który nie wymaga stanu akumulacji, ale używam jedynie nljako przykładu w celu uproszczenia pytania. W rzeczywistości polecenie jest bardziej złożonym niestandardowym skryptem. Tak naprawdę wyglądam for to ogólne rozwiązanie problemu zastosowania standardowego filtra do podsekcji pliku wejściowego )

Dane wyjściowe powinny wyglądać następująco:

line A
line B
     1 line X
     2 line Y
     3 line Z
line C
line D

W pliku może znajdować się kilka takich sekcji, które wymagają transformacji.

Aktualizacja 2 Nie określiłem pierwotnie, co powinno się stać, jeśli jest więcej niż jedna sekcja, na przykład:

line A
line B
@@inline-code-start
line X
line Y
line Z
@@inline-code-end
line C
line D
 @@inline-code-start
line L
line M
line N
@@inline-code-end

Oczekuję, że stan będzie musiał zostać utrzymany tylko w ramach danej sekcji, dając:

line A
line B
     1 line X
     2 line Y
     3 line Z
line C
line D
     1 line L
     2 line M
     3 line N

ale myślę, że interpretacja problemu jako wymagającego zachowania stanu w sekcjach jest poprawna i przydatna w wielu kontekstach.

Zakończ aktualizację 2

Moją pierwszą myślą jest zbudowanie prostej maszyny stanów, która śledzi, w której sekcji się znajdujemy:

#!/usr/bin/bash
while read line
do
  if [[ $line == @@inline-code-start* ]]
  then
    active=true
  elif [[ $line == @@inline-code-end* ]]
  then
    active=false
  elif [[ $active = true ]]
  then
    # pipe
  echo $line | nl
  else
    # output
    echo $line
  fi
done

Z którym biegam:

cat test-inline-codify | ./inline-codify

To nie działa, ponieważ każde połączenie z nljest niezależne, więc numery linii nie zwiększają się:

line A
line B
     1  line X
     1  line Y
     1  line Z
line C
line D

Następną próbą było użycie fifo:

#!/usr/bin/bash
mkfifo myfifo
nl < myfifo &
while read line
do
  if [[ $line == @@inline-code-start* ]]
  then
    active=true
  elif [[ $line == @@inline-code-end* ]]
  then
    active=false
  elif [[ $active = true ]]
  then
    # pipe
    echo $line > myfifo
  else
    # output
    echo $line
  fi
done
rm myfifo

To daje poprawne wyjście, ale w niewłaściwej kolejności:

line A
line B
line C
line D
     1  line 1
     2  line 2
     3  line 3

Prawdopodobnie dzieje się trochę buforowania.

Czy wszystko źle robię? To wydaje się dość ogólnym problemem. Wydaje mi się, że powinien istnieć prosty rurociąg, który by to rozwiązał.

James Scriven
źródło
nlnie musi gromadzić stanu . Spójrz na nl -di sprawdzić man/ infostron zawierających informacje o nl„s sekcja separatora .
mikeserv
nl to tylko przykład. W moim przypadku uruchamiam skrypt niestandardowy zamiast nl.
James Scriven,
W takim przypadku wyjaśnij, co robi Twój skrypt.
terdon
Wyjaśniłem w pytaniu, że używam tylko nljako przykładowego filtra. Pomyślałem, że uprości to pytanie, wyjaśniając szczegóły tego, co dokładnie robi filtr, ale prawdopodobnie spowodowałem więcej zamieszania. W rzeczywistości filtruję podsekcję za pomocą zakreślacza kodu, aby uzyskać domowy generator statycznych blogów. Obecnie używam GNU source-highlight, ale to może się zmienić i mogę dodać więcej filtrów, takich jak formater.
James Scriven,

Odpowiedzi:

7

Zgadzam się z tobą - to prawdopodobnie jest to ogólny problem. Jednak niektóre popularne narzędzia mają pewne funkcje do obsługi tego.


nl

nl, na przykład, dzieli dane wejściowe na strony logiczne, które są -deliminowane przez dwuznakowy separator sekcji . Trzy zdarzenia na linii same w sobie wskazują początek kursu , dwa ciała i jeden stopkę . Zastępuje wszystkie znalezione na wejściu puste linie wyjściowe - które są jedynymi pustymi liniami, jakie kiedykolwiek drukuje

Zmieniłem twój przykład, aby dołączyć inną sekcję i wstawić go ./infile. Wygląda to tak:

line A
line B
@@inline-code-start
line X
line Y
line Z
@@inline-code-end
line C
line D
@@start
line M
line N
line O
@@end

Następnie wykonałem następujące czynności:

sed 's/^@@.*start$/@@@@@@/
     s/^@@.*end$/@@/'  <infile |
nl -d@@ -ha -bn -w1

nlmożna powiedzieć, aby gromadził stan na logicznych stronach, ale domyślnie tak nie jest. Zamiast tego numeruje wiersze danych wejściowych według stylów i według sekcji . Tak więc -haśrodki ponumerować wszystkie nagłówki wierszy i -bnoznacza brak linii nadwozia - jak zaczyna się w organizmie stanie.

Dopóki się tego nie nauczyłem, używałem nldowolnego wejścia, ale po uświadomieniu sobie, że nlmoże to zniekształcić dane wyjściowe zgodnie z domyślnym -deliminatorem \:, nauczyłem się być bardziej ostrożnym i zacząłem używać grep -nF ''zamiast tego nieprzetestowanego wejścia. Ale kolejną lekcją wyciągniętą tego dnia było to, że nlmożna bardzo użytecznie zastosować pod innymi względami - takimi jak ten - jeśli tylko zmodyfikujesz jego dane wejściowe tylko nieznacznie - tak jak ja sedpowyżej.

WYNIK

  line A
  line B

1       line X
2       line Y
3       line Z

  line C
  line D

1       line M
2       line N
3       line O

Oto kilka więcej informacji nl- czy zauważysz powyżej, jak wszystkie wiersze oprócz ponumerowanych zaczynają się od spacji? Kiedy nllinie liczb wstawiają określoną liczbę znaków do nagłówka każdego z nich. W przypadku tych wierszy nie jest numerowany - nawet puste - zawsze dopasowuje wcięcie, wstawiając ( idth -wcount + -separator len) * spacje na początku nienumerowanych linii. Pozwala to na dokładne odtworzenie treści nienumerowanej przez porównanie jej z treścią numerowaną - przy niewielkim wysiłku. Jeśli weźmiesz pod uwagę, że nlpodzieli on twoje dane wejściowe na logiczne sekcje i że możesz wstawić dowolne -sciągi na początku każdej linii, którą numeruje, to całkiem łatwo jest obsłużyć wynik:

sed 's/^@@.*start$/@@@@@@/
     s/^@@.*end/@@/; t
     s/^\(@@\)\{1,3\}$/& /' <infile |
nl -d@@ -ha -bn -s' do something with the next line!
'

Powyższe wydruki ...

                                        line A
                                        line B

 1 do something with the next line!
line X
 2 do something with the next line!
line Y
 3 do something with the next line!
line Z

                                        line C
                                        line D

 1 do something with the next line!
line M
 2 do something with the next line!
line N
 3 do something with the next line!
line O

GNU ANTYLOPA sed

Jeśli nlnie jest twoją aplikacją docelową, GNU sedmoże ewykonać dowolne polecenie powłoki w zależności od dopasowania.

sed '/^@@.*start$/!b
     s//nl <<\\@@/;:l;N
     s/\(\n@@\)[^\n]*end$/\1/
Tl;e'  <infile

Powyżej sedzbiera dane wejściowe w przestrzeni wzorów, dopóki nie będzie wystarczające, aby pomyślnie przejść podstawienie Test i przestać bbiegać z powrotem do :labla. Kiedy tak się dzieje, wykonuje epolecenia nlwejściowe reprezentowane jako <<dokument tutaj dla całej reszty przestrzeni wzorcowej.

Przepływ pracy wygląda następująco:

  1. /^@@.*start$/!b
    • jeśli ^cała linia $ma !nie /pasuje /powyższy wzór, to jest branczerskich z skryptu i autoprinted - więc od tej chwili pracujemy tylko z serią linii, która rozpoczęła się z wzoru.
  2. s//nl <<\\@@/
    • puste s//pole /oznacza ostatni adres, który sedpróbowano dopasować - więc to polecenie zastępuje zamiast tego całą @@.*startlinię nl <<\\@@.
  3. :l;N
    • :Polecenie definiuje etykietę oddział - tu ustawić jeden o nazwie :lAbel. Polecenie Next dołącza następny wiersz danych wejściowych do obszaru wzorów, po którym \nnastępuje znak ewline. Jest to jeden z niewielu sposobów, aby uzyskać \newline w sedprzestrzeni wzorów - \npostać ewline jest pewnym ogranicznikiem dla sedder, który robił to jakiś czas.
  4. s/\(\n@@\)[^\n]*end$/\1/
    • ta s///ubstytucja może się powieść tylko po napotkaniu początku i tylko przy pierwszym wystąpieniu linii końcowej . Będzie działał tylko na przestrzeni wzorów, w której \nnatychmiast po ostatniej linii ewline zostanie @@.*endzaznaczony sam koniec $przestrzeni wzorów. Kiedy działa, zastępuje cały dopasowany ciąg \1pierwszą \(grupą \)lub \n@@.
  5. Tl
    • te Tgałęzie dowodzenia est na etykiecie (jeżeli istnieje) , czy udane podstawienie nie nastąpiło od czasu ostatniego linia wejściowa została wciągnięta w przestrzeń wzoru (tak jak ja w / N) . Oznacza to, że za każdym razem, gdy \newline jest dodawana do przestrzeni wzorów, która nie pasuje do ogranicznika końcowego, Tpolecenie est kończy się niepowodzeniem i rozgałęzia się z powrotem do :labla, co powoduje sedciągnięcie Nlinii zewnętrznej i zapętlanie aż do pomyślnego zakończenia .
  6. e

    • Gdy podstawienie na mecz końcowego jest udany, a skrypt nie oddział z powrotem dla nieudanej Test, sedbędą execute polecenie, które looks tak:

      nl <<\\@@\nline X\nline Y\nline Z\n@@$

Możesz to zobaczyć, edytując ostatnią linię, aby wyglądała Tl;l;e.

Drukuje:

line A
line B
     1  line X
     2  line Y
     3  line Z
line C
line D
     1  line M
     2  line N
     3  line O

while ... read

Ostatnim sposobem, a może najprostszym, jest użycie while readpętli, ale nie bez powodu. Powłoka - (szczególnie bashpowłoka) - jest zazwyczaj dość fatalna w obsłudze dużych ilości lub stałych strumieni. To też ma sens - zadaniem powłoki jest obsługa znaków po znaku i wywoływanie innych poleceń, które mogą obsłużyć większe rzeczy.

Ale co ważne, jego rola polega na tym, że powłoka nie może read przesadzać z danymi wejściowymi - jest określona, ​​aby nie buforować danych wejściowych lub wyjściowych do momentu, w którym zużywa tak dużo lub nie przekazuje wystarczająco dużo czasu, aby wywoływać polecenia, których nie ma - do bajtu. readStanowi więc doskonały test wejściowy - do returninformacji o tym, czy jest jeszcze wejście i powinieneś wywołać następne polecenie, aby je odczytać - ale ogólnie nie jest to najlepsza droga.

Oto przykład, jednak, jak można wykorzystać read i inne polecenia do wejścia procesu synchronizacji:

while   IFS= read -r line        &&
case    $line in (@@*start) :;;  (*)
        printf %s\\n "$line"
        sed -un "/^@@.*start$/q;p";;
esac;do sed -un "/^@@.*end$/q;=;p" |
        paste -d: - -
done    <infile

Pierwszą rzeczą, która dzieje się dla każdej iteracji, jest readciągnięcie linii. Jeśli się powiedzie, oznacza to, że pętla nie uderzyła jeszcze w EOF, a więc w casedopasowaniu ogranicznika początkowegodo blok jest natychmiast wykonywany. W przeciwnym razie printfdrukuje $linego readi sednazywa się.

sedbędzie printować każdą linię, aż napotka znacznik początkowy - kiedy qcałkowicie wykorzysta wejście. -uPrzełącznik nbuffered jest konieczne dla GNU sedponieważ może buforować dość łapczywie inaczej, ale - według specyfikacji - inne POSIX seds powinno działać bez szczególną uwagę - tak długo, jak <infilejest zwykłym plikiem.

Gdy pierwsze sed quituje, powłoka wykonuje doblok pętli - która wywołuje kolejną, sedktóra wypisuje każdą linię, aż napotka znacznik końca . Potokuje dane wyjściowe do paste, ponieważ wypisuje numery linii w osobnych wierszach. Lubię to:

1
line M
2
line N
3
line O

pastenastępnie wkleja je razem do :postaci, a cały wynik wygląda następująco:

line A
line B
1:line X
2:line Y
3:line Z
line C
line D
1:line M
2:line N
3:line O

To tylko przykłady - tutaj można wykonać wszystko w teście lub wykonać bloki, ale pierwsze narzędzie nie może zużywać zbyt wiele danych wejściowych.

Wszystkie zaangażowane narzędzia odczytują te same dane wejściowe - i drukują wyniki - każde z osobna. Tego rodzaju rzeczy może być trudno się zawiesić - ponieważ różne narzędzia będzie buforować więcej niż inni - ale można ogólnie polegać na dd, headi sedzrobić to, co trzeba (choć dla GNU sed, trzeba CLI-switch) i zawsze powinieneś być w stanie polegać read- ponieważ z natury jest bardzo powolny . I dlatego powyższa pętla wywołuje ją tylko raz na blok wejściowy.

mikeserv
źródło
Przetestowałem drugi sedprzykład, który podałeś i działa, ale NAPRAWDĘ mam problem z odczytaniem składni. (mój sed jest dość słaby i zwykle ogranicza się do s / findthis / replaceethis / g. Będę musiał usiąść i naprawdę zrozumieć sed.)
James Scriven
@JamesScriven - Właśnie edytowałem, aby lepiej to wyjaśnić. Daj mi znać, jeśli to nie pomoże. Dużo zmieniłem też polecenie - teraz jest w mniejszych, bardziej rozsądnych częściach.
mikeserv
4

Jedną z możliwości jest zrobienie tego za pomocą edytora tekstu vim. Może potokować dowolne sekcje za pomocą poleceń powłoki.

Jednym ze sposobów na to jest użycie numerów linii za pomocą :4,6!nl. To polecenie ex uruchomi nl w liniach 4-6 włącznie, osiągając to, co chcesz na przykładowym wejściu.

Innym, bardziej interaktywnym sposobem jest wybranie odpowiednich linii za pomocą trybu wyboru linii (Shift-V) i klawiszy strzałek lub wyszukiwania, a następnie za pomocą :!nl. Pełna sekwencja poleceń dla twojego przykładowego wejścia może być

/@@inline-code-start
jV/@@inline-code-end
k:!nl

Nie jest to zbyt dobrze dostosowane do automatyzacji (lepsze są odpowiedzi z użyciem np. Sed), ale w przypadku edycji jednorazowych bardzo przydatne jest to, że nie trzeba uciekać się do 20-liniowych skryptów powłoki.

Jeśli nie znasz vi (m), powinieneś przynajmniej wiedzieć, że po tych zmianach możesz zapisać plik za pomocą :wq.

marcelm
źródło
Tak, vim jest niesamowity! Ale w tym przypadku szukam rozwiązania skryptowego.
James Scriven,
@JamesScriven, każdy, kto mówi, że vim nie jest skryptowalny w niewystarczającym stopniu. Najpierw utwórz katalog projektu i w tym katalogu skopiuj wszystkie pliki startowe vima z twojego katalogu domowego (ln -s działa dobrze, z wyjątkiem .vimrc, które zamierzamy zmodyfikować i .viminfo, które mogą być wypełnione hałasem). Dodaj definicję funkcji, która wykona pracę do nowego pliku .vimrc, a następnie wywołaj vim jako HOME=$(pwd) vim -c 'call Mf()' f. Jeśli używasz xargs, możesz chcieć użyć gvim na dedykowanym serwerze xserver, aby nie uszkodzić twojego tty (vnc jest niezależny od karty graficznej i może być monitorowany).
hildred
@hildred Hmmm ... Czy nie mogę po prostu użyć [XSendEvent] ( tronche.com/gui/x/xlib/event-handling/XSendEvent.html ) do symulacji kliknięć myszką w celu vim?
James Scriven,
2

Najprostszym rozwiązaniem, jakie mogę wymyślić, jest nieużywanie, nlale samodzielne policzenie wierszy:

#!/usr/bin/env bash
while read line
do
    if [[ $line == @@inline-code-start* ]]
    then
        active=true
    elif [[ $line == @@inline-code-end* ]]
    then
        active=false
    elif [[ $active = true ]]
    then
        ## Count the line number
        let num++;
        printf "\t%s %s\n" "$num" "$line"
    else
        # output
        printf "%s\n" "$line"
    fi
done

Następnie uruchom go na pliku:

$ foo.sh < file
line A
line B
    1 line X
    2 line Y
    3 line Z
line C
line D
terdon
źródło
Dzięki, terdon. Zaktualizowałem pytanie, aby wyjaśnić, że szukam ogólnego rozwiązania dla filtrowania podsekcji danych wejściowych, a raczej konkretnego przykładu linii numeracyjnych. być może lepszym przykładem byłoby „tac” (wiersze odwrotne)
James Scriven,
2

Jeśli Twoim celem jest wysłanie całego bloku kodu do pojedynczej instancji procesu, możesz zgromadzić linie i opóźnić potokowanie, aż dojdziesz do końca bloku kodu:

#!/bin/bash

acc=""

while read line
do
  if [[ $line == @@inline-code-start* ]]
  then
    active=true
    acc=""
  elif [[ $line == @@inline-code-end* ]]
  then
    active=false
    # Act on entire block of code
    echo "${acc:1}" | nl  # Chops off first leading new-line character using ${VAR:1}
  elif [[ $active = true ]]
  then
    acc=$( printf "%s\n%s" "$acc" "$line" )
  else
    # output
    echo $line
  fi
done

Powoduje to utworzenie pliku wejściowego, który trzykrotnie powtarza przypadek testowy:

line A
line B
     1  line X
     2  line Y
     3  line Z
line C
line D
line A
line B
     1  line X
     2  line Y
     3  line Z
line C
line D
line A
line B
     1  line X
     2  line Y
     3  line Z
line C
line D

Zrobić coś innego z bloku kodu, np odwrócić, a następnie numer, tylko rury to przez coś innego: echo -E "${acc:1}" | tac | nl. Wynik:

line A
line B
     1  line Z
     2  line Y
     3  line X
line C
line D

Lub liczba słów echo -E "${acc:1}" | wc:

line A
line B
      3       6      21
line C
line D
Supr
źródło
2

Edytuj dodała opcję definiowania filtra dostarczonego przez użytkownika

#!/usr/bin/perl -s
use IPC::Open2;
our $p;
$p = "nl" unless $p;    ## default filter

$/ = "\@\@inline-code-end\n";
while(<>) { 
   chomp;
   s/\@\@inline-code-start\n(.*)/pipeit($1,$p)/se;
   print;
}

sub pipeit{my($text,$pipe)=@_;
  open2(my $R, my $W,$pipe) || die("can open2");
  local $/ = undef;
  print $W $text;
  close $W;
  return <$R>;
}

Domyślnie filtr to „nl”. Aby zmienić filtr, użyj opcji „-p” za pomocą polecenia podanego przez użytkownika:

codify -p="wc" file

lub

codify -p="sed -e 's@^@ ║ @; 1s@^@ ╓─\n@; \$s@\$@\n ╙─@'" file

Ten ostatni filtr wyświetli:

line A
line B
 ╓─
  line X
  line Y
  line Z
 ╙─
line C
line D

Aktualizacja 1 Użycie IPC :: Open2 ma problemy ze skalowaniem: przekroczenie rozmiaru bufora może spowodować zablokowanie. (w mojej maszynie buforowany rozmiar rury, jeśli 64K odpowiada 10_000 x „linia Y”).

Jeśli potrzebujemy większych rzeczy (czy potrzebujemy więcej 10000 „linii Y”):

(1) zainstaluj i użyj use Forks::Super 'open2';

(2) lub zamień funkcję pipeit na:

sub pipeit{my($text,$pipe)=@_;
  open(F,">","/tmp/_$$");
  print F $text;
  close F;
  my $out = `$pipe < /tmp/_$$ `;
  unlink "/tmp/_$$";
  return $out;
}
JJoao
źródło
To jest naprawdę świetne. Sądzę, że sztuczki polegają na tym, że nie przetwarzasz linii po linii (przez redifining $/i sflagę), a użycie eflagi do wykonania rzeczywistego wywołania polecenia zewnętrznego. Naprawdę podoba mi się drugi przykład (ascii art)!
James Scriven,
Zauważyłem jednak, że w tym podrozdziale nie wydaje się, aby przekraczało kilka tysięcy linii. Podejrzewam, że ma to związek z traktowaniem podsekcji jako jednego dużego bloku tekstu.
James Scriven
Dzięki. Tak: `/ e` = eval; /s= („.” oznacza (.|\n)); $/redefiniuje separator rejestru.
JJoao,
@JamesScriven, masz rację (rura blokuje). Pozwól, że przetestuję, co się dzieje ...
JJoao,
@JamesScriven, proszę zobaczyć moją aktualizację ...
JJoao
1

To praca dla awk.

#!/usr/bin/awk -f
$0 == "@@inline-code-start" {pipe = 1; next}
$0 == "@@inline-code-end" {pipe = 0; close("nl"); next}
pipe {print | "nl"}
!pipe {print}

Kiedy skrypt widzi znacznik początkowy, zauważa, że ​​powinien rozpocząć pipowanie nl. Gdy pipezmienna ma wartość true (niezerowa), dane wyjściowe są przesyłane do nlpolecenia; gdy zmienna ma wartość false (nieustawiona lub zero), dane wyjściowe są drukowane bezpośrednio. Polecenie potokowe jest rozwidlane przy pierwszym napotkaniu konstrukcji potoku dla każdego ciągu polecenia. Kolejne oceny operatora potoku z tym samym łańcuchem ponownie wykorzystują istniejącą potok; inna wartość ciągu utworzyłaby inny potok. closeZamyka się rurę do danego łańcucha poleceń.


Jest to zasadniczo ta sama logika, co skrypt powłoki przy użyciu nazwanego potoku, ale dużo łatwiej jest przeliterować, a ścisła logika jest wykonana poprawnie. Musisz zamknąć potok we właściwym czasie, aby nlpolecenie zakończyło się, opróżniając jego bufory. Twój skrypt faktycznie zamyka potok zbyt wcześnie: potok jest zamykany, gdy tylko echo $line >myfifozakończy się wykonywanie pierwszego . Jednak nlpolecenie widzi koniec pliku tylko wtedy, gdy otrzyma wycinek czasu przed następnym uruchomieniem skryptu echo $line >myfifo. Jeśli masz dużą ilość danych lub dodajesz sleep 1po zapisaniu myfifo, zobaczysz, że nlprzetwarza tylko pierwszą linię lub pierwszą szybką wiązkę linii, a następnie kończy działanie, ponieważ widzi koniec danych wejściowych.

Korzystając ze swojej struktury, musisz trzymać rurkę otwartą, dopóki jej nie będziesz już potrzebować. Musisz mieć pojedyncze przekierowanie wyjścia do potoku.

nl <myfifo &
exec 3>&1
while IFS= read -r line
do
  if [[ $line == @@inline-code-start* ]]
  then
    exec >myfifo
  elif [[ $line == @@inline-code-end* ]]
  then
    exec >&3
  else
    printf '%s\n' "$line"
  fi
done

(Skorzystałem również z okazji, aby dodać poprawne cytowanie i tym podobne - zobacz Dlaczego mój skrypt powłoki dusi się na białych znakach lub innych znakach specjalnych? )

Jeśli to robisz, równie dobrze możesz użyć potoku zamiast nazwanego potoku.

while IFS= read -r line
do
  if [[ $line == @@inline-code-start* ]]
  then
    while IFS= read -r line && [[ $line != @@inline-code-end* ]] do
      printf '%s\n' "$line"
    done | nl
  else
    printf '%s\n' "$line"
  fi
done
Gilles „SO- przestań być zły”
źródło
twoje rozwiązanie awk jest naprawdę fajne! Myślę, że jest to zdecydowanie najbardziej zwięzłe (ale bardzo czytelne) rozwiązanie. Czy zachowanie awk polegające na ponownym użyciu rury do nl jest gwarantowane, czy może awk zdecyduje: „hej, na razie masz już wystarczająco dużo rur… Zamierzam zamknąć tę rurkę i otworzyć nową”? Twoje rozwiązanie „potokowe” jest również bardzo miłe. Pierwotnie pominąłem podejście z osadzonymi pętlami while, ponieważ myślałem, że może to być nieco mylące, ale myślę, że to, co masz, jest świetne. Przed znakiem. Brakuje średnika do. (Nie mam tutaj przedstawiciela, aby dokonać drobnej edycji.)
James Scriven,
1
... Nie mogłem uruchomić twojego nazwanego rozwiązania do rur. Wydaje się, że istnieje warunek wyścigu, taki, że odcinek prowadzony do nl czasami ginie całkowicie. Ponadto, jeśli jest druga sekcja @@ inline-code-start / end, zawsze się gubi.
James Scriven,
0

OK, po pierwsze; rozumiem , że nie szukasz sposobu numerowania linii w sekcjach pliku. Ponieważ nie podałeś rzeczywistego przykładu tego, jaki może być Twój filtr (inny niż nl), załóżmy, że tak

tr "[[:lower:]]" "[[:upper:]]"

tzn. przekonwertuj tekst na wielkie litery; więc dla wejścia

line A
line B
@@inline-code-start
line X
line Y
line Z
@@inline-code-end
line C
line D

chcesz wynik

line A
line B
LINE X
LINE Y
LINE Z
line C
line D

Oto moje pierwsze przybliżenie rozwiązania:

#!/bin/sh
> file0
> file1
active=0
nl -ba "$@" | while IFS= read -r line
do
        case "$line" in
            ([\ 0-9][\ 0-9][\ 0-9][\ 0-9][\ 0-9][\ 0-9]"        @@inline-code-start")
                active=1
                ;;
            ([\ 0-9][\ 0-9][\ 0-9][\ 0-9][\ 0-9][\ 0-9]"        @@inline-code-end")
                active=0
                ;;
            (*)
                printf "%s\n" "$line" >> file$active
        esac
done
(cat file0; tr "[[:lower:]]" "[[:upper:]]" < file1) | sort | sed 's/^[ 0-9]\{6\}        //'

gdzie spacje przed @@ciągami znaków i blisko końca ostatniego wiersza to tabulatory. Pamiętaj, że używam nl do własnych celów . (Oczywiście robię to, aby rozwiązać Twój problem, ale nie po to, aby uzyskać dane wyjściowe z numerami wierszy).

To numeruje linie wejścia, dzięki czemu możemy rozdzielić je na znaczniki sekcji i wiedzieć, jak je później złożyć ponownie. Główny korpus pętli opiera się na pierwszej próbie, biorąc pod uwagę fakt, że znaczniki sekcji mają na sobie numery linii. Dzieli dane wejściowe na dwa pliki: file0(nieaktywne; nie w sekcji) i file1(aktywne; w sekcji). Tak wyglądają powyższe dane wejściowe:

file0:
     1  line A
     2  line B
     8  line C
     9  line D

file1:
     4  line X
     5  line Y
     6  line Z

Potem biegniemy file1 (co jest konkatenacją wszystkich linii w sekcji) przez filtr wielkich liter; połącz to z niefiltrowanymi liniami poza sekcją; sortuj, aby przywrócić je do pierwotnej kolejności; a następnie zdejmij numery linii. Daje to wynik pokazany u góry mojej odpowiedzi.

Zakłada się, że Twój filtr pozostawia numery linii w spokoju. Jeśli tak nie jest (np. Jeśli wstawia lub usuwa znaki na początku wiersza), to uważam, że to ogólne podejście może być nadal stosowane, ale będzie wymagało nieco trudniejszego kodowania.

Scott
źródło
nljuż tam wykonuje większość pracy - po to jest jego -dopcja eliminatora.
mikeserv
0

Skrypt powłoki, który używa sed do wyprowadzania fragmentów niewyznaczonych linii i dostarczania określonych fragmentów linii do programu filtrującego:

#!/bin/bash

usage(){
    echo "  usage: $0 <input file>"
}

# Check input file
if [ ! -f "$1" ]; then
    usage
    exit 1
fi

# Program to use for filtering
# e.g. FILTER='tr X -'
FILTER='./filter.sh'

# Generate arrays with starting/ending line numbers of demarcators
startposs=($(grep -n '^@@inline-code-start$' "$1" | cut -d: -f1))
endposs=($(grep -n '^@@inline-code-end$' "$1" | cut -d: -f1))

nums=${#startposs[*]}
nume=${#endposs[*]}

# Verify both line number arrays have the same number of elements
if (($nums != $nume)); then
    echo "Tag mismatch"
    exit 2
fi

lastline=1
i=0
while ((i < nums)); do
    # Exclude lines with code demarcators
    sprev=$((${startposs[$i]} - 1))
    snext=$((${startposs[$i]} + 1))
    eprev=$((${endposs[$i]} - 1))

    # Don't run this bit if the first demarcator is on the first line
    if ((sprev > 1)); then
        # Output lines leading up to start demarcator
        sed -n "${lastline},${sprev} p" "$1"
    fi

    # Filter lines between demarcators
    sed -n "${snext},${eprev} p" "$1" | $FILTER

    lastline=$((${endposs[$i]} + 1))
    let i++
done

# Output lines (if any) following last demarcator
sed -n "${lastline},$ p" "$1"

Napisałem ten skrypt do pliku o nazwie detagger.sh i wykorzystał je jako tak: ./detagger.sh infile.txt. Utworzyłem osobny plik filter.sh, aby naśladować funkcję filtrowania w pytaniu:

#!/bin/bash
awk '{ print "\t" NR " " $0}'

Ale operację filtrowania można zmienić w kodzie.

Próbowałem za tym podążać za ogólnym rozwiązaniem , aby operacje takie jak linie numeracyjne nie wymagały dodatkowego / wewnętrznego zliczania. Skrypt przeprowadza pewne podstawowe sprawdzanie, aby zobaczyć, czy znaczniki demarkatora są w parach, i nie obsługuje w pełni z wdziękiem tagów zagnieżdżonych.

Kupa
źródło
-1

Dzięki za wszystkie świetne pomysły. Wymyśliłem własne rozwiązanie, śledząc podsekcję w pliku tymczasowym i przesyłając wszystko naraz do mojego zewnętrznego polecenia. Jest to bardzo podobne do tego, co sugerował Supr (ale ze zmienną powłoki zamiast pliku tymczasowego). Poza tym bardzo podoba mi się pomysł użycia sed, ale dla mnie ta składnia wydaje się nieco przesadzona.

Moje rozwiązanie:

(Używam nltylko jako przykładowego filtra)

#!/usr/bin/bash

while read line
do
  if [[ $line == @@inline-code-start* ]]
  then
    active=true
    tmpfile=$(mktemp)
    trap "rm -f $tmpfile" EXIT
  elif [[ $line == @@inline-code-end* ]]
  then
    active=false
    <$tmpfile nl
    rm $tmpfile
  elif [[ $active = true ]]
  then
    echo $line >> $tmpfile
  else
    echo $line
  fi
done

Wolałbym nie mieć do czynienia z zarządzaniem plikami tymczasowymi, ale rozumiem, że zmienne powłoki mogą mieć raczej niskie limity wielkości i nie znam żadnej konstrukcji bash, która działałaby jak plik tymczasowy, ale znika automatycznie, gdy proces się kończy.

James Scriven
źródło
Myślałem, że chcesz być w stanie „stanie akumuluj drugiej linii”, więc, na przykład, przy użyciu danych testowych Mike'a, linie M, Ni Obyłyby policzone 4, 5i 6. To nie robi tego. Moja odpowiedź brzmi (poza tym, że w obecnym wcieleniu nie działa nljako filtr). Jeśli ta odpowiedź daje pożądany wynik, to co miałeś na myśli przez „akumuluj stan w poprzek linii”? Czy chodziło Ci o to, że chcesz zachować stan tylko przez każdą sekcję, ale nie między sekcjami? (Dlaczego nie podałeś przykładu z wieloma sekcjami w swoim pytaniu?)
Scott,
@ Scott - użyj, nl -paby uzyskać M,N,O==4,5,6.
mikeserv
Zaktualizowałem pytanie, aby wyjaśnić, że jestem zainteresowany jedynie utrzymywaniem stanu w podsekcji, chociaż uważam, że druga interpretacja jest równie interesująca.
James Scriven,