bash найти строки, начинающиеся со строки

10

У меня есть куча файлов, и я хочу найти, какой из них содержит последовательные строки, начинающиеся с определенной строки.

Например, для следующего файла:

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

Существует более одной строки, начинающейся с 'C', поэтому я хочу, чтобы этот файл находился по команде.
Например, для следующего файла:

Aaaaaaaaaaaa
Baaaaaaaaaaa
Cxxxxxxxxx
Abbbbbbbbbbb
Bbbbbbbbbbbb
Caaaaaa
Accccccccccc
Bccccccccccc
Cdddddd

Всегда есть одна строка, начинающаяся с 'C', я не хочу этот файл. Я думал об использовании grepили, sedно я не знаю точно, как это сделать. Может быть, с помощью регулярного выражения ^C.*$^Cили что-то в этом роде. Есть идеи ?

Жереми
источник
Во Cвтором примере начинаются две строки .
cuonglm
5
Этот вопрос неясен. Вы ищете файлы, которые имеют более одной последовательной строки, начиная с C?
Грэм
Да, это то, что я хочу. Извините за недопонимание.
Жереми
2
@terdon, похоже, что многострочный поиск с -P работал до 2.5.4 и больше не после этого, хотя я не могу найти в журнале изменений ничего, что объясняло бы почему.
Стефан Шазелас
1
@ Возможно, вы можете восстановить свой ответ, см. Комментарий Стефана, очевидно, он работает для некоторых старых grepверсий.
Тердон

Ответы:

5

С 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}' {} +

(хотя это означает полное чтение всех файлов с теми awkреализациями, которые не поддерживают nextfile).


С версиями GNU grepдо 2.5.4:

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

кажется, что работает, но это случайно, и это не гарантировано, чтобы работать.

Прежде, чем это было исправлено в 2.6 (с помощью этого коммита ), GNU grepупустила из виду, что используемая им функция поиска pcre будет соответствовать всему буферу, обрабатываемому в данный момент grep, вызывая все виды неожиданного поведения. Например:

grep -P 'a\s*b'

будет соответствовать на файле, содержащем:

bla
bla

Это будет соответствовать:

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

Но это:

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

Или:

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

не будет (так как 1\n2\nобрабатывается через два буфера grep).

Это поведение в итоге было задокументировано, хотя:

15- Как я могу сопоставить между строк?

Стандартный grep не может этого сделать, так как он основан на строках. Поэтому простое использование класса символов '[: space:]' не соответствует символу новой строки, как вы ожидаете. Однако, если ваш grep скомпилирован с включенными шаблонами Perl, можно использовать модификатор Perl '(который заставляет'. 'Соответствовать символам новой строки):

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

После того, как это было исправлено в 2.6, документация не была изменена (я однажды сообщил об этом там ).

Стефан Шазелас
источник
Есть ли причина не использовать exitи -exec \;вместо nextfile?
Тердон
@terdon, это будет означать запуск по одному awkна файл. Вы захотите сделать это только в том случае, если ваш файл awkне поддерживает, nextfileи у вас есть большая часть файлов, которые имеют большой размер и совпадают с линиями в начале файла.
Стефан Шазелас
Как насчет этого метода grep (я полагаю, с более поздними версиями GNU grep), который облегчает многострочные совпадения, делая весь файл похожим на одну строку, устанавливая терминатор строки в NUL - вы бы знали, есть ли какие-то ограничения?
iruvar
1
@ 1_CR, Это загрузит весь файл в память, если там нет символов NUL, и предполагается, что строки не содержат символов NUL. Также обратите внимание, что более старые версии GNU grep (который есть у OP) не могут использоваться -zс -P. Там нет \Nбез -P, вы должны были бы написать ее , $'[\01-\011\013-\0377]'которая будет работать только в локалей C (см thread.gmane.org/gmane.comp.gnu.grep.bugs/5187 )
Stéphane Chazelas
@StephaneChazelas, очень полезная деталь, спасибо
iruvar
2

С awk:

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

Это напечатает содержимое файла, если есть последовательные строки, начинающиеся с C. Выражение (p ~ /^C/ && $1 ~ /^C/)будет смотреть на последовательные строки в файле и будет иметь значение true, если первый символ в обоих совпадениях C. Если это так, строка будет напечатана.

Чтобы найти все файлы с таким шаблоном, вы можете запустить приведенный выше awk с помощью findкоманды:

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

