Tryb Slurp w awk?

16

Narzędzia podoba sed, awklub perl -nprzetworzyć ich jedno wejście rekord w czasie, zapisylinie domyślnie.

Niektórzy, jak awkz RSGNU sedz -zlub perlze -0ooomoże zmienić typ rekordu, wybierając inny separator rekordu.

perl -nmoże sprawić, że cały plik wejściowy (każdy pojedynczy plik po przekazaniu kilku plików) będzie jednym rekordem z -0777opcją (lub -0po nim dowolna liczba ósemkowa większa niż 0377, 777 będąca kanoniczną). Tak nazywają tryb slurp .

Czy coś podobnego zrobić z awk„s RSlub jakikolwiek inny mechanizm? Gdzie awkprzetwarza zawartość każdego pliku w całości, a nie w każdym wierszu każdego pliku?

Stéphane Chazelas
źródło

Odpowiedzi:

15

Możesz zastosować różne podejścia w zależności od tego, czy awktraktuje się RSjako pojedynczy znak (jak awkrobią to tradycyjne implementacje), czy jako wyrażenie regularne (jak gawklub mawkrobią). Puste pliki są również trudne do rozważenia, ponieważ awkzwykle je pomijają.

gawk, mawkLub inne awkimplementacje, gdzie RSmoże być wyrażeniem regularnym.

W tych implementacjach (na przykład mawk, uważaj, że niektóre systemy operacyjne, takie jak Debian, dostarczają bardzo starą wersję zamiast nowoczesnej obsługiwanej przez @ThomasDickey ), jeśli RSzawiera pojedynczy znak, separatorem rekordów jest ten znak lub awkwchodzi w tryb akapitowy, gdy RSjest pusty, lub inaczej traktuje RSjako wyrażenie regularne.

Rozwiązaniem jest użycie wyrażenia regularnego, którego nie można dopasować. Niektórzy przychodzą na myśl jak x^lub $x( xprzed rozpoczęciem lub po zakończeniu). Jednak niektóre (szczególnie z gawk) są droższe niż inne. Jak dotąd uważam, że ^$jest to najbardziej wydajny. Można dopasować tylko przy pustym wejściu, ale wtedy nie byłoby nic, z czym można by się dopasować.

Więc możemy zrobić:

awk -v RS='^$' '{printf "%s: <%s>\n", FILENAME, $0}' file1 file2...

Jedynym zastrzeżeniem jest to, że pomija puste pliki (w przeciwieństwie do perl -0777 -n). Można to rozwiązać za pomocą GNU awk, umieszczając kod w ENDFILEinstrukcji. Ale musimy również zresetować $0w instrukcji BEGINFILE, ponieważ w przeciwnym razie nie zostałby zresetowany po przetworzeniu pustego pliku:

gawk -v RS='^$' '
   BEGINFILE{$0 = ""}
   ENDFILE{printf "%s: <%s>\n", FILENAME, $0}' file1 file2...

tradycyjne awkwdrożenia, POSIXawk

W nich RSjest tylko jeden znak, nie mają BEGINFILE/ ENDFILE, nie mają RTzmiennej, ogólnie też nie mogą przetwarzać znaku NUL.

Można by pomyśleć, że użycie RS='\0'może wtedy działać, ponieważ i tak nie mogą przetwarzać danych wejściowych zawierających bajt NUL, ale nie, że RS='\0'w tradycyjnych implementacjach jest traktowane jako RS=tryb akapitowy.

Jednym z rozwiązań może być użycie znaku, który prawdopodobnie nie znajdzie się w danych wejściowych, takich jak \1. W lokalizacjach znaków wielobajtowych możesz nawet sprawić, że sekwencje bajtów będą bardzo mało prawdopodobne, ponieważ tworzą one znaki, które nie są przypisane lub znaki inne niż $'\U10FFFE'w ustawieniach regionalnych UTF-8. Nie bardzo niezawodny i masz również problem z pustymi plikami.

Innym rozwiązaniem może być przechowywanie całego wejścia w zmiennej i przetwarzanie go w instrukcji END na końcu. Oznacza to jednak, że możesz przetwarzać tylko jeden plik na raz:

awk '{content = content $0 RS}
     END{$0 = content
       printf "%s: <%s>\n", FILENAME, $0
     }' file

To odpowiednik sed:

sed '
  :1
  $!{
   N;b1
  }
  ...' file1

Kolejny problem z tym podejściem jest to, że jeśli plik nie kończy się znakiem nowej linii (i nie była pusta), jeden jest wciąż arbitralnie dodane $0na końcu (z gawk, można obejść, że stosując RTzamiast RSw kod powyżej). Jedną z zalet jest to, że masz rekord liczby linii w pliku w NR/ FNR.

Stéphane Chazelas
źródło
co do ostatniej części („jeśli plik nie kończył się znakiem nowej linii (i nie był pusty), jeden jest nadal arbitralnie dodawany w 0 $ na końcu”): w przypadku plików tekstowych powinny one mieć zakończenie Nowa linia. vi dodaje na przykład jeden, a tym samym modyfikuje plik podczas jego zapisywania. Brak nowej linii powoduje, że niektóre polecenia odrzucają ostatnią „linię” (np. Wc), ale inne nadal „widzą” ostatnią linię ... ymmv. Twoje rozwiązanie jest zatem ważne, imo, jeśli masz traktować pliki tekstowe (co prawdopodobnie jest tak, ponieważ awk jest dobry do przetwarzania tekstu, ale nie tak dobry do plików binarnych ^^)
Olivier Dulac
1
próba włamania się do środka może mieć pewne ograniczenia ... tradycyjny awk miał najwyraźniej (miał?) limit 99 pól w linii ... więc może być konieczne użycie innego FS, aby uniknąć tego limitu, ale możesz mają również ograniczenia dotyczące długości całkowitej długości linii (lub całej rzeczy, jeśli uda ci się uzyskać wszystko na jednej linii)?
Olivier Dulac
w końcu: (głupiutki ...) hack może polegać na 1. parsowaniu całego pliku i szukaniu znaku, którego nie ma, a następnie tr '\n' 'thatchar' pliku przed wysłaniem go do awk i tr 'thatchar' \n'wyniku? (może być konieczne dołączenie nowego wiersza, aby upewnić się, jak wspomniano powyżej, że plik wejściowy ma kończący znak nowego wiersza: { tr '\n' 'missingchar' < thefile ; printf "\n" ;} | awk ..... | { tr 'missingchar' '\n' }(ale na końcu dodajemy znak „\ n”, którego może być konieczne pozbycie się ... może dodanie sed przed końcowym tr? jeśli ten tr akceptuje pliki bez kończenia nowego wiersza ...)
Olivier Dulac
@OlivierDulac, limit liczby pól zostałby osiągnięty tylko wtedy, gdybyśmy mieli dostęp do NF lub dowolnego pola. awknie dokonuje podziału, jeśli tego nie zrobimy. To powiedziawszy, nawet /bin/awkSolaris 9 (oparty na awk z lat 70.) nie miał tego ograniczenia, więc nie jestem pewien, czy możemy znaleźć takie, które ma (wciąż możliwe, ponieważ oawk SVR4 miał limit 99 i nawk 199, więc jest to prawdopodobnie zniesienie tego limitu zostało dodane przez Sun i może nie być znalezione w innych awksach opartych na SVR4, czy możesz przetestować w systemie AIX?).
Stéphane Chazelas