bash znajdź linie zaczynające się od łańcucha

10

Mam kilka plików i chcę znaleźć, który zawiera sekwencyjne linie zaczynające się od określonego ciągu.

Na przykład dla następującego pliku:

Aaaaaaaaaaaa
Baaaaaaaaaaa
Cxxxxxxxxx
Cyyyyyyyyy
Czzzzzzzzz
Abbbbbbbbbbb
Bbbbbbbbbbbb
Caaaaaa
Accccccccccc
Bccccccccccc
Cdddddd
Ceeeeee

Istnieje więcej niż jedna linia rozpoczynająca się od „C”, więc chcę, aby ten plik został znaleziony za pomocą polecenia.
Na przykład dla następującego pliku:

Aaaaaaaaaaaa
Baaaaaaaaaaa
Cxxxxxxxxx
Abbbbbbbbbbb
Bbbbbbbbbbbb
Caaaaaa
Accccccccccc
Bccccccccccc
Cdddddd

Zawsze jest jedna linia zaczynająca się od „C”, nie chcę tego pliku. Myślałem o użyciu a greplub a, sedale nie wiem dokładnie, jak to zrobić. Może za pomocą wyrażenia regularnego ^C.*$^Club czegoś takiego. Dowolny pomysł ?

Jérémie
źródło
W Cdrugim przykładzie zaczynają się dwie linie .
cuonglm
5
To pytanie jest niejasne. Szukasz plików, które mają więcej niż jeden kolejny wiersz rozpoczynający się C?
Graeme
Tak, tego właśnie chcę. Przepraszam za nieporozumienie.
Jérémie,
2
@terdon, wygląda na to, że wyszukiwania wieloliniowe z opcją -P działały do ​​2.5.4 i już nie później, chociaż w dzienniku zmian nie mogę znaleźć niczego, co wyjaśniałoby dlaczego.
Stéphane Chazelas
1
@Graeme możesz cofnąć usunięcie swojej odpowiedzi, zobacz komentarz Stephane'a, najwyraźniej działa to w przypadku niektórych starszych grepwersji.
terdon

Odpowiedzi:

5

Z pcregrep:

pcregrep -rMl '^C.*\nC' .

POSIXly:

find . -type f -exec awk '
  FNR==1 {last=0; printed=0; next}
  printed {next}
  /^C/ {if (last) {print FILENAME; printed=1; nextfile} else last=1; next}
  {last=0}' {} +

(choć oznacza to, że w pełni odczytuje wszystkie pliki z tymi awkimplementacjami, które nie obsługują nextfile).


W wersjach GNU grepdo 2.5.4:

grep -rlP '^C.*\nC' .

wydaje się działać, ale jest to przypadek i nie ma gwarancji, że zadziała.

Zanim zostało to naprawione w 2.6 (przez to zatwierdzenie ), GNU grepprzeoczyło, że używana funkcja wyszukiwania pcre pasuje do całego aktualnie przetwarzanego bufora grep, powodując różnego rodzaju zaskakujące zachowanie. Na przykład:

grep -P 'a\s*b'

pasuje do pliku zawierającego:

bla
bla

To by pasowało:

printf '1\n2\n' | grep -P '1\n2'

Ale to:

(printf '1\n'; sleep 1; printf '2\n') | grep -P '1\n2'

Lub:

(yes | head -c 32766; printf '1\n2\n') > file; grep -P '1\n2' file

nie zrobiłby tego (tak jak 1\n2\nw dwóch przetwarzanych buforach grep).

Zachowanie to zostało jednak udokumentowane:

15- Jak mogę dopasować między liniami?

Standardowy grep nie może tego zrobić, ponieważ jest zasadniczo oparty na linii. Dlatego samo użycie klasy znaków „[: space:]” nie odpowiada nowym wierszom w sposób, jakiego można się spodziewać. Jednak jeśli twój grep jest skompilowany z włączonymi wzorami Perla, możesz użyć modyfikatora Perla (który sprawia, że ​​„.” Pasuje do nowych linii):

     printf 'foo\nbar\n' | grep -P '(?s)foo.*?bar'

Po tym, jak został naprawiony w wersji 2.6, dokumentacja nie została zmieniona (raz go tam zgłosiłem ).

