Просмотр логов (базы данных в sed).

Просматривать логи лениво и сильно утомляет, но - надо. Утомляет это всё тем, что там одна и та-же фигня, которая повторяется десять тысяч раз, и отличается только датой. Неплохо-бы было отфильтровать одинаковые строчки, так что-бы печатались только первые, а остальные такие-же можно и не печатать - если это нас заинтересует, мы можем просмотреть только их командой grep, или той-же sed. Для решения этой задачи нам потребуется хранить все уникальные строчки, и перед выводом каждой проверять, не выводили-ли мы её прежде. Наверное можно извратится используя утилиты sort, затем uniq для фильтрации одинаковых строк, ну а потом ещё раз sort для вывода всего этого в хронологическом порядке. Думаю такой изврат будет работать ещё медленнее чем sed (хотя sed и не рассчитана на такое применение, фактически это работа с довольно большой базой данных!), впрочем, гуру линукса могут это и проверить и подтвердить (или опровергнуть) правильность моего решения. Я конечно буду благодарен тому, кто это сделает ;)

При использовании sed у нас не возникает вопроса «где хранить временные данные», у нас на всё про всё всего одна переменная, буфер удержания (hold space), и выбора особого нет, в данном случае и с форматом нашей БД не должно быть вопросов - команда загрузки в ОУ (H) пишет в конец области разделяя строчки символом «\n». я возьму кусочек своего /var/log/syslog, для примера:

Nov  8 13:08:29 localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov  8 13:08:29 localhost kernel: Current sd0b:00: sns = 70  3
Nov  8 13:08:29 localhost kernel: ASC=11 ASCQ= 0
Nov  8 13:08:29 localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00
Nov  8 13:08:29 localhost kernel:  I/O error: dev 0b:00, sector 4046908
Nov  8 13:11:40 localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov  8 13:11:40 localhost kernel: Current sd0b:00: sns = 70  3
Nov  8 13:11:40 localhost kernel: ASC=11 ASCQ= 0
Nov  8 13:11:40 localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00
Nov  8 13:11:40 localhost kernel:  I/O error: dev 0b:00, sector 3681164
Nov  8 13:12:10 localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov  8 13:12:10 localhost kernel: Current sd0b:00: sns = 70  3
Nov  8 13:12:10 localhost kernel: ASC=11 ASCQ= 0
Nov  8 13:12:10 localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00
Nov  8 13:12:10 localhost kernel:  I/O error: dev 0b:00, sector 3690236
Nov  8 13:12:13 localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov  8 13:12:13 localhost kernel: Current sd0b:00: sns = 70  3
Nov  8 13:12:13 localhost kernel: ASC=11 ASCQ= 0
Nov  8 13:12:13 localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00

Там такой фигни много, это я царапанные CD пытался прочитать... Для начала загрузим нашу строчку в БД командой H, и извлечём БД в буфер командой x, а затем найдём новую строчку среди старых, с помощью выражения

/.*([^\n]+)\n.*\1$/

...Ага... я тут подумал, и решил сделать по другому: записывать новую строчку не в конец базы, а в её начало. У этого подхода есть три преимущества: во-первых sed просматривает строки слева-направо, и ей будет намного удобнее, если она УЖЕ знает что искать - она сразу найдёт первую строчку (а что её искать? Понятно-же что она вначале, мы её туда сами записали!), и будет просто проверять все остальные на совпадение, найти две одинаковые строки, не зная ни то, где начало и той и другой, ни то, какой они длинны значительно медленнее. Во-вторых, если записывать строки в начало базы, тогда старые строчки будут в конце базы, а известно, что строки чаще совпадают с более новыми записями (например, когда-то давно я поменял CPU, в логе это отражено конечно, и каждый раз, когда я запускаю комп, в лог пишется тип процессора, если проверять с конца, то сначала sed проверит новый со старым, и пойдёт проверять далее по базе, пока не найдёт новый тип, ежели начать с новых записей, то sed найдёт новую запись, и на этом успокоится). В третьих, если строчка добавляется командой G, то при нахождении новой строки в базе вообще ничего делать: ведь печатать не надо да и базу менять - то-же, команда G как раз и не меняет базу, что нельзя сказать о команде H. С таким временем поиска скорость падает пропорционально квадрату уникальных строчек, что довольно быстро, потому скорость работы этого скрипта чрезвычайно критична.

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