В этой команде find+ execбудет проходить через каждый из файлов и выполнять аналогичную awkфильтрацию для каждого файла и печатать его имя, FILENAMEесли выражение awk оценивается как true. Чтобы не печатать FILENAMEнесколько раз для одного файла с несколькими совпадениями, используется exitоператор (спасибо @terdon).

MKC
источник
Мой вопрос не был достаточно ясен, я хочу знать имена файлов с несколькими последовательными строками, начиная сC
Jérémie
@ Jérémie Я обновил свой ответ.
MKC
Не могли бы вы добавить объяснение, как это работает? Кроме того, нет необходимости flag, просто exitвместо этого. Таким образом, вам не нужно продолжать обработку файлов после того, как совпадение было найдено.
Тердон
2

Еще один вариант с GNU sed:

Для одного файла:

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

(хотя он также сообщит о файлах, которые он не может прочитать).

Для find:

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

Проблемы с нечитаемыми файлами при печати можно избежать, написав это:

find . -type f -size +2c -exec sed -n '$q1;/^C/{n;/^C/q}' {} \; -print
порыв
источник
Можете ли вы подробно описать sed -n '$q1;/^C/{n;/^C/q}'?
Жереми
Кто-нибудь, чтобы объяснить мне?
Жереми
@ Jérémie $q1- вынуждает sed завершиться с ошибкой, если шаблон не найден. Он также завершится с ошибкой, если что-то не так с файлом (он не читается или поврежден). Таким образом, он выйдет с нулевым статусом выхода только в том случае, если шаблон найден и будет передан на печать. Расстаться с /^C/{n;/^C/qдовольно просто. Если он находит строку, которая начинается с C, он будет читать следующую строку, а если он также начинается с C, он выйдет с нулевым статусом выхода.
Раш
1

Предполагая, что ваши файлы достаточно малы для чтения в память:

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

Объяснение:

  • - 000: установить \n\nв качестве разделителя записей, это включает режим абзаца, который будет обрабатывать абзацы (разделенные последовательными символами новой строки) как отдельные строки.
  • -ne: применить скрипт, указанный в качестве аргумента, к -eкаждой строке входного файла (ов).
  • $ARGV : файл обрабатывается в данный момент
  • /^C[^\n]*\nC/: совпадение Cв начале строки (см. описание smмодификаторов ниже, чтобы узнать, почему это работает здесь), за которым следуют 0 или более символов, не являющихся символом новой строки, символ новой строки и затем еще один C. Другими словами, найдите последовательные строки, начинающиеся с C. * //sm: эти модификаторы соответствия (как описано [здесь]):

    • m : рассматривать строку как несколько строк. Таким образом, измените «^» и «$», сопоставляя начало или конец строки только в левом и правом концах строки, чтобы сопоставлять их в любом месте строки.

    • s : рассматривать строку как одну строку. То есть изменить "." соответствовать любому символу, даже символу новой строки, который обычно не соответствует.

Вы также можете сделать что-то ужасное, как:

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

Здесь perlкод заменяет символы новой строки на %%so, так что, если у вас нет %%входного файла ( если, конечно, большого ), они grepбудут соответствовать последовательным строкам, начинающимся с C.

Тердон
источник
1

РЕШЕНИЕ:

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

DEMO:

Сначала мы создадим тестовую базу:

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

Выше создается 26 файлов в /tmpименованных file1-26. В каждом файле 27 или 28 строк, начинающихся с букв a-zи сопровождаемых остальной частью алфавита. Каждый третий файл содержит две последовательные строки, в которых дублируется первый символ.

ОБРАЗЕЦ:

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

И когда я меняюсь:

set -- *files

чтобы:

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

Я получил...

ВЫВОД:

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

Итак, вкратце, решение работает так:

setS позиционирует подоболочку для всех ваших файлов, и для каждого

setS positionals вложенной подоболочка к первой букве каждой строки в каждом файле , как это петлями.

[ tests ]если $1отрицает, $2указывая на совпадение, и если так

echoesимя файла , то break˙s текущей итерации цикла

остальное shifts к следующему позиционного одного персонажа , чтобы попробовать еще раз

mikeserv
источник
0

Этот скрипт использует grepи cutдля получения номеров строк совпадающих строк и проверяет любые два последовательных номера. Предполагается, что файл является допустимым именем файла, переданным в качестве первого аргумента скрипту:

#!/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
Майкл Мартинес
источник