Когда нам нужно найти строку, соответствующую определённому образцу, нам на помощь приходит команда grep и регулярные выражения. А что если нам нужно найти то, что находится между совпадением первого шаблона и между совпадением второго шаблона? То есть нам нужно найти содержимое находящееся между двумя определёнными строками.
Как найти текст, который начинает и заканчивается с определённых строк
Допустим, в HTML коде есть конструкция:
<div class="onp-locker-call….. здесь интересующие нас строки </div>
Строка <div class="onp-locker-call….. может иметь различные варианты например:
<div class="onp-locker-call" style="display: none;" data-lock-id="onpLock443607">
Или так:
<div class="onp-locker-call" style="display: none;" data-lock-id="onpLock781340">
Но суть в любом случае одна — нам нужно найти фрагмент, начинающийся и заканчивающийся с определённых строк. При этом мы не знаем точное количество строк и, возможно, окаймляющие строки также могут различаться (то есть поиск выполняется по регулярному выражению).
Имеется несколько способов выполнить такой поиск. На ум в первую очередь приходит команда grep, но у Linux есть более удобные инструменты — выражения диапазона, которые поддерживаются командами sed и awk.
Если нам нужно найти строки вида:
<div class="onp-locker-call….. здесь интересующие нас строки </div>
Тогда команда sed будет следующей:
sed -n '/<div class="onp-locker-call/,/<\/div>/p' ФАЙЛ
Рассмотрим чуть более простой пример, имется файл со следующим содержимым:
zdk aaa b12 cdn dke kdn
Мне нужно найти содержимое между любыми произвольными строками.
Допустим, я хочу найти содержимое между строками aaa и cdn, то есть я должен получить:
aaa b12 cdn
Или я хочу найти содержимое между строками zdk и dke, то есть вывод должен быть таким:
zdk aaa b12 cdn dke
Каким образом добиться этого?
Нужно задействовать команду sed, использующую выражение диапазонов.
Синтаксис запуска:
sed -n '/НАЧАЛО_ДИАПАЗОНА/,/КОНЕЦ_ДИАПАЗОНА/p' ФАЙЛ
Для указанных выше примеров запуск команды такой:
sed -n '/aaa/,/cdn/p' ФАЙЛ aaa b12 cdn
Для второго случая:
sed -n '/zdk/,/dke/p' ФАЙЛ zdk aaa b12 cdn dke
Использование опции -n подавляет автоматический вывод, то есть будут напечатаны только строки, для которых это явно запрошено. То есть это случиться когда будет найден диапазон /aaa/,/cdn/.
Эти выражения диапазонов также доступны в awk, там вы можете сказать:
awk '/zdk/,/dke/' ФАЙЛ
Конечно, все эти условия могут быть развёрнуты в более строгие выражения схожие с регулярными выражениями, к примеру:
sed -n '/^aaa$/,/^cdn$/p' ФАЙЛ
Это позволит проверять, что строки состоят из точных совпадений aaa и cdn и ничего более.
Показанные два примера можно скомпоновать в одну единственную команду sed с более сложным синтаксисом:
sed -n ' /^aaa$/,/^cdn$/w output1 /^zdk$/,/^dke$/w output2 ' ФАЙЛ
Так что там с командой grep?
С командой grep конструкция для данного примера выглядела бы так:
grep -o "aaa.*cdn" <(paste -sd_ ФАЙЛ) | tr '_' '\n'
В grep можно добиться множественного совпадения, но нужно использовать такой вариант grep как perl-regexp (то есть добавить опцию -P, которая поддерживается не на всех платформах, например, на OS X), поэтому в качестве рабочего решения мы заменяем новые строки на символ _ и после grep меняем их обратно.
В качестве альтернативы можно использовать pcregrep, которая поддерживает многостроковые шаблоны (опция -M).
Или используйте ex:
ex +"/aaa/,/cdn/p" -scq! ФАЙЛ
Рассмотрим ещё один пример поиска фрагментов из нескольких строк
Допустим имеется файл со следующим модержимым:
kkkkkkkkkkk jjjjjjjjjjjjjjjjjj gggggggggggg/CK JHGHHHHHHHH HJKHKKLKLLL JNBHBHJKJJLKKL JLKKKLLKJLKJ/D GGGGGGGGGGGGGG GGGGGGGGGGGGGG
Мне хочется, чтобы были выбраны строки начиная с CK с конца строки и поиск совпадений был остановлен, когда строка на конце имеет D.
То есть должно быть выведено:
gggggggggggg/CK JHGHHHHHHHH HJKHKKLKLLL JNBHBHJKJJLKKL JLKKKLLKJLKJ/D
Лучше использовать awk или sed:
awk '/CK$/,/D$/' file.txt
ИЛИ:
sed -n '/CK$/,/D$/p' file.txt
Если хочется именно grep, то для GNU grep это делается следующим образом:
grep -oPz '(?s)(?<=\n)\N+CK\n.*?D(?=\n)' file.txt
Здесь:
- -P активирует perl-regexp
- -z устанавливает разделитель строк на NUL. Это принуждает grep видеть весь файл как одну строку
- -o печать только совпадающей части
- (?s) активирует PCRE_DOTALL, поэтому . (точка) это любые символы или newline
- \N совпадает со всем, кроме newline
- .*? находит . в режиме nongreedy (не жадный)
- (?<=..) это look-behind (смотреть после) выражения
- (?=..) это look-ahead (смотреть до) выражения
Рассмотрим ещё пример.
Как с awk или sed выбрать строки между двумя шаблонами, которые могут встречаться несколько раз
Могу ли я используя awk или sed выбрать строки, которые встречаются между двумя различными шаблонами маркеров? Может быть несколько секций, отмеченных этими шаблонами.
Например, допустим есть файл, содержащий:
abc def1 ghi1 jkl1 mno abc def2 ghi2 jkl2 mno pqr stu
Начальным паттерном является abc, а конечным паттерном является mno, мне нужно, чтобы вывод был таким:
def1 ghi1 jkl1 def2 ghi2 jkl2
Есть ли способ в sed или awk сделать так, чтобы находилось не единичное совпадение, а чтобы поиск повторялся пока не будет достигнут конец файла?
Решение:
Нужно использовать awk с флагом, который будет запускать вывод когда необходимо:
awk '/abc/{flag=1;next}/mno/{flag=0}flag' ФАЙЛ def1 ghi1 jkl1 def2 ghi2 jkl2
Как это работает?
- /abc/ совпадает со строками, имеющими этот текст, также делает /mno/.
- /abc/{flag=1;next} устанавливает flag, когда найден текст abc. Затем эта строка пропускается.
- /mno/{flag=0} убирает flag, когда найдено mno.
- Конечный flag — это шаблон с дефолтным действием, которым является print $0: если флаг равен 1, то печатается строка. Таким образом, он напечатает все строки, появившиеся с момента появления abc и до следующего mno. Это также напечатает строки от последнего совпадения abc до конца файла.
Более детальное описание и примеры, вместе со случаями, когда паттерны показываются или нет, будут ниже.
Если вы хотите, чтобы печаталось всё между, а также сами паттерны, тогда вы можете использовать:
awk '/abc/{a=1}/mno/{print;a=0}a' ФАЙЛ
Или так:
awk '/abc/{a=1} a; /mno/{a=0}' ФАЙЛ
Или даже так:
awk '/abc/,/mno/' ФАЙЛ
Используя sed:
sed -n -e '/^abc$/,/^mno$/{ /^abc$/d; /^mno$/d; p; }'
Опция -n означает не печатать по умолчанию (эта опция разъяснена выше).
Шаблон ищет строки, содержащие только с abc до только mno, затем выполняет действия в { … }.
Первое действие удаляет строку abc, второе удаляет строку mno, а p печатает оставшиеся строки. Вы можете расслабить регулярные выражения по мере необходимости. Любые строки за пределами abc..mno просто не печатаются.
Поиск фрагментов текста, начинающихся и заканчивающихся с определённых строк, вывод этих фрагментов с маркерами или без
Для систематизации изучим ещё несколько примеров, некоторые из которых пересекаются с уже рассмотренными.
Допустим имеется текстовый файл примерно как показано ниже, и я хочу вывести строки между двумя заданными паттернами, обозначенными как PAT1 и PAT2:
1 2 PAT1 3 - первый блок 4 PAT2 5 6 PAT1 7 - второй блок PAT2 8 9 PAT1 10 - третий блок
Решения на основе awk
Печать строк между PAT1 и PAT2
awk '/PAT1/,/PAT2/' ФАЙЛ PAT1 3 - первый блок 4 PAT2 PAT1 7 - второй блок PAT2 PAT1 10 - третий блок
Или используя переменные:
awk '/PAT1/{flag=1} flag; /PAT2/{flag=0}' ФАЙЛ
Как это работает?
- /PAT1/ соответствует строкам, имеющим этот текст, также делает /PAT2/.
- /PAT1/{flag=1} устанавливает flag когда в строке найден текст PAT1.
- /PAT2/{flag=0} удаляет flag когда в строке найден текст PAT2.
- flag — это паттерн с дефолтным действием, которым является print $0: если флаг равен 1, то строка печатается. Таким образом, он напечатает все те строки с вхождениями со времён случившихся с PAT1 и вплоть до следующей увиденной PAT2. Также будут напечатаны строки с последнего совпадения PAT1 и вплоть до конца файла.
Напечатать строки между PAT1 и PAT2 — не включая PAT1 и PAT2
awk '/PAT1/{flag=1; next} /PAT2/{flag=0} flag' ФАЙЛ 3 - первый блок 4 7 - второй блок 10 - третий блок
Это использует next для пропуска строки, которая содержит PAT1, чтобы она не печаталась.
Этот вызов next может быть отброшен перетасовкой блоков:
awk '/PAT2/{flag=0} flag; /PAT1/{flag=1}' ФАЙЛ
Напечатать строки между PAT1 и PAT2 - включая PAT1
awk '/PAT1/{flag=1} /PAT2/{flag=0} flag' ФАЙЛ PAT1 3 - первый блок 4 PAT1 7 - второй блок PAT1 10 - третий блок
Помещая флаг в самом конце, он запускает действие, которое было установлено в PAT1 или PAT2: печатать в PAT1, а не печатать в PAT2.
Печать строк между PAT1 и PAT2 - включая PAT2
awk 'flag; /PAT1/{flag=1} /PAT2/{flag=0}' ФАЙЛ 3 - первый блок 4 PAT2 7 - второй блок PAT2 10 - третий блок
Помещая флаг в самом начале, он запускает действие, которое было установлено ранее, и, следовательно, печатает закрывающий шаблон, но не начальный.
Вывести строки между PAT1 и PAT2 — исключая строки с последнего PAT1 до конца файла, если не найдено другого PAT2
Ещё вариант:
awk 'flag{ if (/PAT2/) {printf "%s", buf; flag=0; buf=""} else buf = buf $0 ORS } /PAT1/ {flag=1}' ФАЙЛ
В одну строку:
awk 'flag{ if (/PAT2/){printf "%s", buf; flag=0; buf=""} else buf = buf $0 ORS}; /PAT1/{flag=1}' ФАЙЛ 3 - первый блок 4 7 - второй блок # обратите внимание на отсутствие третьего блока, так как никакой другой PAT2 не происходит после него
Это сохраняет все выбранные строки в буфере, который заполняется с момента обнаружения PAT1. Затем он продолжает заполняться следующими строками, пока не будет найден PAT2. В этот момент он печатает сохранённый контент и очищает буфер.
Решения на основе sed
Печать строк между PAT1 и PAT2
sed -n '/PAT1/,/PAT2/{/PAT1/!{/PAT2/!p}}' ФАЙЛ
или:
sed -n '/PAT1/,/PAT2/{//!p}'
Как и выше, но исключая границы диапазона.
Напечатать строки между PAT1 и PAT2 — включая PAT1 и PAT2
Следующий пример включит границы диапазона, что ещё проще:
sed -n '/PAT1/,/PAT2/p' ФАЙЛ
Напечатать строки между PAT1 и PAT2 — включая PAT1
Следующее включит только начало диапазона:
sed -n '/PAT1/,/PAT2/{/PAT2/!p}' ФАЙЛ
Напечатать строки между PAT1 и PAT2 — включая PAT2
Следующее включит только конец диапазона:
sed -n '/PAT1/,/PAT2/{/PAT1/!p}' ФАЙЛ
Решения на основе grep
Примеры с grep заслуживают отдельного внимания, поскольку работают в ситуациях, когда нужно вырезать фрагмент с начальным и конечным маркером из одной длинной строки. Рассмотренные выше примеры могут подвести, так как будут выводить строки целиком.
Использование grep с PCRE (где доступен) для печати маркеров и строк между маркерами:
grep -Pzo "(?s)(PAT1(.*?)(PAT2|\Z))" ФАЙЛ PAT1 3 - первый блок 4 PAT2 PAT1 7 - второй блок PAT2 PAT1 10 - третий блок
- -P использовать perl-regexp, PCRE. Не во всех вариантах grep
- -z Трактовать ввод как набор строк, каждая из которых заканчивается на нулевой байт, а не на newline
- -o печатать только совпадения
- (?s) точка соответствует всему, то есть точка также находит newlines (символ новой строки)
- (.*?) нежадный поиск
- \Z Соответствует только концу строки, или перед newline в конце
Печатать строки между маркерами, исключая конечный маркер:
grep -Pzo "(?s)(PAT1(.*?)(?=(\nPAT2|\Z)))" ФАЙЛ PAT1 3 - первый блок 4 PAT1 7 - второй блок PAT1 10 - третий блок
- (.*?)(?=(\nPAT2|\Z)) поиск нежадный поиск с lookahead для \nPAT2 и \Z
Печать строк между маркерами исключая маркеры:
grep -Pzo "(?s)((?<=PAT1\n)(.*?)(?=(\nPAT2|\Z)))" ФАЙЛ 3 - первый блок 4 7 - второй блок 10 - третий блок
- (?<=PAT1\n) положительный lookbehind для PAT1\n
Печать строк между маркерами, исключая начальный маркер:
grep -Pzo "(?s)((?<=PAT1\n)(.*?)(PAT2|\Z))" ФАЙЛ 3 - первый блок 4 PAT2 7 - второй блок PAT2 10 - третий блок
Связанные статьи:
- Как добавить строку в начало или в конец каждой строчки (72.1%)
- Как сделать замену от совпадения до конца строки (72.1%)
- Как добавить нули до определённого размера строки (66.9%)
- Как удалить newline (символ новой строки) из вывода команд и файлов в командной строке Linux (59.3%)
- Как вывести от определённого столбца до последнего в командной строке Linux (59.3%)
- Как конвертировать JPG в PDF (RANDOM - 4.6%)
Что-то тут не то с кавычками:
awk ‘/PAT1/{flag=1} flag; /PAT2/{flag=0}’ p}’ ФАЙЛ
Здравствуйте! Спасибо, что обратили внимание. Что-то случилось на этапе коррекции, в результате много команд содержали лишние символы. Сейчас всё поправил.
Здравствуйте !
Есть лог файл в которы пишутся состояние контроллеров (обмен данными между контроллером и сервером)
Есть строка начала синхронизации и сторка конца синхронизации, обе они известны и всегда одинаковы, но в логе они встречаются часто, мне нужно вывести последние актуальные данные между этими строками. Как я могу это сделать
Вот пример лога:
09.09.2022 16:51:20 CONSOLA:9
09.09.2022 16:51:23 Init Com Eth(0):10.100.0.10:14101
09.09.2022 16:51:23 Open communication port
09.09.2022 16:51:24 Trying to establish communication with the controller
09.09.2022 16:51:24 KIT: GK7C-3 VERSION: V9.18 CPU NUMBER: CPA102001
09.09.2022 16:51:25 Established communication
09.09.2022 16:51:25 Sending date and hour
09.09.2022 16:51:26 COMMUNICATION: The controller has been synchronized.
09.09.2022 16:51:27 Receiving supplies
09.09.2022 16:52:25 Supplies correctly received
09.09.2022 16:52:27 Processed
09.09.2022 16:52:27 Sending users
09.09.2022 16:52:52 Users correctly sent
09.09.2022 16:52:52 Sending vehicles
09.09.2022 16:53:06 Vehicles correctly sent
09.09.2022 16:53:06 Sending controller configuration
09.09.2022 16:53:08 Controller configuration correctly sent
09.09.2022 16:53:08 Sending product configuration
09.09.2022 16:53:10 Product configuration correctly sent
09.09.2022 16:53:10 Sending tank configuration
09.09.2022 16:53:11 Tank configuration correctly sent
09.09.2022 16:53:11 Sending hose configuration
09.09.2022 16:53:13 Hose configuration correctly sent
09.09.2022 16:53:14 Disconnecting the controller
09.09.2022 16:53:15 Disconnected controller
09.09.2022 16:53:15 Close communication port
Приветствую! Предположим, что исходные данные следующие:
Следующая команда выведет последний фрагмент между строками «CONSOLA:9» и «Close communication port»:
Я не знал, как вывести именно последний фрагмент, поэтому командой tac сделан обратный порядок строк в файле. Затем находится и выводится фрагмент между «CONSOLA:9» и «Close communication port». Обратите внимание, что начало и конец фрагмента поменяны местами (поскольку весь файл имеет обратный порядок строк). Затем команда «q» (фрагмент «/CONSOLA:9/q» указывает выйти из команды, после первого совпадения. Наконец, « | tac» возвращает нормальный порядок строк.