/(\S+\s+){3}/

я выкрутился так: я использую не одну, а две sed, конвейером. Первая форматирует дату таким образом:

$ sed -r 's/^(\S+\s+){3}/\0~/' sys.txt
Nov  8 13:08:29 ~localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov  8 13:08:29 ~localhost kernel: Current sd0b:00: sns = 70  3
Nov  8 13:08:29 ~localhost kernel: ASC=11 ASCQ= 0
Nov  8 13:08:29 ~localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00
Nov  8 13:08:29 ~localhost kernel:  I/O error: dev 0b:00, sector 4046908
Nov  8 13:11:40 ~localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov  8 13:11:40 ~localhost kernel: Current sd0b:00: sns = 70  3
Nov  8 13:11:40 ~localhost kernel: ASC=11 ASCQ= 0
Nov  8 13:11:40 ~localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00
Nov  8 13:11:40 ~localhost kernel:  I/O error: dev 0b:00, sector 3681164
Nov  8 13:12:10 ~localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov  8 13:12:10 ~localhost kernel: Current sd0b:00: sns = 70  3
Nov  8 13:12:10 ~localhost kernel: ASC=11 ASCQ= 0
Nov  8 13:12:10 ~localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00
Nov  8 13:12:10 ~localhost kernel:  I/O error: dev 0b:00, sector 3690236
Nov  8 13:12:13 ~localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov  8 13:12:13 ~localhost kernel: Current sd0b:00: sns = 70  3
Nov  8 13:12:13 ~localhost kernel: ASC=11 ASCQ= 0
Nov  8 13:12:13 ~localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00

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

/^[^~]+~/

Это то-же использование маркёров, в качестве маркёра можно использовать не только ~, но и любой другой символ, которого не бывает в дате.

теперь можно писать сам скрипт:

$ sed -r 's/^(\S+\s+){3}/\0~/' sys.txt | sed -rn 'G;s/^[^~]+~([^\n]+)\n.*[^~]+~\1.*/\0/;t;h;s/\n.*//p'
Nov  8 13:08:29 ~localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov  8 13:08:29 ~localhost kernel: Current sd0b:00: sns = 70  3
Nov  8 13:08:29 ~localhost kernel: ASC=11 ASCQ= 0
Nov  8 13:08:29 ~localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00
Nov  8 13:08:29 ~localhost kernel:  I/O error: dev 0b:00, sector 4046908
Nov  8 13:11:40 ~localhost kernel:  I/O error: dev 0b:00, sector 3681164
Nov  8 13:12:10 ~localhost kernel:  I/O error: dev 0b:00, sector 3690236

Если строчка уже есть в базе - я завершаю скрипт командой t (и перехожу к новой строке), а вот если строчка новая, то я сначала сохраняю новую базу (команда h), а затем выделяю и печатаю эту строку.

В качестве "домашнего задания" предлагаю немного доделать этот скрипт: во первых, маркёр неплохо-бы убрать из вывода, делается это достаточно просто: надо добавить ещё одну команду s в самый конец однострока. Во вторых, сильно тормозят работу строчки

Nov  8 13:08:29 ~localhost kernel:  I/O error: dev 0b:00, sector 4046908

Их очень много, и они все уникальные. Я вынес номер сектора (который всегда разный и который мне не слишком интересен) в дату, перед ~, потому у меня эти строки не считаются уникальными. Конечно было-бы неплохо засунуть номер сектора на своё место. Лог-файл из 2362 строк обработался за 410 секунд, что вполне приемлемо для моего CPU на 400MHz. Это конечно после исключения номера сектора, с номером время работы как минимум в 10 раз больше! При этом первая команда sed практически не потребляет ресурсов.В итоге получилось всего 145 строк, что конечно очень полезно - теперь не надо читать огромные логи!

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

