Как выполнить grep-inverse-match и исключить строки «до» и «после»

26

Рассмотрим текстовый файл со следующими записями:

aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
iii

Учитывая шаблон (например fff), я хотел бы grep файл выше, чтобы получить в выводе:

all_lines except (pattern_matching_lines  U (B lines_before) U (A lines_after))

Например, если B = 2и A = 1, вывод с pattern = fffдолжен быть:

aaa
bbb
ccc
hhh
iii

Как я могу сделать это с помощью grep или других инструментов командной строки?


Обратите внимание, когда я пытаюсь:

grep -v 'fff'  -A1 -B2 file.txt

Я не понимаю, чего хочу. Я вместо этого получаю:

aaa
bbb
ccc
ddd
eee
fff
--
--
fff
ggg
hhh
iii
Амелио Васкес-Рейна
источник

Ответы:

9

don может быть лучше в большинстве случаев, но только в том случае, если файл действительно большой, и вы не можете sedобработать файл сценария такого большого размера (который может иметь место в более чем 5000 строк сценария) , вот это с простым sed:

sed -ne:t -e"/\n.*$match/D" \
    -e'$!N;//D;/'"$match/{" \
            -e"s/\n/&/$A;t" \
            -e'$q;bt' -e\}  \
    -e's/\n/&/'"$B;tP"      \
    -e'$!bt' -e:P  -e'P;D'

Это пример того, что называется скользящим окном при вводе. Он работает путем создания упреждающего буфера $Bстрок -count, прежде чем пытаться что-либо напечатать.

И на самом деле, вероятно, я должен уточнить мой предыдущий пункт: основной ограничитель производительности как для этого решения, так и для Дон будет напрямую связан с интервалом. Это решение будет замедляться с большими интервальными размерами , в то время как Дон замедлится с большими интервальными частотами . Другими словами, даже если входной файл очень большой, если фактический интервал встречается все еще очень редко, то его решение, вероятно, является путем. Однако, если размер интервала относительно управляем и, вероятно, встречается часто, то это решение, которое вы должны выбрать.

Итак, вот рабочий процесс:

  • Если $matchон найден в пространстве шаблона, которому предшествует \newline, sedрекурсивно Dудалит каждую \newline, которая ему предшествует.
    • Раньше $matchя полностью очищал пространство шаблонов - но чтобы легко справиться с перекрытием, оставление ориентира, кажется, работает намного лучше.
    • Я также попытался s/.*\n.*\($match\)/\1/сделать это за один раз и уклониться от цикла, но когда он $A/$Bбольшой, Dцикл Elete оказывается значительно быстрее.
  • Затем мы вытягиваем Nстроку ввода ext, которой предшествует \nразделитель ewline, и снова Dпытаемся выбрать /\n.*$match/один раз, ссылаясь на наше последнее использованное регулярное выражение w / //.
  • Если пространство образца совпадает, $matchто это может быть сделано только в начале строки $match- все $Bпредыдущие строки были очищены.
    • Итак, мы начинаем $Aциклы после.
    • Каждый запуск этого цикла мы будем пытаться s///ubstitute для &себя $Aго \nсимвола ewline в пространстве картины, и, в случае успеха, tЭст произрастет нас - и весь наш $Aосле буфер - из сценария полностью запустить скрипт через сверху со следующей строкой ввода, если есть.
    • Если test не увенчался успехом, мы bвернемся к :tметке op и вернемся к другой строке ввода - возможно, начнем цикл снова, если $matchпроизойдет при сборе $After.
  • Если пройти в $matchпетлю функции, то мы будем стараться pРинт в $последнюю строку , если это, и если !не пытаться s///ubstitute для &себя $Bго \newline характер в пространстве картины.
    • Мы тоже tэто сделаем , и если это будет успешно, мы перейдем к :Pметке rint.
    • Если нет, мы вернемся к :top и добавим еще одну строку ввода в буфер.
  • Если мы сделаем это для того, чтобы :Pнабрать текст, мы будем Pпечатать, а затем Dподняться до первой \nстроки в шаблонном пространстве и перезапустить сценарий сверху с оставшимся.

И вот на этот раз, если бы мы делали A=2 B=2 match=5; seq 5 | sed...

Пространство шаблона для первой итерации в :Print будет выглядеть так:

^1\n2\n3$

И вот как sedсобирает свой $Before буфер. И поэтому sedпечатает в выходные $Bстроки -count за собранным вводом. Это означает, что, учитывая наш предыдущий пример, sedбудет Pвыведен 1вывод, а затем его Dвыбор и отправка обратно в верхнюю часть скрипта пространства шаблона, которое выглядит следующим образом:

^2\n3$

... и в верхней части скрипта Nизвлекается строка ввода ext, поэтому следующая итерация выглядит следующим образом:

^2\n3\n4$

И поэтому, когда мы находим первое вхождение 5во входных данных, пространство шаблона на самом деле выглядит так:

^3\n4\n5$

