Как получить все линии между первым и последним появлением шаблонов?

8

Как я могу обрезать файл (хороший входной поток), чтобы получить только строки в диапазоне от первого вхождения шаблона fooдо последнего вхождения шаблона bar?

Например, рассмотрим следующий вход:

A line
like
foo
this 
foo
bar
something
something else
foo
bar
and
the
rest

Я ожидаю этот вывод:

foo
this 
foo
bar
something
something else
foo
bar
rahmu
источник
3
Однопроходной поток или файл? Это гораздо проще сделать, когда разрешен произвольный доступ. С файлом вы просто найдете первое fooи последнее barи распечатаете все между ними, если что-нибудь. С потоком вам придется читать до первой fooи буферизовать все последующие строки в памяти до EOF, очищая буфер каждый раз, когда barпросматривается a . Это может означать буферизацию всего потока в памяти.
jw013

Ответы:

6
sed -n '/foo/{:a;N;/^\n/s/^\n//;/bar/{p;s/.*//;};ba};'

Соответствие шаблону sed /first/,/second/читает строки одну за другой. Когда какая-либо строка соответствует /first/ей, она запоминает ее и ожидает первого совпадения для /second/шаблона. В то же время применяются все действия, указанные для этого шаблона. После этого процесс начинается снова и снова до конца файла.

Это не то, что нам нужно. Нам нужно посмотреть до последнего соответствия /second/шаблона. Поэтому мы строим конструкцию, которая выглядит только для первой записи /foo/. Когда найдено, цикл aначинается. Мы добавляем новую строку в буфер совпадений Nи проверяем, соответствует ли она шаблону /bar/. Если это произойдет, мы просто распечатаем его и очистим буфер совпадений и переместимся в начало цикла с помощью ba.

Также нам нужно удалить символ новой строки после очистки буфера с помощью /^\n/s/^\n//. Я уверен, что есть намного лучшее решение, к сожалению, оно не пришло мне в голову.

Надеюсь, все ясно.

порыв
источник
1
Оно работает! Было бы очень круто, если бы вы могли провести нас через создание такой команды. Я чувствую себя глупо просто скопировать / вставить его с какого-нибудь веб-сайта в Интернете;)
rahmu
1
Извините, я не опубликовал объяснение с ответом. Теперь это в посте.
пик
В некоторых sedверсиях, например, BSD sed (что и есть на Mac), за тегами должен следовать символ новой строки или конца строки, поэтому необходима следующая настройка: sed -n -e '/foo/{:a' -e 'N;/^\n/s/^\n//;/bar/{p;s/.*//;};ba' -e '};' это также работает в GNU sed, поэтому я думаю, что это изменение (несколько -eаргументов) окончание аргумента после каждого имени ветки) - хорошая переносимая привычка при использовании ветвей в sed.
Wildcard
4

Я бы сделал это с небольшим Perl однострочником.

cat <<EOF | perl -ne 'BEGIN { $/ = undef; } print $1 if(/(foo.*bar)/s)'
A line
like
foo
this 
foo
bar
something
something else
foo
bar
and
the
rest
EOF

доходность

foo
this 
foo
bar
something
something else
foo
bar
user1146332
источник
3
Если бы это был код-гольф, вы могли бы использовать Eвместо eи -00777вместо $/бита (см. Perlrun (1)). Что бы сократить его до:, perl -0777 -nE 'say /(foo.*bar)/s'все еще вроде читабельным.
Тор
1
Я не знал об этих флагах! Я уверен, что особенно -0[octal]найдет свой путь в моем рабочем процессе! Спасибо за это
user1146332
3

Вот двухпроходное решение GNU sed, которое не требует большого количества памяти:

< infile                                     \
| sed -n '/foo/ { =; :a; z; N; /bar/=; ba }' \
| sed -n '1p; $p'                            \
| tr '\n' ' '                                \
| sed 's/ /,/; s/ /p/'                       \
| sed -n -f - infile

объяснение

  • Первый sedвызов проходит infile и находит первое вхождение fooи все последующие вхождения bar.
  • Эти адреса затем формируются в новый sedскрипт с двумя вызовами sedи одним tr. Выход третий sedесть [start_address],[end_address]p, без скобок.
  • Окончательный вызов снова sedпроходит infile, печатая найденные адреса и все, что между ними.