ну примерно таким образом:

Пример 4.1. Скрипт для подсчёта слов.

wcx.sh
#!/bin/sh

# Это вроде как shell, но на самом деле, это 2 скрипта на sed.

sed -r '
# если в строке нет ни одной буквы - то нет и слов, исключаем
/\w/!d

# небуквы в конце строки удаляем
s/\W+$//

# вставка перевода строки и маркёра в дыры между словами
# дырка - это много небукв после правой границы слова
# правых границ столько-же сколько слов, но мы меняем
# не всё - после последней границы не может быть небукв
# мы их удалили прошлой командой
s/\>\W+/\na~/g

# вставка маркёра перед первым словом
s/^\W*/a~/
'  $@ |\
sed -rn '

G
# на эту строчку приходится основная нагрузка:
# первое подвыражение в скобках захватывает полностью анализируемое слово
# второе подвыражение расположено в начале базы до найденного совпадения
# третье - это старый номер в позиционной системе счисления, тут a означает 1,
# aa == 2, aaa == 3 ... b == 10, ba == 11, baa = 12 ... bbbaa == 32, dccbaaaa == 1214
# четвёртое подвыражение - это хвост, то, что расположено за совпадением
# в случае совпадения слово с номером убирается из базы, и записывается в начало,
# так-же к номеру добавляется a, т.о. счётчик увеличивается на 1
# кроме того, часто используемые слова группируются в начале базы для увеличения
# быстродействия.
s/^a~([^\n]+)(.*)\n([a-f]+)~\1\n(.*)/\3a~\1\2\n\4/
t old_word

# новое слово
h
# что-бы не было очень скучно выводим новые слова в stderr
s/\n.*//
s/^a~//
w /dev/stderr
b ctrl_end

:old_word
# Старое слово, мы его нашли, и вытащили в начало базы
# и ещё добавили букву а в конец. Если букв a больше 9,
# нам следует сменить их на b, и т.д.
s/^([b-f]*)a{10}/\1b/; T done
s/^([c-f]*)b{10}/\1c/; T done
s/^([d-f]*)c{10}/\1d/; T done
s/^([ef]*)d{10}/\1e/; T done
s/^(f*)e{10}/\1f/; T done
s/^f{10}//
# максимум 999999 единиц - вполне хватит ИМХО
:done
x

:ctrl_end
$!b
#следующие команды выполняются в конце текста
s/.*/ ----- Найденные слова -----/p
x
# перевод позиционной системы счисления в обычную
:begin_convert
# проверяем, надо-ли ещё переводить?
/^[0-9 ]/ b end_convert

# сохраняем, что-бы не конвертить всю базу
h
s/~.*//
t lx_convert
:lx_convert
s/a{9}/9/; t dx_convert
s/a{8}/8/; t dx_convert
s/a{7}/7/; t dx_convert
s/a{6}/6/; t dx_convert
s/a{5}/5/; t dx_convert
s/a{4}/4/; t dx_convert
s/a{3}/3/; t dx_convert
s/a{2}/2/; t dx_convert
s/a/1/; t dx_convert
s/$/0/; t dx_convert
:dx_convert
y/bcdef/abcde/
s/[a-f]/\0/
t lx_convert
# теперь мы отконвертировали наш счётчик

# выравнивание: слева от числа добавляется 5 пробелов
s/^/     /
# затем берутся 6 последних символов
s/.*(......)/\1/

# теперь мы можем добавить базу
G

# ротация базы. первая запись переносится на последнее место
# кроме того, мы затираем позиционный счётчик.
s/([^\n]+)\n[a-f]+~([^\n]+\n)(.*)/\3\1 \2/
b begin_convert

:end_convert
p
'


Думаю, пояснений вполне достаточно и в комментариях, впрочем ещё чуть-чуть: ну тут я активно использую жадность, всегда следует учитывать, что первый квантификатор захватывает всё что может, а второй - тоже всё, из того, что осталось, потому, часто нет смысла использовать якоря (это «^» и «$», которые означают начало и конец строки соответственно), к примеру шаблон /.*/ уже и так неявно заякорен, и применение якорей ничего не изменят. Хотя в некоторых случаях применять якоря желательно для повышения быстродействия (если нам надо искать что-то, и мы уверены что это что-то в самом конце строки, нужно сообщить об этом sed, возможно уже сейчас или в будущих версиях появятся оптимизации, которые используют эту информацию). Привязывать к началу строки без необходимости нет смысла - sed и так просматривает строку с начала.