Затем Dвключается цикл Elete, и когда он проходит, это выглядит так:

^5$

И когда Nстрока ввода ext sedвыдвигается, нажимает EOF и выходит. К тому времени в нем были только когда-либо Pнабранные линии 1 и 2.

Вот пример запуска:

A=8 B=7 match='[24689]0'
seq 100 |
sed -ne:t -e"/\n.*$match/D" \
    -e'$!N;//D;/'"$match/{" \
            -e"s/\n/&/$A;t" \
            -e'$q;bt' -e\}  \
    -e's/\n/&/'"$B;tP"      \
    -e'$!bt' -e:P  -e'P;D'

Это печатает:

1
2
3
4
5
6
7
8
9
10
11
12
29
30
31
32
49
50
51
52
69
70
71
72
99
100
mikeserv
источник
Я на самом деле работаю с огромными файлами, и ответ Дона был заметно медленнее, чем это решение. Сначала я не решался изменить свой принятый ответ, но разница в скорости вполне заметна.
Амелио Васкес-Рейна
4
@Amelio - это будет работать с потоком любого размера, и ему не нужно читать файл для работы. Самый большой фактор производительности - это размер $Aи / или $B. Чем больше вы сделаете эти цифры, тем медленнее они станут - но вы можете сделать их достаточно большими.
mikeserv
1
@ AmelioVazquez-Reina - если вы используете старую версию, я думаю, это лучше.
mikeserv
11

Вы можете использовать gnu grepс -Aи -Bдля печати именно те части файла, которые вы хотите исключить, но добавьте -nпереключатель, чтобы также печатать номера строк, а затем отформатировать вывод и передать его как командный сценарий sedдля удаления этих строк:

grep -n -A1 -B2 PATTERN infile | \
sed -n 's/^\([0-9]\{1,\}\).*/\1d/p' | \
sed -f - infile

Это также должно работать с файлами шаблонов, передаваемыми grepчерез, -fнапример:

grep -n -A1 -B2 -f patterns infile | \
sed -n 's/^\([0-9]\{1,\}\).*/\1d/p' | \
sed -f - infile

Я думаю, что это можно было бы немного оптимизировать, если бы он объединял любые три или более последовательных номеров строк в диапазоны, например, 2,6dвместо 2d;3d;4d;5d;6d... хотя, если на входе есть только несколько соответствий, делать это не стоит.


Другие способы, которые не сохраняют порядок строк и, скорее всего, медленнее:
with comm:

comm -13 <(grep PATTERN -A1 -B2 <(nl -ba -nrz -s: infile) | sort) \
<(nl -ba -nrz -s: infile | sort) | cut -d: -f2-

commтребует отсортированного ввода, что означает, что порядок строк не будет сохранен в конечном выводе (если ваш файл уже не отсортирован), поэтому nlиспользуется для нумерации строк перед сортировкой, comm -13печатает только строки, уникальные для 2-го ФАЙЛА, а затем cutудаляет часть, которая была добавлена nl(то есть первое поле и разделитель :)
с join:

join -t: -j1 -v1 <(nl -ba -nrz -s:  infile | sort) \
<(grep PATTERN -A1 -B2 <(nl -ba -nrz -s:  infile) | sort) | cut -d: -f2-
don_crissti
источник
Спасибо Дон! Быстрый вопрос, ожидаете ли вы, что решение с commболее быстрым решением, чем оригинальное с sedи grep?
Амелио Васкес-Рейна
1
@ AmelioVazquez-Reina - я так не думаю, поскольку он все еще читает входной файл дважды (плюс выполняет некоторую сортировку), в отличие от решения Майка, которое обрабатывает файл только один раз.
don_crissti
9

Если вы не возражаете против использования vim:

$ export PAT=fff A=1 B=2
$ vim -Nes "+g/${PAT}/.-${B},.+${A}d" '+w !tee' '+q!' foo
aaa
bbb
ccc
hhh
iii
  • -Nesвключает несовместимый бесшумный режим ex. Полезно для написания сценариев.
  • +{command}скажите vim запустить {command}файл.
  • g/${PAT}/- на всех совпадающих линиях /fff/. Это сложно, если шаблон содержит специальные символы регулярного выражения, которые вы не намеревались обрабатывать таким образом.
  • .-${B} - с 1 строки над этой
  • .+${A}- на 2 строки ниже этой (см. :he cmdline-rangesдля этих двух)
  • d удалить строки.
  • +w !tee затем пишет в стандартный вывод.
  • +q! выходит без сохранения изменений.

Вы можете пропустить переменные и использовать шаблон и числа напрямую. Я использовал их только для ясности цели.

Мур
источник
3

Как насчет (используя GNU grepи bash):

$ grep -vFf - file.txt < <(grep -B2 -A1 'fff' file.txt)
aaa
bbb
ccc
hhh
iii

Здесь мы находим строки, от которых нужно отказаться grep -B2 -A1 'fff' file.txt, а затем используем это как входной файл, чтобы найти нужные строки, отбрасывающие их.

