Говорят, что изучение bash «по взрослому» начинается с команды awk. И действительно раньше эти конструкции отталкивали, но как оказалось, всё не так страшно как представлялось вначале пути. И теперь меня уже пугают другие bash конструкции, типа сложных условий sed обработки и выборки. Неужели я перешёл на «взрослый» уровень? Не знаю. А вот команда awk понравилась за свою гибкость, ведь в ней доступны условия и даже подскрипты, позволяющие разобрать данные каждой колонки, проверить и обработать их с нужной логикой. Кстати, если быть точным, это уже никакой не bash, это awk!
Допустим у нас есть некий лог файл, в котором через разделитель мы сохраняем инфу с числами. Разобъём его на клонки командой column указав этот разделитель. Ну вот например, подсчёт количества строк с условием, что значение в колонке 9 больше или равно 3:
cat current.log | column -t -s ';' | awk '$9>=3 {print $9}' | wc -l
Какой grep вам такое выполнит? Нет, силу регулярок, конечно никто не отменял и grep -e справится, но нужно писать регулярку. А если условий будет несколько, придётся писать и под них регулярки. А здесь мы сразу указываем условия напрямую. Ну хорошо, допустим первый пример неудачный, тогда вот такой:
cat current.log | column -t -s ';' | awk 'BEGIN{sum=0} {sum+=$9} END{print sum;}' cat current.log | column -t -s ';' | awk 'BEGIN{sum=0} {if($9>=3) sum+=$9} END{print sum;}'
Вот здесь уж точно только awk справится с такими задачами. В первом случае мы просто подсчитали сумму данных в колонке 9. А второй командой при помощи условия в awk мы подсчитали сумму только тех цифр, что более или равны 3.
Кстати, если в колонке были дробные числа типа float, то и результат будет дробным. А вы знали, что bash при всех своих достоинствах, тупо не работает с дробными числами? При делении они их округляет в меньшую сторону, а при остальных операциях выдаёт ошибку. Попробуем:
echo $((5/2)) # результат 2 echo $((3+5)) # результат 8 echo $((3.5+5)) # ошибка!
И как же быть, когда нам нужно произвести в bash операции с дробными числами? Ответ прост: использовать отдельный скриптовый язык awk передав ему управление потоком:
echo 3.5 5 | awk '{print $1+$2}' # результат 8.5
Вот почему я сразу заметил, что awk вам не bash! А недавно, у меня возникла ситуация, в которой column отказывался форматировать вывод поскольку в нем присутствовала кириллица. Он просто проглатывал поток и ничего не отдавал. Причём в терминале column разбивл без проблем, проблема была с вызовом shell из под php. А вот awk справился с этой задачей, обработав строки и колонки по разделителю:
cat logfile.log | awk 'BEGIN {FS = ";"}{ for (x = 1; x <= NF; x++) { printf "%-18s%s", substr(\$x, 1, 45), x == NF ? "\\n" : ""} }'
А вот что он ещё умеет:
Синтаксис команды | Описание |
awk ' {print $1,$3} ' |
Печатает только первый и третий столбцы, используя stdin |
awk ' {print $0} ' |
Печатает все столбцы, используя stdin |
awk ' /'pattern'/ {print $2} ' |
Печатает только элементы второго столбца, соответствующие шаблону «pattern», используя stdin |
awk -f script.awk inputfile |
Как и sed, awk использует ключ -f для получения инструкций из файла, что полезно, когда их большое количество и вводить их вручную в терминале непрактично. |
awk ' program ' inputfile |
Исполняет program, используя данные из inputfile |
awk "BEGIN { print \"Hello, world!!\" }" |
Классическое «Hello, world» на awk |
awk '{ print }' |
Печатает все, что вводится из командной строки, пока не встретится EOF |
#! /bin/awk -f BEGIN { print "Hello, world!" } |
Скрипт awk для классического «Hello, world!» (сделайте его исполняемым с помощью chmod и запустите) |
# This is a program that prints \ "Hello, world!" # and exits |
Комментарии в скриптах awk |
awk -F "" 'program' files |
Определяет разделитель полей как null, в отличие от пробела по умолчанию |
awk -F "regex" 'program' files |
Разделитель полей также может быть регулярным выражением |
awk '{ if (length($0) > max) max = \ length($0) } END { print max }' inputfile |
Печатает длину самой длинной строки |
awk 'length($0) > 80' inputfile |
Печатает все строки длиннее 80 символов |
awk 'NF > 0' data |
Печатает каждую строку, содержащую хотя бы одно поле (NF означает Number of Fields) |
awk 'BEGIN { for (i = 1; i <= 7; i++) print int(101 * rand()) }' |
Печатает семь случайных чисел в диапазоне от 0 до 100 |
ls -l . | awk '{ x += $5 } ; END \ { print "total bytes: " x }' total bytes: 7449362 |
Печатает общее количество байтов, используемое файлами в текущей директории |
ls -l . | awk '{ x += $5 } ; END \ { print "total kilobytes: " (x + \ 1023)/1024 }' total kilobytes: 7275.85 |
Печатает общее количество килобайтов, используемое файлами в текущей директории |
awk -F: '{ print $1 }' /etc/passwd | sort |
Печатает отсортированный список имен пользователей |
awk 'END { print NR }' inputfile |
Печатает количество строк в файле, NR означает Number of Rows |
awk 'NR % 2 == 0' data |
Печатает четные строки файла. |
ls -l | awk '$6 == "Nov" { sum += $5 } END { print sum }' |
Печатает общее количество байтов файла, который последний раз редактировался в ноябре. |
awk '$1 ~/J/' inputfile |
Регулярное выражение для всех записей в первом поле, которые начинаются с большой буквы j. |
awk '$1 ~!/J/' inputfile |
Регулярное выражение для всех записей в первом поле, которые не начинаются с большой буквы j. |
awk 'BEGIN { print "He said \"hi!\" \to her." }' |
Экранирование двойных кавычек в awk. |
echo aaaabcd | awk '{ sub(/a+/, \"<A>"); print }' |
Печатает «<A>bcd» |
awk '{ $2 = $2 - 10; print $0 }' inventory |
Модифицирует inventory и печатает его с той разницей, что значение второго поля будет уменьшено на 10. |
awk '{ $6 = ($5 + $4 + $3 + $2); print \ $6' inventory |
Даже если поле шесть не существует в inventory, вы можете создать его и присвоить значение, затем вывести его. |
echo a b c d | awk '{ OFS = ":"; $2 = "" > print $0; print NF }' |
OFS — это Output Field Separator (разделитель выходных полей) и команда выведет «a::c:d» и «4», так как хотя второе поле аннулировано, оно все еще существует, поэтому может быть подсчитано. |
echo a b c d | awk '{ OFS = ":"; \ $2 = ""; $6 = "new" > print $0; print NF }' |
Еще один пример создания поля; как вы можете видеть, поле между $4 (существующее) и $6 (создаваемое) также будет создано (как пустое $5), поэтому вывод будет выглядеть как «a::c:d::new» «6». |
echo a b c d e f | awk '\ { print "NF =", NF; > NF = 3; print $0 }' |
Отбрасывание трех полей (последних) путем изменения количества полей. |
FS=[ ] |
Это регулярное выражения для установки пробела в качестве разделителя полей. |
echo ' a b c d ' | awk 'BEGIN { FS = \ "[ \t\n]+" } > { print $2 }' |
Печатает только «a». |
awk -n '/RE/{p;q;}' file.txt |
Печатает только первое совпадение с регулярным выражением. |
awk -F\\\\ '...' inputfiles ... |
Устанавливает в качестве разделителя полей \\ |
BEGIN { RS = "" ; FS = "\n" } { print "Name is:", $1 print "Address is:", $2 print "City and State are:", $3 print "" } |
Если у нас есть запись вида «John Doe 1234 Unknown Ave. Doeville, MA», этот скрипт устанавливает в качестве разделителя полей новую строку, так что он легко может работать со строками. |
awk 'BEGIN { OFS = ";"; ORS = "\n\n" } > { print $1, $2 }' inputfile |
Если файл содержит два поля, записи будут напечатаны в виде:
«field1:field2 field3;field4 |
awk 'BEGIN { > OFMT = "%.0f" # print numbers as \ integers (rounds) > print 17.23, 17.54 }' |
Будет напечатано 17 и 18 , так как в качестве выходного формата (Output ForMaT) указано округление чисел с плавающей точкой до ближайших целых значений. |
awk 'BEGIN { > msg = "Dont Panic!" > printf "%s\n", msg >} ' |
Вы можете использовать printf практически так же, как и в C. |
awk '{ printf "%-10s %s\n", $1, \ $2 }' inputfile |
Печатает первое поле в виде строки длиной 10 символов, выровненной по левому краю, а затем второе поле в обычном виде. |
awk '{ print $2 > "phone-list" }' \inputfile |
Простой пример извлечения данных, где второе поле записывается под именем «phone-list». |
awk '{ print $1 > "names.unsorted" command = "sort -r > names.sorted" print $1 | command }' inputfile |
Записывает имена, содержащиеся в $1, в файл, затем сортируем и выводим результат в другой файл. |
awk 'BEGIN { printf "%d, %d, %d\n", 011, 11, \ 0x11 }' Will print 9, 11, 17 if (/foo/ || /bar/) print "Found!" |
Простой поиск для foo или bar. |
awk '{ sum = $2 + $3 + $4 ; avg = sum / 3 > print $1, avg }' grades |
Простые арифметические операции (в большинстве похожи на C) |
awk '{ print "The square root of", \ $1, "is", sqrt($1) }' 2 The square root of 2 is 1.41421 7 The square root of 7 is 2.64575 |
Простой расширяемый калькулятор |
awk '$1 == "start", $1 == "stop"' inputfile |
Печатает каждую запись между start и stop. |
awk ' > BEGIN { print "Analysis of \"foo\"" } > /foo/ { ++n } > END { print "\"foo\" appears", n,\ "times." }' inputfile |
Правила BEGIN и END исполняются только один раз, до и после каждой обработки записи. |
echo -n "Enter search pattern: " read pattern awk "/$pattern/ "'{ nmatches++ } END { print nmatches, "found" }' inputfile Search using shell if (x % 2 == 0) print "x is even" else print "x is odd" |
Простое условие. awk, как и C, также поддерживает операторы ?:. |
awk '{ i = 1 while (i <= 3) { print $i i++ } }' inputfile |
Печатает первые три поля каждой записи, по одной в строке. |
awk '{ for (i = 1; i <= 3; i++) print $i }' |
Печатает первые три поля каждой записи, по одной в строке. |
BEGIN { if (("date" | getline date_now) <= 0) { print "Can't get system date" > \ "/dev/stderr" exit 1 } print "current date is", date_now close("date") } |
Выход с кодом ошибки, отличным от 0, означает, что что-то идет не так. Пример: |
awk 'BEGIN { > for (i = 0; i < ARGC; i++) > print ARGV[i] > }' file1 file2 |
Печатает awk file1 file2 |
for (i in frequencies) delete frequencies[i] |
Удаляет элементы в массиве |
foo[4] = "" if (4 in foo) print "This is printed, even though foo[4] \ is empty" |
Проверяют элементы массива |
function ctime(ts, format) { format = "%a %b %d %H:%M:%S %Z %Y" if (ts == 0) ts = systime() # use current time as default return strftime(format, ts) } |
awk-вариант функции ctime() в C. Так вы можете определять свои собственные функции в awk. |
BEGIN { _cliff_seed = 0.1 } function cliff_rand() { _cliff_seed = (100 * log(_cliff_seed)) % 1 if (_cliff_seed < 0) _cliff_seed = - _cliff_seed return _cliff_seed } |
Генератор случайных чисел Cliff. |
cat apache-anon-noadmin.log | \ awk 'function ri(n) \ { return int(n*rand()); } \ BEGIN { srand(); } { if (! \ ($1 in randip)) { \ randip[$1] = sprintf("%d.%d.%d.%d", \ ri(255), ri(255)\ , ri(255), ri(255)); } \ $1 = randip[$1]; print $0 }' |
Анонимный лог Apache (IP случайные) |
Ну и добавлю ещё базовый синтаксис:
awk опции 'условие {действие}' awk опции 'условие {действие} условие {действие}' С помощью действия можно выполнять преобразования с обрабатываемой строкой. Об этом мы поговорим позже, а сейчас давайте рассмотрим опции утилиты: -F, --field-separator - разделитель полей, используется для разбиения текста на колонки; -f, --file - прочитать данные не из стандартного вывода, а из файла; -v, --assign - присвоить значение переменной, например foo=bar; -b, --characters-as-bytes - считать все символы однобайтовыми; -d, --dump-variables - вывести значения всех переменных awk по умолчанию; -D, --debug - режим отладки, позволяет вводить команды интерактивно с клавиатуры; -e, --source - выполнить указанный код на языке awk; -o, --pretty-print - вывести результат работы программы в файл; -V, --version - вывести версию утилиты. Это далеко не все опции awk, однако их вам будет достаточно на первое время. Теперь перечислим несколько функций-действий, которые вы можете использовать: print(строка) - вывод чего либо в стандартный поток вывода; printf(строка) - форматированный вывод в стандартный поток вывода; system(команда) - выполняет команду в системе; length(строка) - возвращает длину строки; substr(строка, старт, количество) - обрезает строку и возвращает результат; tolower(строка) - переводит строку в нижний регистр; toupper(строка) - переводить строку в верхний регистр.