awk - язык програмирования, который неплохо подходит для обработки форматированных текстовых данных. Первая версия интерпретатора awk появилась еще на заре unix'состроения и с тех пор ту или иную реализацию можно найти практически в любой unix, или unix-подобной системе. Насколько я понимаю, в современных linux'ах awk встречается в виде gawk - версия awk от проекта GNU; а так же в более легком, но немного урезанном по функциональности варианте - mawk. mawk, например, по-умолчанию устанавливается в Debian. Основная функциональность одинакова во всех возможных вариантах, различия как правило в деталях.
Интерпретатор awk можно вызывать либо с указанием файла сценария:
awk [параметры] -f 'имя файла сценария' [файл(ы)]
или задавая нужные инструкции прямо в командной строке:
awk [параметры] 'сценарий' [файл(ы)]
где "[файл(ы)]" - имя файла или файлов которые awk-сценарий и будет обрабатывать.
awk-сценарий выполняется над каждой строкой входного файла, исключение представляют собой блоки BEGIN{} и END{}, которые выполняются соответственно в начале, и в конце работы сценария (условно говоря - перед началом чтения файла(ов) и после того как прочитана последняя строка).
Каждая обрабатываемая строка, доступна awk-сценарию через переменную $0. Если исходный файл представляет собой текстовую таблицу,разбитую на поля каким-то определенным разделителем, то в переменных $1,$2,...$n окажутся значения этих полей. По-умолчанию, разделителями считаются пробельные символы; задать свой вариант разделителя можно либо в блоке BEGIN, или с помощью параметра -F. Например, логины пользователей из /etc/passwd можно вывести следующим образом:
diesel@indie:~$ awk -F: '{print $1}' /etc/passwd
diesel@indie:~$ awk 'BEGIN{FS=":"} {print $1}' /etc/passwd
Программа на awk представляет собой набор блоков, каждый блок состоит из условия, и списка команд которые будут выполнятся если условие истинно. Команды в блоках заключаются в фигурные скобки. Блок перед которым не стоит никаких условий, например {print $1} из примера выше, будет выполняться на каждой обрабатываемой строке.
В качестве условия может выступать регулярное выражение заключенные в символы // - в таком случае с регулярным выражением будет сравниваться вся входная строка; операции сравнения с регулярным выражением, например $1~/regexp/ или $1!~/regexp/; а так же другие операции отношения, например $1 == "test". Допустим, вывести логины пользователей, которые начинаются с буквы d можно вот таким вот нехитрым образом:diesel@debian:~$ awk -F: '/^d/{print $1}' /etc/passwd
daemon
diesel
В отличии от shell, или скажем perl, имена переменных в awk не предваряются символом $ . Конструкция вида $a будет трактоваться как "поле, номер которого, содержится в переменной a", Например результат выполнения команды:awk -F: '/^d/{a=1; print $a}' /etc/passwd
, будет аналогичен тому что мы получили в предыдущем примере. Существует стандартная переменная NF в которой содержится колличество полей в текущей строке, print $NF
напечатает содержимое последнего поля, print $(NF-1)
- содержимое предпоследнего, и так далее.
diesel@debian:~$ cat test
Name: John Smith
Town: New Yourk
Name: Alex Diesel
Town: Nikolaev
В котором разделителем полей является перевод строки(\n), а разделителем строк таблицы - пустая строка(\n\n); Превратить это в более "табличную" форму можно вот так:
diesel@debian:~$ cat test|awk 'BEGIN{RS="\n\n"; FS="\n"} {print $1" "$2}'
Name: John Smith Town: New Yourk
Name: Alex Diesel Town: Nikolaev
Конкатенация строк, как видите, до боли проста, просто записываем подряд все строки которые нам хочется соединить(пробельные символы вне кавычек не учитываются, поэтому
print $1 " " $2
сделает ровно тоже самое). Хотя в данном случае было бы неплохо не ставить после каждой переменной один и тот же разделитель, а задать общий разделитель для строк которые мы печатаем в print, для этого можно воспользоваться еще одной специальной переменной: OFS(Output Field Separator):diesel@debian:~$ cat test|awk 'BEGIN{RS="\n\n"; FS="\n"; OFS=";"} {print $1,$2}'
Name: John Smith;Town: New Yourk
Name: Alex Diesel;Town: Nikolaev
А если мы хотим еще и сохранить исходный разделитель строк(\n\n), то к нашим услугам будет переменная ORS(Output Record Separator):
diesel@debian:~$ cat test|awk 'BEGIN{RS="\n\n"; FS="\n"; OFS=";"; ORS="\n\n"} {print $1,$2}'
Name: John Smith;Town: New Yourk
Name: Alex Diesel;Town: Nikolaev
Как видимо уже стало понятно из примеров, отдельне команды в блоке разделяются точкой с запятой, точка запятая после последней команды в блоке необязательна.
Иногда хочется при выводе результатов произвести некоторые замены, например, нам не очень сильно нужны "Name:" и "Town:", из предидущего примера. Конечно, можно воспользоваться sed'ом:
diesel@debian:~$ cat test|awk 'BEGIN{RS="\n\n"; FS="\n"; OFS=";"} {print $1,$2}' |sed -e 's!Name: !!g; s!Town: !!g'
John Smith;New Yourk
Alex Diesel;Nikolaev
но можно обойтись и awk, благо есть целый набор функций для операций со строками, например gensub:
diesel@debian:~$ cat test|awk 'BEGIN{RS="\n\n"; FS="\n"; OFS=";"} {print gensub("^Name: ","","",$1),gensub("^Town: ","","",$2)}'
John Smith;New Yourk
Alex Diesel;Nikolaev
Функция ожидает указания: того что заменяем, того на что заменяем, опций для замены(здесь фактически можно указать только "g", - глобальная замена в строке - то есть заменять все найденные совпадения, а не только первое), и собственно строки в которой замену проводить, а возвращает строку-результат замены. Если gensub в вашей версии awk нет, скорее всего окажется gsub, gsub не возвращает результата замены, а сохраняет результат в той же самой переменной в которой мы дадим ей строку(возвращает эта функция как раз таки колличество проведенных замен, поэтому и возможности указать"g" в качестве опции не имеет, итого колличество параметров на один меньше), получится несколько длинее, но результат будет достигнут:
diesel@debian:~$ cat test|awk 'BEGIN{RS="\n\n"; FS="\n"; OFS=";"}
{ gsub("^Name: ","",$1);
gsub("Town: ", "", $2);
print $1,$2
}'
John Smith;New Yourk
Alex Diesel;Nikolaev
Фактически, основная сфера использования awk - мелкие однострочники, иногда в состве shell-скриптов, сценариев на awk, занимающих более 20 строк я видел достаточно мало, - сколько-нибудь большие скрипты предпочитают писать на "олдскульном" perl, или "модных" ныне python, и ruby. В составе shell-скриптов иногда хочется передать awk значение какой-нибудь shell-переменной. Самый простой способ(но не единственный) это сделать вот так:
diesel@debian:~$ cat passwd_pattern.sh
#!/bin/bash
awk -F: '$1~/'$1'/{print $1}' /etc/passwd;
В качестве второго $1 подставится значение переменной $1 из shell'а - первый параметр переданный shell скрипту, например:
diesel@debian:~$ ./passwd_pattern.sh ^d
daemon
diesel
как и раньше мы получили логины пользователей которые начинаются с d. Стоит немного подредактировать скрипт, и пользовтелей соответствующих шаблону в системе не окажется:
diesel@debian:~$ cat ./passwd_pattern.sh
#!/bin/bash
awk -F: '$1~/'$1'/{print "userdel " $1}' /etc/passwd;
diesel@debian:~$ ./passwd_pattern.sh ^d
userdel daemon
userdel diesel
diesel@debian:~$ ./passwd_pattern.sh ^d |sh
В awk вобщем-то есть и условный оператор, и разнообразные циклы, и еще много разных функций, обо всем этом рекоменду читать при необходимости man-страницу, там достаточно подробно все рассказано. Пост все-таки про "as I use it", и тот subset awk, который я использую, описан выше. На закуску сегодня будет еще один пример.
Время от времени меня посещают странные идеи заставить мой домашний proftpd писать логи в MySQL, дабы логи эти было потом легче разбирать и выводы строить. Пожалуйста, не надо предлагать мне системы анализа логов - я про них знаю, да и речь сейчас не об этом. Но даже если этот самый proftpd начнет сегодня писать логи в базу, останутся ведь текстовые логи за вчера, и позавчера.... ну вы поняли, которые бы хорошо для статистики тоже в базе иметь. И поможет нам в этом awk. :)
Допустим таблица база данных имеет вот такую структуру:
CREATE TABLE downloads(
id INT NOT NULL AUTO_INCREMENT,
username VARCHAR(30),
filename VARCHAR(256) ,
size BIGINT,
host VARCHAR(30),
ip VARCHAR(16),
action VARCHAR(8),
duration VARCHAR(8),
time timestamp NULL default NULL,
success TINYINT,
PRIMARY KEY (id)
);
Лог, который пишет proftpd по умолчанию выглядит вот таким вот образом:
Sun Jan 13 16:24:12 2008 222 192.168.62.53 40622476 /srv/ftp/debian/debian_main/pool/main/g/gcj-4.1/libgcj-doc_4.1.1-20_all.deb b _ o a ftp 1 * c
Sun Jan 13 16:24:24 2008 11 192.168.62.53 9030846 /srv/ftp/debian/debian_main/pool/main/g/gcj-4.1/libgcj7-0_4.1.1-20_i386.deb b _ o a ftp 1 * c
Sun Jan 13 16:24:24 2008 0 192.168.62.53 80180
А вот небольшой скрипт, который эти непонятные строки, превращает в что-то более полезное и осмысленное:
#!/bin/bash
FILENAME=$1;
if [ x"$FILENAME" == "x" ]; then
echo " Script for converting proftpd logs into mysql queries. Usage: $0 ";
exit;
fi
awk '{
if ( $18 == "c"){
success="1";
}else{
success="0";
}
month=$2;
sub("Jan","01",month);
sub("Feb","02",month);
sub("Mar","03",month);
sub("Apr","04",month);
sub("May","05",month);
sub("Jun","06",month);
sub("Jul","07",month);
sub("Aug","08",month);
sub("Sep","09",month);
sub("Oct","10",month);
sub("Nov","11",month);
sub("Dec","12",month);
time=$1" "$2" "$3" "$4" "$5;
time=$5"-"month"-"$3" "$4;
print "INSERT INTO downloads (username,filename,size,host,ip,action,duration,time,success)"
print "VALUES (\""$14"\",\""$9"\",\""$8"\",\""$7"\",\""$7"\",\"RETR\",\""$6"\",\""time"\",\""success"\");";
}' "$FILENAME"
Работает это все примерно вот так:
diesel@debian:~$ ./proftpd-for-blog.sh test_log
INSERT INTO downloads (username,filename,size,host,ip,action,duration,time,success)
VALUES ("ftp","/srv/ftp/debian/debian_main/pool/main/g/gcj-4.1/libgcj-doc_4.1.1-20_all.deb","40622476","192.168.62.53","192.168.62.53","RETR","222","2008-01-13 16:24:12","0");
INSERT INTO downloads (username,filename,size,host,ip,action,duration,time,success)
VALUES ("ftp","/srv/ftp/debian/debian_main/pool/main/g/gcj-4.1/libgcj7-0_4.1.1-20_i386.deb","9030846","192.168.62.53","192.168.62.53","RETR","11","2008-01-13 16:24:24","0");
diesel@debian:~$ ./proftpd-for-blog.sh test_log | mysql -u root test_for_blog
diesel@debian:~$ mysql -e 'select * from downloads limit 1 \G' -u root test_for_blog
*************************** 1. row ***************************
id: 1
username: ftp
filename: /srv/ftp/debian/debian_main/pool/main/g/gcj-4.1/libgcj-doc_4.1.1-20_all.deb
size: 40622476
host: 192.168.62.53
ip: 192.168.62.53
action: RETR
duration: 222
time: 2008-01-13 16:24:12
success: 0
Да, конечно, INSERT можно(и даже нужно) не повторять на каждой строке, как модифицировать для этого написанное мной, предлагаю подумать самостоятельно. А я спешу откланяться, до новых встречь!
4 комментария:
Есть опция --assign=var=val, и не стоит (имхо) колдовать над сборкой скрипта в баше :)
Оно не всегда работает:
diesel@indie:~$ a="test"; awk --assign=a=$a '{print a}' /etc/passwd
awk: unknown option --assign=a=test ignored
есть -v val=val которое работает чаще, хотя лично мне проще "собирать awk в bash'е, как и sed. Если, конечно речь идет о одной строчке. Да, это иногда работает не очень корректно.
М. Ну у меня gawk (под винду), может поэтому такое различие.
да, это как раз и есть опция gawk'а. приведенный пример - с OS X, в котором реализация awk'а, если не ошибаюсь, BSD'шная - они немного отличаются - то что будет работать у меня - будет работать и у Вас, обратное - не всегда.
Отправить комментарий