Подсчёт и выборка данных при помощи awk и column в терминале, операции с дробными числами в bash

Говорят, что изучение 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(строка) - переводить строку в верхний регистр.

Оставить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *