Удалите N-ю строку из каждой строки, соответствующей шаблону

У меня есть несколько файлов, таких как file1 , file2 … и т. Д. В одном каталоге, и каждый файл может содержать несколько строк, соответствующих PATTERN .
Я хотел бы удалить N ю строку из каждой строки, соответствующей PATTERN например, с N = 3 и file1 например

 1 no match 2 PATTERN 3 same PATTERN 4 no match here 5 no match here either 6 another PATTERN 7 again, no match 8 no 9 last line 

ожидаемый результат

 1 no match 2 PATTERN 3 same PATTERN 4 no match here 7 again, no match 8 no 

Редактирование файлов на месте – это бонус, а не требование (хотя есть хотя бы один инструмент gnu , который, как я знаю, может редактировать их все за один раз …)


Здесь был задан аналогичный вопрос, но это особый случай (в каждом файле есть только один шаблон соответствия линии, и решения там будут работать только с несколькими строками, соответствующими шаблону, если они разделены по меньшей мере N +1 несогласованными строками ).

Вы можете использовать awk для этого, я так считаю:

 awk -vN=3 '/PATTERN/ {skips[FNR+N]=1;} {if(!(FNR in skips)) print;}' <file> 

поэтому каждый раз, когда мы PATTERN мы записываем строку, которая находится N отсюда, и печатаем только те строки, которые мы не помечены для пропусков.

с gawk вы можете использовать -i inplace на месте, чтобы сделать это на месте

Как вы отметили, это не будет обрабатывать несколько файлов. Конечно, вы можете обернуть циклом for для итерации по всем файлам, но если этого недостаточно, чтобы сделать слишком длинную командную строку, вы также можете сделать это так:

  awk -vN=3 '{if(FNR==1) split("", skips, ":");} /PATTERN/ {skips[FNR+N]=1;} {if(!(FNR in skips)) print;}' * 

где мы перезапускаем skips до пустого массива каждый раз, когда FNR достигает 1, поэтому начало каждого файла.
С gnu awk вы можете написать это как:

 gawk -i inplace 'FNR==1{delete nr};/PATTERN/{nr[FNR+3]++};!(FNR in nr)' file* 

Мне нравится двухпроходный механизм, поэтому мы можем использовать sed -i :

 for file in file1 ... do sed -i "$file" -e "$(awk <"$file" -v N=3 '/PATTERN/{ print (NR+N) "d" }')" done 
 for f in file1 file2 file...; do sed -i -f <(grep -n PATTERN "$f" | while IFS=: read line rest; do printf "%dd; " $((line+3)); done) "$f" done 

Разделить это отдельно:

  1. Скопируйте файл file1 file2 …

  2. создайте выражение sed внутри подстановки процесса, чтобы в конечном итоге запустить файл.

  3. grep выводит номера строк, соответствующие PATTERN в файле (вместе с фактической совпадающей строкой).

Пример вывода:

 2:2 PATTERN 3:3 same PATTERN 6:6 another PATTERN 
  1. цикл while отключает номер строки, отбрасывая соответствующую строку, затем отправляя ее в printf, увеличивая на 3

  2. printf печатает номер целевой линии, а затем команду sed d delete и разделительную точку с запятой.

Пример вывода (в качестве входного сигнала для sed ):

 5d; 6d; 9d; 

Этот метод обеспечивает достаточную гибкость; вы можете установить N=3 и использовать $((line+N)) в качестве аргумента printf.

Чтобы учесть редактирование на месте, я принимаю sed, который поддерживает -i «на месте» редактирования.

Этот прецедент просто умоляет использовать ex .

К сожалению, поскольку удаление третьей строки после данной строки может удалить строку, содержащую PATTERN, и, таким образом, исключить удаление, связанное с этой линией (или, что еще хуже, удалить неправильную строку), вам нужно отменить файл, используя, например, tac first , Затем вы можете удалить третью строку перед каждым экземпляром PATTERN и снова отменить файл:

 for f in *.txt; do printf %s\\n '%!tac' 'g/PATTERN/-3d' '%!tac' x | ex "$f"; done 

Если у вас есть доступ к tac я думаю, что это самое чистое решение.


Для полностью совместимого с POSIX решения , используя мой ответ на:

  • Обратная последовательность файла с инструментами POSIX?

Вы можете сделать это так:

 for f in *.txt; do printf %s\\n '%!sed -n '\''1h;1\!{x;H;};${g;p;}'\' 'g/PATTERN/-3d' '%!sed -n '\''1h;1\!{x;H;};${g;p;}'\' x | ex "$f"; done 

Не очень читаемый, но функциональный.