Для фильтрации слов я использовал спец-символы «\w» и «\W», это более предпочтительней чем «[a-zA-Z0-9_]», потому-что работает во всех кодировках и на всех языках. Кроме того, я использовал границу «\>». Этот спец-символ совпадает с пустой строкой, которая лежит между «\w\W», или между «\w$», конечно есть и левая граница слова.

Тема записи в файл в этом скрипте не раскрыта, хотя я и применил команду w, но это просто для вывода в поток ошибок - вполне естественно сохранить список найденных слов, и при этом скучно глядеть как этот скрипт работает (а работает он долго - sed это вам не MySQL).

В скрипте имеются циклы, в том числе и вложенные, обратите внимание на сброс флага перехода, после выполнения команды s, которая всегда находит своё RE, необходимо перейти командой t на следующую команду s, результат работы которой мы будем анализировать. Если этого не сделать, то даже если вторая команда s ничего не найдёт и не поменяет, команда перехода t всё равно передаст управление. Для сброса команды перехода я использовал так-же команду b, однако в примерах из info sed почему-то используется именно t.

Есть в скрипте и команда y - это достаточно редкая команда у меня служит для деления на 10 позиционного счётчика, например если счётчик равен

ddcccccbbbbbbb

(это 2570 по человечески), то после выполнения

y/bcdef/abcde/

мы получим

ccbbbbbaaaaaaa

что как раз и составляет необходимые нам 257.

Замечание

Смысл работы команды y довольно прост: ищутся все символы из первого списка, и заменяются на символы из второго. В нашем случае все b меняются на a, c на b, d на c, и так далее. Невозможно использовать в этой команде списки, классы и прочее, потому «одностроки на перле», которые удаляют всё содержимое на всех дисках здесь невозможны. Не знаю, хорошо это или плохо...

А вообще я думаю, что для ускорения работы следует применить например пятеричную систему счисления, хотя её и сложнее переводить в десятичную, но сам перевод нужно сделать единожды для каждого слова, а эти счётчики сильно тормозят поиск... Не знаю... Попробуйте если есть желание...

В целом использовать sed как базу данных можно. Если конечно число уникальных элементов не слишком велико. Если счёт идёт на десятки-сотни тысяч - поднимайте MySQL сервер - без вариантов! Впрочем, управлять этим сервером можно и с помощью sed. Кроме того, иногда в качестве БД можно использовать файловую систему - насоздавать тыщу файлов, и пусть ось сама ищет нужное нам. Например, учитывая что у нас слова, можно сделать для каждого слова отдельный файл, в котором будет лежать счётчик, а имя файла - само слово. Я не знаю, будет-ли такой подход более эффективным, возможно это будет намного быстрее, кроме того, можно сегодня добавить в наш словарь один текст, а завтра - другой. Кроме того, в файлах можно хранить не только счётчик, но и даже информацию о том, в каком тексте(текстах) скрывается данное слово.

Недавно у меня возникла задача скачать и сохранить около 7000 файлов. При этом, каждый файл идентифицировался уникальной строкой, «темой», мне пришлось просматривать каждые 2 часа определённый сервер, и выкачивать оттуда файлы с новой «темой». Эту самую «тему» я свернул в CRC-32 командой cksum, а затем перевёл в восьмеричную систему командой bc Конечно всё это делалось внутри «sed», вот например перевод числа в восьмеричный вид:

s/^[0-9]+$/echo \x27obase=8; &\x27 | bc/e
s/.*/0000000000&/
s/.*([0-7]{11})$/\1/