Тор
источник
2

Если входной файл удобно помещается в памяти, сделайте это просто .

Если входной файл огромен, вы можете использовать csplitего сначала для разбиения на части, fooа barзатем для каждой последующей сборки. Куски называется piece-000000000, piece-000000001и т.д. Выберите префикс (здесь piece-) , что не конфликтует с другими существующими файлами.

csplit -f piece- -n 9 - '%foo%' '/bar/' '{*}' <input-file

(В системах, отличных от Linux, вам придется использовать большое количество внутри фигурных скобок, например {999999999}, и пропустить -kопцию. Это число - количество barштук.)

Вы можете собрать все части с cat piece-*, но это даст вам все после первого foo. Так что сначала удалите этот последний кусок. Так как имена файлов, создаваемые с помощью csplit, не содержат никаких специальных символов, вы можете работать с ними без каких-либо особых мер предосторожности, например с

rm $(echo piece-* | sed 's/.* //')

или эквивалентно

rm $(ls piece-* | tail -n 1)

Теперь вы можете объединить все части и удалить временные файлы:

cat piece-* >output
rm piece-*

Если вы хотите удалить части по мере их объединения для экономии места на диске, сделайте это в цикле:

mv piece-000000000 output
for x in piece-?????????; do
  cat "$x" >>output; rm "$x"
done
Жиль "ТАК - перестань быть злым"
источник
1

Вот еще один способ с sed:

sed '/foo/,$!d;H;/bar/!d;s/.*//;x;s/\n//' infile

Он добавляет каждую строку в /foo/,$диапазоне (строки, !не входящие в этот диапазон, dвыбираются) в Hстарое пространство. Линии, которые не совпадают bar, затем удаляются. На совпадающих строках пространство образца освобождается, e xизменяется с пространством удержания, а ведущая пустая строка в пространстве образца удаляется.

При огромных входных данных и небольшом числе случаев barэто должно быть (намного) быстрее, чем вытягивать каждую строку в пространство шаблона, а затем каждый раз проверять пространство шаблона bar.
Разъяснение:

sed '/foo/,$!d                     # delete line if not in this range
H                                  # append to hold space
/bar/!d                            # if it doesn't match bar, delete 
s/.*//                             # otherwise empty pattern space and
x                                  # exchange hold buffer w. pattern space then
s/\n//                             # remove the leading newline
' infile

Конечно, если это файл (и помещается в памяти), вы можете просто запустить:

 ed -s infile<<'IN'
.t.
/foo/,?bar?p
q
IN

потому что ed можно искать вперед и назад.
Вы даже можете прочитать вывод команды в текстовый буфер, если ваша оболочка поддерживает подстановку процесса:

printf '%s\n' .t. /foo/,?bar?p q | ed -s <(your command)

или, если это не так, с помощью gnu ed:

printf '%s\n' .t. /foo/,?bar?p q | ed -s '!your command'
don_crissti
источник
0

Использование любого awk в любой оболочке в любой системе UNIX и без чтения всего файла или входного потока в память одновременно:

$ awk '
    f {
        rec = rec $0 ORS
        if (/bar/) {
            printf "%s", rec
            rec = ""
        }
        next
    }
    /foo/ { f=1; rec=$0 ORS }
' file
foo
this
foo
bar
something
something else
foo
bar
Эд Мортон
источник
0

Grep также может это сделать (ну, GNU grep):

<infile grep -ozP '(?s)foo.*bar' | tr '\0' '\n'

<infile grep -ozP '        #  call grep to print only the matching section (`-o`)
                           #  use NUL for delimiter (`-z`) (read the whole file).
                           #  And using pcre regex.
(?s)foo.*bar               #  Allow the dot (`.`) to also match newlines.
' | tr '\0' '\n'           #  Restore the NULs to newlines.

Для ввода из тела вопроса:

$ <infile grep -ozP '(?s)foo.*bar' | tr '\0' '\n'
foo
this 
foo
bar
something
something else
foo
bar
Исаак
источник