Получение текста от последнего маркера до EOF в POSIX.2

8

У меня есть текст с маркерными линиями, такими как:

aaa
---
bbb
---
ccc

Мне нужно получить текст от последнего маркера (не включительно) до EOF. В этом случае это будет

ccc

Есть ли элегантный способ в POSIX.2? Прямо сейчас я использую два прогона: первый с nlи grepдля последнего вхождения с соответствующим номером строки. Затем я извлекаю номер строки и использую sedдля извлечения рассматриваемого фрагмента.

Сегменты текста могут быть довольно большими, поэтому я боюсь использовать какой-либо метод добавления текста, например, мы добавляем текст в буфер, если мы сталкиваемся с маркером, мы очищаем буфер, чтобы в EOF у нас был последний фрагмент в буфера.

aikipooh
источник

Ответы:

6

Если ваши сегменты не очень большие (как, например: вы действительно не можете сэкономить столько ОЗУ, предположительно потому, что это крошечная встроенная система, управляющая большой файловой системой), один подход - действительно лучший подход. Не только потому, что это будет быстрее, но и самое главное, потому что это позволяет источнику быть потоком, из которого любые данные, считанные и не сохраненные, будут потеряны. Это действительно работа для awk, хотя sed тоже может это сделать.

sed -n -e 's/^---$//' -e 't a' \
       -e 'H' -e '$g' -e '$s/^\n//' -e '$p' -e 'b' \
       -e ':a' -e 'h'              # you are not expected to understand this
awk '{if (/^---$/) {chunk=""}      # separator ==> start new chunk
      else {chunk=chunk $0 RS}}    # append line to chunk
     END {printf "%s", chunk}'     # print last chunk (without adding a newline)

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

</input/file tail -n +$((1 + $(</input/file         # print the last N lines, where N=…
                               grep -n -e '---' |   # list separator line numbers
                               tail -n 1 |          # take the last one
                               cut -d ':' -f 1) ))  # retain only line number