Узнав crc32 я могу узнать, есть-ли у меня этот файл, к примеру, для темы «test» сумма будет 06757644257, и узнать есть-ли такой файл можно просто проверить наличие файла 06757644257.html, в реальности, я упаковал все html файлы в архивы, но сделал короткие файлы с самой темой, например 06757644257.subj. К примеру, если в буфере лежит crc, то наличие нужного файла внутри sed-скрипта узнаётся так:

s/^[0-7]+$/test -f &.subj; echo $?/e
/^0$/ b переход_если_файл_существует
# а здесь обрабатывается случай когда такого файла ещё нет

Такой подход естественно намного быстрее чем хранение базы в области удержания (в моём случае, если файлов ~7000).

Внимание

Обратите внимание, как я задаю выражение при использовании модификатора e: я мог-бы написать /.*/, однако я задаю /^[0-7]+$/ - это связано с безопасностью, злоумышленник может подсунуть в мой скрипт какую-нибудь кривую «тему» с вредоносным кодом. Этот код будет выполнен командой s/.*//e. Однако у меня это невозможно - s/^[0-7]+$/.../e выполняется только если в буфере лежит восьмеричное число (если враг мне подсунет какую-нибудь каку, то s просто не выполнится). ИМХО лучше так перестраховаться, чем потом кусать локти - например сейчас этот скрипт работает в пяти километрах от меня, и я не имею никакой возможности остановить его, или как-то изменить логику его работы.

Продолжим рассмотрение найденной уязвимости (см. здесь).

Как я уже показал, злоумышленник может протолкнуть любой код сквозь фильтр. Однако, что в этом такого? Подумаешь... Ан нет - рассмотрим простой пример: требуется выделить первые буквы(все, что есть), и отправить их в команду shell (выше я выполнил shell-команду test). Для примера я возьму команду echo (она безвредная). Итак:

$ echo -e "--ф; ls" | sed -r 's/\W*(\w*).*/\1/;s/.*/echo "&"/e'
				ф

тут мы имеем 2 команды s, первая из них служит фильтром, который отфильтровывает буквы. Используется RE /\W*(\w*).*/, оно отфильтровывает только первые буквы. Ну а затем отфильтрованные буквы передаются в команду echo, т.к. там только буквы, то их можно распечатать и так, но на всякий случай программер заключил их в кавычки. Вроде всё хорошо, и даже кавычки есть. Тут для простоты двойные, но можно и одиночные, это без разницы.

Подразумевается, что на вход sed может поступить всё что угодно, и вроде как всё нормально, отфильтруются только буквы, а даже если их и не будет, то «\w*» вполне себе совпадает ни с чем, и этот однострок отлично работает. Но вот приходит враг, и подставляет несимвол:

$ echo -e "--ф\xD1; ls" | sed -r 's/\W*(\w*).*/\1/;s/.*/echo "&"/e'
ф?
Makefile
Makefile.bak
current_commit.sh
main.c
main.c.bak
main.o
regex

О бл*!!! Злоумышленник выполнил свой вредоносный код!!!

В данном случае просто просмотрел каталог, но команда могла быть любой другой.

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

Что-бы исключить такую атаку, необходимо и достаточно выполнять shell команды тогда и только тогда, когда в буфере размещается целиком допустимое значение. Т.е. нужна вторая проверка, непосредственно в команде, которая вызывает shell (почему я кстати и не использую команду e - там такой проверки нет).

$ echo -e "--ф\xD1; ls" | sed -r 's/\W*(\w*).*/\1/;s/^\w+$/echo "&"/e'
				ф?; ls

Вот - какая-то вражья фигня не совпадает с /^\w+$/ (только буквы, с начала и до конца), и потому shell-команда просто не выполняется.

Вы можете обсудить этот документ на форуме. Текст предоставляется по лицензии GNU Free Documentation License (Перевод лицензии GFDL).

Вы можете пожертвовать небольшую сумму яндекс-денег на счёт 41001666004238 для оплаты хостинга, интернета, и прочего. Это конечно добровольно, однако это намного улучшит данный документ (у меня будет больше времени для его улучшения). На самом деле, проект часто находится на грани закрытия, ибо никаких денег никогда не приносил, и приносить не будет. Вы можете мне помочь. Спасибо.