heemayl
источник
Хм, это ничего не выводит на мою машину (OS X)
Амелио Васкес-Рейна
@ AmelioVazquez-Reina извините за это .. я не знал вашей ОС раньше .. во
всяком случае,
2
Это будет иметь ту же проблему, что kosи (теперь удаленное) решение, как если бы во входном файле были повторяющиеся строки, и некоторые из них выходили за пределы диапазона, а другие находились внутри этого диапазона, и все они будут удалены. Кроме того, при множественном вхождении шаблона , если есть строки, как --во входном файле (за пределами диапазонов), это приведет к их удалению, потому что --в grepвыходных данных появляется разделитель, когда более чем одна строка соответствует шаблону (последняя крайне маловероятна, но стоит упоминание, я думаю).
don_crissti
@don_crissti Спасибо .. вы правы .. хотя я буквально взял пример ОП .. я собираюсь оставить его на тот случай, если кто-то посчитает его полезным позже ...
Heemayl
1

Вы можете получить достаточно хороший результат, используя временные файлы:

my_file=file.txt #or =$1 if in a script

#create a file with all the lines to discard, numbered
grep -n -B1 -A5 TBD "$my_file" |cut -d\  -f1|tr -d ':-'|sort > /tmp/___"$my_file"_unpair

#number all the lines
nl -nln "$my_file"|cut -d\  -f1|tr -d ':-'|sort >  /tmp/___"$my_file"_all

#join the two, creating a file with the numbers of all the lines to keep
#i.e. of those _not_ found in the "unpair" file
join -v2  /tmp/___"$my_file"_unpair /tmp/___"$my_file"_all|sort -n > /tmp/___"$my_file"_lines_to_keep

#eventually use these line numbers to extract lines from the original file
nl -nln $my_file|join - /tmp/___"$my_file"_lines_to_keep |cut -d\  -f2- > "$my_file"_clean

Результат достаточно хорош, потому что вы можете потерять некоторые отступы в процессе, но если это файл, не чувствительный к XML или отступам, это не должно быть проблемой. Поскольку этот сценарий использует ram-диск, запись и чтение этих временных файлов выполняется так же быстро, как и работа в памяти.

RafDouglas
источник
1

Также, если вы просто хотите исключить некоторые строки перед указанным маркером, вы можете использовать:

awk -v nlines=2 '/Exception/ {for (i=0; i<nlines; i++) {getline}; next} 1'

(Гленн Джекман на /programming//a/1492538 )

При помощи некоторых команд вы можете получить поведение до / после:

awk -v nlines_after=5 '/EXCEPTION/ {for (i=0; i<nlines_after; i++) {getline};print "EXCEPTION" ;next} 1' filename.txt|\
tac|\
awk -v nlines_before=1 '/EXCEPTION/ {for (i=0; i<nlines_before; i++) {getline}; next} 1'|\
tac
RafDouglas
источник
1
Блестяще, используйте awkобратный файл, чтобы обрабатывать следующие строки, когда вы хотите повлиять на строки до и перевернуть результат.
Кармаказе
0

Один из способов сделать это, возможно, самый простой - создать переменную и выполнить следующие действия:

grep -v "$(grep "fff" -A1 -B2 file.txt)" file.txt

Таким образом, у вас все еще есть ваша структура. И вы можете легко увидеть из одного вкладыша, что вы пытаетесь удалить.

$ grep -v "$(grep "fff" -A1 -B2 file.txt)" file.txt
aaa
bbb
ccc
hhh
iii
lordpavel
источник
То же решение, что и для heemayl, и та же проблема, что описана don_crissti: Это будет иметь ту же проблему, что и решение kos (теперь удаленное), как если бы во входном файле были повторяющиеся строки, и некоторые из них выходили за пределы диапазона, а другие находились внутри этого диапазона это удалит их всех. Кроме того, при многократном вхождении шаблона, если во входном файле (за пределами диапазонов) есть такие строки, как это, они будут удалены, потому что разделитель - появляется в выводе grep, когда более чем одна строка соответствует шаблону (последняя сильно вряд ли, но стоит упомянуть, я думаю).
Бодо Тизен
0

Если есть только 1 совпадение:

A=1; B=2; n=$(grep -n 'fff' file.txt | cut -d: -f1)
head -n $((n-B-1)) file.txt ; tail -n +$((n+A+1)) file.txt

В противном случае (awk):

# -vA=a -vB=b -vpattern=pat must be provided
BEGIN{

    # add file again. assume single file
    ARGV[ARGC]=ARGV[ARGC-1]
    ++ARGC
}

# the same as grep -An -Bn pattern
FNR==NR && $0 ~ pattern{
    for (i = 0; i <= B; ++i)
        a[NR-i]++
    for (i = 1; i <= A; ++i)
        a[NR+i]++
}

FNR!=NR && !(FNR in a)
dedowsdi
источник