</input/file tail -n +$(</input/file awk '/^---$/ {n=NR+1} END {print n}')
</input/file tail -c +$(</input/file LC_CTYPE=C awk '
    {pos+=length($0 RS)}        # pos contains the current byte offset in the file
    /^---$/ {last=pos}          # last contains the byte offset after the last separator
    END {print last+1}          # print characters from last (+1 because tail counts from 1)
')

Приложение: Если у вас больше POSIX, вот простая однопроходная версия, основанная на общем расширении awk, которое позволяет разделителю записей RSбыть регулярным выражением (POSIX допускает только один символ). Это не совсем правильно: если файл заканчивается разделителем записей, он печатает фрагмент перед разделителем последней записи вместо пустой записи. Вторая версия использует RTэтот недостаток, но RTона специфична для GNU awk.

awk -vRS='(^|\n)---+($|\n)' 'END{printf $0}'
gawk -vRS='(^|\n)---+($|\n)' 'END{if (RT == "") printf $0}'
Жиль "ТАК - перестань быть злым"
источник
@Gilles: sedработает нормально, но я не могу awkзапустить пример; он зависает ... и я получаю ошибку в 3-м примере: cut -f ':' -t 1 ... cut: неверный параметр - 't'
Peter.O
@ fred.bear: я понятия не имею, как это произошло - я протестировал все свои фрагменты, но каким-то образом испортил редактирование после копирования-вставки в cutпримере. Я не вижу ничего плохого в этом awkпримере, какую версию awk вы используете и каков ваш тестовый ввод?
Жиль "ТАК - перестань быть злым"
... на самом деле awkверсия работает .. это просто занимает очень много времени на большом файле .. sedверсия обработала тот же файл за 0.470 с .. Мои тестовые данные очень взвешены ... только два куска с одиноким '---' три строки с конца миллиона строк ...
Peter.O
@Gilles .. (я думаю , что я должен прекратить тестирование в 3 часа. Я как - то проходят все три из «два прохода» awks как единое целое :( ... Я теперь проходят каждый по отдельности , а второй из них очень быстро на 0,204 секунды ... Howerver, первый «двухпроходный» awk выводит только: « (стандартный ввод) » (-l кажется виновником) ... что касается третьего «двухпроходного» awk, я не ничего не выводить ... но второй "два прохода" является самым быстрым из всех представленных методов (POSIX или иным способом
:)
@ fred.bear: исправлено и исправлено. Мой QA не очень хорош для этих коротких фрагментов - я обычно копирую-вставляю из командной строки, форматирую, затем замечаю ошибку и пытаюсь исправить встроенное, а не переформатировать. Мне любопытно посмотреть, является ли подсчет символов более эффективным, чем подсчет строк (2-й или 3-й двухпроходные методы).
Жиль "ТАК - перестань быть злым"
3

Стратегия с двумя проходами кажется правильной. Вместо sed я бы использовал awk(1). Два прохода могут выглядеть так:

$ LINE=`awk '/^---$/{n=NR}END{print n}' file`

чтобы получить номер строки. И затем отобразите весь текст, начиная с этого номера строки:

$ awk "NR>$LINE" file

Это не должно требовать чрезмерной буферизации.

Маки Мессер
источник
и тогда они могут быть объединены:awk -v line=$(awk '/^---$/{n=NR}END{print n}' file) 'NR>line' file
Гленн Джекман
Видя, что я тестировал другие материалы, я также протестировал приведенный выше фрагмент "glen jackman's". Это занимает 0,352 секунды (с тем же файлом данных, упомянутым в моем ответе) ... Я начинаю получать сообщение о том, что awk может быть быстрее, чем я предполагал изначально (я думал, что sed почти так же хорош, как и получил, но это похоже на случай "лошадей на курсах") ...
Peter.O
Очень интересно увидеть все эти сценарии в бенчмарке. Отличная работа, Фред.
Маки Мессер
Самые быстрые решения используют tac и tail, которые фактически читают входной файл в обратном направлении. Теперь, если бы только awk мог читать входной файл задом наперед ...
Mackie Messer
3
lnum=$(($(sed -n '/^---$/=' file | sed '$!d') +1)); sed -n "${lnum},$ p" file 

Первый sedвыводит номера строк строк "---" ...
Второй sedизвлекает последний номер из выходных данных первого sed ...
Добавьте 1 к этому числу, чтобы получить начало вашего блока "ccc" ...
Третий 'sed' выводит из начала блока "ccc" в EOF

Обновление (с исправленной информацией о методах Жиля)

Что ж, меня интересовало, как будут работать Гленн Джекман tac , поэтому я проверил время на три ответа (на момент написания) ... Каждый тестовый файл (-ы) содержал 1 миллион строк (их собственных номеров строк).
Все ответы сделали то, что ожидалось ...

Вот времена ..


Жиль sed (один проход)

# real    0m0.470s
# user    0m0.448s
# sys     0m0.020s

Жиль awk (один проход)

# very slow, but my data had a very large data block which awk needed to cache.

Жиль «два прохода» (первый метод)

# real    0m0.048s
# user    0m0.052s
# sys     0m0.008s

Жиль "два прохода" (второй метод) ... очень быстро

# real    0m0.204s
# user    0m0.196s
# sys     0m0.008s

Жиль «два прохода» (третий метод)

# real    0m0.774s
# user    0m0.688s
# sys     0m0.012s

Жиль "gawk" (метод RT) ... очень быстрый , но не POSIX.

# real    0m0.221s
# user    0m0.200s
# sys     0m0.020s

Гленн Джекман ... очень быстро , но не POSIX.

# real    0m0.022s
# user    0m0.000s
# sys     0m0.036s

fred.bear

# real    0m0.464s
# user    0m0.432s
# sys     0m0.052s

Маки Мессер

# real    0m0.856s
# user    0m0.832s
# sys     0m0.028s
Peter.O
источник
Из любопытства, какую из моих двухпроходных версий вы тестировали, и какую версию awk вы использовали?
Жиль "ТАК - перестань быть злым"
@Gilles: я использовал GNU Awk 3.1.6 (в Ubuntu 10.04 с 4 ГБ ОЗУ). Все тесты содержат 1 миллион строк в первом «чанке», затем «маркер», за которым следуют 2 строки «данных» ... Потребовалось 15,540 секунд, чтобы обработать файл меньшего размера из 100 000 строк, но для 1 000 000 строк я запустить его сейчас, и это было более 25 минут до сих пор. Он использует одно ядро ​​на 100% ... убивает его сейчас ... Вот еще несколько дополнительных тестов: lines = 100000 (0m16.026s) - lines = 200000 (2m29.990s) - lines = 300000 (5m23. 393 с) - строки = 400000 (11 млн. 938 с)
Peter.O
Упс .. В моем вышеупомянутом комментарии я пропустил Вашу ссылку на "два прохода" awk. Вышеприведенные подробности относятся к awk "однопроходного" ... Версия awk верна ... Я сделал дополнительный комментарий о различных версиях "двухпроходного" под вашим ответом (изменено время, приведенное выше)
Peter.O
2

Используйте « tac », который выводит строки файла от конца к началу:

tac afile | awk '/---/ {exit} {print}' | tac
Гленн Джекман
источник
tacэто не POSIX, это специфично для Linux (в GNU coreutils и в некоторых установках busybox).
Жиль "ТАК - перестань быть злым"
0

Вы могли бы просто использовать ed

ed -s infile <<\IN
.t.
1,?===?d
$d
,p
q
IN

Как это работает: tдублирует .строку current ( ) - которая всегда является последней строкой, когда edначинается (только в случае, если в последней строке присутствует разделитель), 1,?===?dудаляет все строки до и включая предыдущее совпадение ( edвсе еще находится на последней строке ) затем $dудаляет (дублирует) последнюю строку, ,pпечатает текстовый буфер (замените на, wчтобы отредактировать файл на месте) и, наконец, qзавершает работу ed.


Если вы знаете, что на входе есть хотя бы один разделитель (и вам все равно, напечатан ли он), тогда

sed 'H;/===/h;$!d;x' infile

будет короче.
Как это работает: он добавляет все строки в Hстарый буфер, перезаписывает hстарый буфер при обнаружении совпадения, он dвыбирает все строки, кроме $последней, когда он xизменяет буферы (и автопечать).

don_crissti
источник