Stéphane Chazelas
źródło
Czy jest jakiś powód, aby nie używać exiti -exec \;zamiast nextfile?
terdon
@terdon, oznaczałoby to uruchomienie jednego awkna plik. Zrobisz to tylko wtedy, awkgdy nie obsługujesz nextfilei masz dużą część plików, które są duże i mają pasujące linie na początku pliku.
Stéphane Chazelas
Co powiesz na tę technikę grep (myślę, że z nowszymi wersjami GNU grep), która ułatwia dopasowanie wieloliniowe, sprawiając, że cały plik wygląda jak pojedynczy ciąg znaków, ustawiając terminator linii na NUL - czy byłbyś świadomy, czy istnieją jakieś ograniczenia?
iruvar
1
@ 1_CR, To załadowałoby cały plik do pamięci, jeśli nie ma tam znaku NUL i zakłada, że ​​linie nie zawierają znaków NUL. Zauważ też, że starsze wersje GNU grep (które ma OP) nie mogą używać -zz -P. Nie ma \Nbez tego -P, musisz go napisać, $'[\01-\011\013-\0377]'który działałby tylko w lokalizacjach C (patrz thread.gmane.org/gmane.comp.gnu.grep.bugs/5187 )
Stéphane Chazelas
@StephaneChazelas, bardzo użyteczny szczegół, dzięki
iruvar
2

Z awk:

awk '{if (p ~ /^C/ && $1 ~ /^C/) print; p=$1}' afile.txt

Spowoduje to wydrukowanie zawartości pliku, jeśli są kolejne wiersze zaczynające się od C. Wyrażenie (p ~ /^C/ && $1 ~ /^C/)zajrzy do kolejnych linii w pliku i oceni, czy pierwszy znak w obu jest zgodny C. W takim przypadku linia zostanie wydrukowana.

W celu znalezienia wszystkich plików, które mają taki wzór, można uruchomić powyższą awk poprzez findkomendy:

find /your/path -type f -exec awk '{if (p ~ /^C/ && $1 ~ /^C/) {print FILENAME; exit;} p=$1}' {} \;

W tym poleceniu find+ execprzejdzie przez każdy z plików i przeprowadzi podobne awkfiltrowanie na każdym pliku i wydrukuje jego nazwę, FILENAMEjeśli wyrażenie awk zostanie ocenione jako prawda. Aby uniknąć FILENAMEwielokrotnego drukowania pojedynczego pliku z wieloma dopasowaniami, exitużywana jest instrukcja (dzięki @terdon).

mkc
źródło
Moje pytanie nie było wystarczająco jasne, chcę poznać nazwę plików z więcej niż jedną kolejną linią, zaczynając odC
Jérémie
@ Jérémie Zaktualizowałem swoją odpowiedź.
mkc
Czy możesz dodać wyjaśnienie, jak to działa? Ponadto nie ma takiej potrzeby flag, tylko exitzamiast tego. W ten sposób nie musisz kontynuować przetwarzania plików po znalezieniu dopasowania.
terdon
2

Jeszcze jedna opcja z GNU sed:

W przypadku pojedynczego pliku:

sed -n -- '/^C/{n;/^C/q 1}' "$file" || printf '%s\n' "$file"

(chociaż zgłosi również pliki, których nie może odczytać).

Dla find:

find . -type f ! -exec sed -n '/^C/{n;/^C/q 1}' {} \; -print

Problemu z drukowaniem nieczytelnych plików można uniknąć, pisząc je:

find . -type f -size +2c -exec sed -n '$q1;/^C/{n;/^C/q}' {} \; -print
wysypka
źródło
Czy możesz opisać szczegółowo sed -n '$q1;/^C/{n;/^C/q}'?
Jérémie
Czy ktoś mi to wyjaśni?
Jérémie
@ Jérémie $q1- zmusza sed do wyjścia z błędem, jeśli wzorzec nie zostanie znaleziony. Zakończy się również błędem, jeśli coś jest nie tak z plikiem (jest nieczytelne lub uszkodzone). Więc wyjdzie z 0 statusem wyjścia tylko w przypadku znalezienia wzoru i zostanie przekazany do drukowania. Część z /^C/{n;/^C/qjest dość prosta. Jeśli znajdzie ciąg rozpoczynający się od C, odczyta następny wiersz, a jeśli zacznie także od C, wyjdzie ze stanu zerowego wyjścia.
pędzi
1

Zakładając, że twoje pliki są wystarczająco małe, aby można je było wczytać do pamięci:

perl -000ne 'print "$ARGV\n" if /^C[^\n]*\nC/sm' *

Wyjaśnienie:

  • - 000: ustawiony \n\njako separator rekordów, włącza tryb akapitowy, który będzie traktował akapity (oddzielone kolejnymi znakami nowej linii) jako pojedyncze linie.
  • -ne: zastosuj skrypt podany jako argument -edo każdego wiersza plików wejściowych.
  • $ARGV : jest aktualnie przetwarzanym plikiem
  • /^C[^\n]*\nC/: dopasuj Cna początku wiersza (zobacz opis smmodyfikatorów poniżej, dlaczego to działa tutaj), a następnie 0 lub więcej znaków innych niż nowa linia, nowa linia, a następnie kolejne C. Innymi słowy, znajdź kolejne linie zaczynające się od C. * //sm: te modyfikatory dopasowania są (jak udokumentowano [tutaj]):

    • m : Traktuj ciąg jako wiele linii. Oznacza to, że zmień „^” i „$” z dopasowywania początku lub końca wiersza tylko na lewym i prawym końcu ciągu na dopasowywanie ich w dowolnym miejscu ciągu.

    • s : Traktuj ciąg jako pojedynczą linię. Oznacza to zmianę „”. aby dopasować dowolny znak, nawet nowy wiersz, który normalnie nie pasowałby.

Możesz także zrobić coś brzydkiego jak:

for f in *; do perl -pe 's/\n/%%/' "$f" | grep -q 'C[^%]*%%C' && echo "$f"; done

Tutaj perlkod zastępuje znaki nowej linii, %%więc zakładając, że nie masz %%w pliku wejściowym (duży, jeśli oczywiście), grepdopasuje kolejne wiersze zaczynające się od C.

terdon
źródło
1

ROZWIĄZANIE:

( set -- *files ; for f ; do (
set -- $(printf %c\  `cat <$f`)
while [ $# -ge 1 ] ;do [ -z "${1#"$2"}" ] && {
    echo "$f"; break ; } || shift
done ) ; done )

PRÓBNY:

Najpierw stworzymy bazę testową:

abc="a b c d e f g h i j k l m n o p q r s t u v w x y z" 
for l in $abc ; do { i=$((i+1)) h= c= ;
    [ $((i%3)) -eq 0 ] && c="$l" h="${abc%"$l"*}"
    line="$(printf '%s ' $h $c ${abc#"$h"})"
    printf "%s$(printf %s $line)\n" $line >|/tmp/file${i}
} ; done

Powyżej tworzy 26 plików /tmpo nazwie file1-26. W każdym pliku znajduje się 27 lub 28 linii rozpoczynających się od liter, a-zpo których następuje reszta alfabetu. Co trzeci plik zawiera dwa kolejne wiersze, w których pierwszy znak jest duplikowany.

PRÓBA:

cat /tmp/file12
...
aabcdefghijkllmnopqrstuvwxyz
babcdefghijkllmnopqrstuvwxyz
cabcdefghijkllmnopqrstuvwxyz
...
kabcdefghijkllmnopqrstuvwxyz
labcdefghijkllmnopqrstuvwxyz
labcdefghijkllmnopqrstuvwxyz
mabcdefghijkllmnopqrstuvwxyz
...

A kiedy się zmieniam:

set -- *files

do:

set -- /tmp/file[0-9]*

Dostaję...

WYNIK:

/tmp/file12
/tmp/file15
/tmp/file18
/tmp/file21
/tmp/file24
/tmp/file3
/tmp/file6
/tmp/file9

Krótko mówiąc, rozwiązanie działa w ten sposób:

sets pozycjonuje podpowłokę do wszystkich plików i dla każdego z nich

sets pozycjonuje zagnieżdżoną podpowłokę do pierwszej litery każdego wiersza w każdym pliku, gdy jest zapętlony.

[ tests ]jeśli $1neguje $2wskazanie dopasowania, a jeśli tak

echoesnazwa pliku następnie breaks obecny iteracji

else shifts do następnego pojedynczego znaku pozycyjnych, aby spróbować ponownie

mikeserv
źródło
0

Ten skrypt używa grepi, cutaby uzyskać numery pasujących pasujących linii, i sprawdza, czy są jakieś dwie kolejne liczby. Przyjmuje się, że plik jest poprawną nazwą pliku przekazaną jako pierwszy argument do skryptu:

#!/bin/bash

checkfile () {
 echo checking $1
 grep -n -E "^C.*$" $1 | cut -d: -f1 | while read linenum
     do
        : $[ ++PRV ] 
        if [ $linenum == $PRV ]; then return 1; fi
        PRV=$linenum
     done
     return 0
}

PRV="-1"
checkfile $1
if [ $? == 0 ]; then
   echo Consecutive matching lines found in file $1
fi
Michael Martinez
źródło