Баззворды наподобие Big Data, Data Science, Spark и Hadoop уже довольно плотно въелись в наш мозг. Обычно при их упоминании мы сразу представляем себе большой дата-центр, поверх которого работает мощная распределенная система, ищущая сложные закономерности и тренирующая модели. Но не зря кто-то из титанов сказал: «Вы заслуживаете получить второй компьютер, только когда научитесь правильно пользоваться первым». Многие утилиты командной строки *nix уже давно написаны на C, хорошо оптимизированы и позволяют решать многие современные задачи с очень высокой эффективностью всего лишь на одной машине. Не веришь? Тогда читай дальше наш cправочник по shell командам!

С резкого скачка популярности анализа данных уже прошло некоторое время, и простые смертные постепенно начинают понимать, чем именно занимаются эти самые пресловутые data scientist’ы и data-инженеры. Однако ворочание гигантскими массивами данных обычно требует времени, поэтому тяжелые джобы на Hadoop или Spark инженеры запускают далеко не каждый день. Если говорить точнее, то все, что можно автоматизировать, автоматизируется, но это отнюдь не значит, что можно плевать в потолок весь день, — есть множество небольших, но важных задач, которые приходится решать. И обычно это рутина, которая как раз и представляет собой то самое сочетание технологии и магии data-инженерии, прикладного программирования и научных методов. Я взял на себя смелость составить наверняка неполный, но включающий основное список:

  • фильтрация набора записей по какому-то критерию;
  • семплирование;
  • извлечение конкретных колонок либо сортировка по ним;
  • замена значений, их формата либо же заполнение пропусков;
  • подсчет базовых статистических показателей и операции GroupBy.

А что там с данными? Обычно они лежат по старинке в какой-нибудь реляционной базе наподобие PostgreSQL, либо же они могут быть доступны через API, например социальной сети или корпоративного веб-сервиса. Иногда можно столкнуться с чем-то чуть более экзотическим вроде формата HDF5, однако топы чартов обычно занимает (как ни банально) куча tar.gz-файлов где-нибудь на HDFS или даже локально. Эти файлы разложены по частично детерминированной иерархии каталогов и представляют собой обычные CSV/TSV либо же логи какого-нибудь сервиса в определенном формате.

«Все ясно, — сразу скажет разработчик/аналитик. — Грузим все поблочно в Pandas либо же запускаем Hadoop MapReduce job’у».

Стоп, серьезно? Это же просто файлы данных, разделенные на колонки и поля, нас с такими учили работать еще на вводных лекциях по Линуксу. Плюс они доступны с более-менее вменяемой скоростью чтения чаще всего не локально, а с какого-то сервера, на котором и Pandas-то соберется не всегда (например, из-за комбинации древнего CentOS’а и отсутствия админских прав). А уж использовать мощности корпоративного Hadoop’а для того, чтобы посчитать среднее арифметическое, да и ждать результата много минут — это как-то не выглядит эффективно.

Ах, если бы была возможность делать все перечисленные операции быстро, с использованием нескольких ядер CPU, читая файлы поколоночно и извлекая нужные данные регулярными выражениями, используя готовые, проверенные временем утилиты, написанные на низкоуровневом языке!

1Базовые команды

Да, практически в любой *nix/BSD-системе присутствует командная оболочка Bash (или Zsh, или даже Tcsh), в которой типичный инженер проводит довольно большую долю своего рабочего времени. В винде с этим сильно хуже (не будем про PowerShell), но большинство команд вполне можно заставить работать через Cygwin. Вот список команд, которые мы используем каждый день: cat, wc, tar, sort, echo, chmod, ls, ps, grep, less, find, history. Дальше в тексте я подразумеваю, что этот стандартный минимум тебе известен (а если нет — прочитай man по тем, что незнакомы).

Кроме базового набора, существует также целый ряд команд, дающих дополнительные плюшки в отдельных случаях; часть из них — это разновидности приведенных выше. Давай глянем на некоторые.
seq — генерирует последовательность чисел с заданным шагом:

$ seq 4  # От 1 до 4
1
2
3
4
$ seq 7 -2 3
# От 7 до 3 с шагом –2
7
5
3
$ seq -f "Line %g" 3  # Небольшая шаблонизация
Line 1
Line 2 
Line 3

tr — производит простейшую замену символов во входном потоке:

$ echo "lol" | tr 'l' 'w'
wow
$ echo "OMG" | tr '[:upper:]' '[:lower:]'
omg

zcat / gzcat / gunzip -c — то же, что cat, но для файлов, сжатых в gzip-архив. Первая команда под OS X работает иначе, поэтому можно поставить gzcat через brew install coreutils либо просто использовать третий вариант — он работает везде, хоть и длиннее.

head — выводит несколько (по умолчанию десять) строк с начала файла. Удобна тем, что не требует загрузки файла в память целиком. Помогает подглядеть формат данных, например названия колонок и пару строк значений в CSV.

$ head -n 100 file.csv  # Задаем число строк руками


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

$ tail -n 100  # 100 последних строк
$ tail -n+100 file.csv  # Выводим все строки файла, начиная с сотой

zgrep — аналог grep для поиска по содержимому файлов в архивах.

uniq — передать на вывод только неповторяющиеся строки:

$ echo "foo bar baz foo baz omg omg" | tr ' ' '\n' | sort | uniq -c # Сколько раз встречаем уникальные строки
1 bar
2 baz
2 foo
2 omg

Стоит помнить о том, что по умолчанию uniq отслеживает только одинаковые строки, идущие подряд, поэтому, если хочется найти уникальные строки по всему входу, надо его сначала отсортировать.

shuf — делает случайную выборку из переданных на вход строк:

$ echo "foo bar baz foo baz omg omg" | tr ' ' '\n' |
 
shuf -n 2  # Здесь 2 — размер выборки
omg
baz

У нас также есть готовая система pipe’инга, которую за нас реализует система, позволяя связывать стандартный вывод одной команды со стандартным вводом другой. В Bash это вертикальная черта между двумя командами, ну, ты знаешь:

$ cat jedi.txt | grep -v Anakin

Правда, есть небольшой подвох — такая конструкция всегда при выполнении вернет exit-код последней команды в цепочке, даже если посередине что-то упало. Но мы же хотим быть в курсе! Поэтому в продакшене большинство shell-скриптов содержат такой вот вызов:

$ set -o pipefail

Таким макаром мы перехватываем коды выхода у всех команд и падаем, если что не так. Однако это сразу может привести к новым открытиям — к примеру, ты знал, что grep возвращает ненулевой exit-код и повалит весь трубопровод, если просто ничего не найдет? Но эта ситуация тоже решаемая (злоупотреблять этим не стоит, но в случае grep ненулевой код по другой причине мы вряд ли когда-нибудь получим). Можно сделать вот так:

$ cat file | (grep "foo" || true) | less # Хорошая мина при плохой игре

Двойная вертикальная черта здесь означает, что вторая команда отработает только в случае ненулевого exit-кода первой, а круглые скобки запускают всю конструкцию в subshell’е, то есть как одну команду. Разумеется, все минусы такого подхода налицо — мы маскируем любые неудачи, однако иногда это все-таки помогает. Кстати, а как запустить вторую только при успехе первой? Правильно, через &&.

Ну и еще один хак: бывает, нам нужно объединить результат запуска не- скольких независимых команд в один вывод и использовать его как часть нашего трубопровода. Делается это так:

$ { echo '1'; echo '2'; } | другая_команда

Обрати внимание, что есть тонкая грань между использованием круглых скобок выше и фигурных здесь. Круглые скобки — это гарантированное создание сабшелла, то есть системный вызов fork() c ожиданием результата выполнения child-процесса. Фигурные же скобки — это просто ограничение области видимости и создание блока кода в контексте текущего шелла, как в языках программирования (которым, собственно, можно назвать и Bash).

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

sleep 3 | echo 1

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

2Работа с HTTP

Как есть споры между любителями Nikon и Canon, так существуют и перепалки между приверженцами разных консольных HTTP-клиентов, например cURL и Wget. Я расскажу про cURL, потому что он лучше :).

Собственно, рассказывать особо и нечего — cURL позволяет довольно легко общаться по HTTP-протоколу с сервером, обрабатывать куки и редиректы, делать REST-запросы и выводить все это дело на stdout. Чего нам, вообще говоря, и хочется. Разумеется, если речь идет, скажем, о скачивании файла большого размера, то лучше его сохранить на диск, а потом по нему что-то считать, иначе при любой проблеме придется начинать все с самого начала и гонять трафик.

Я не буду сейчас в подробностях расписывать все хитрости с курлом (RTFM в помощь, поверь, это проще, чем делать запрос из программы на C/Java/ Python), упомяну только, что выбор типа запроса делается так:

$ curl [-XGET|-XPOST|-XPUT|-XHEAD]

А также стоит отметить полезный флаг -s, который запрещает выводить всякую отладочную информацию, например о прогрессе скачивания, на стандартный вывод. Все потому, что нам не нужны эти данные на стандартном вводе следующей программы, если код запущен в pipeline.

Итак, возьмем вот такую цепочку:

$ curl -s http://URL/some_file.txt | tr '[:upper:]' '[:lower:]' |
    grep -oE '\w+' | sort | uniq -c | sort -nr -k1,1 | head -n 100

Все понятно? Как это нет? Качаем файл, конвертируем на лету в нижний регистр, разбиваем на буквочисленные (alphanumeric, ага) слова, сортируем, считаем гистограмму, которую снова сортируем, но уже по первому столбику в обратном порядке и численно, а не лексикографически. Осталось взять 100 первых строк, чтобы увидеть, какие слова в нашем тексте встречаются чаще всего.

Если же у тебя слово «гистограмма» ассоциируется только с красивыми картинками — что ж, ты сам напросился. Можно вспомнить, что у нас есть такая утилита, как gnuplot, и дописать в конец команды выше (через pipe, разумеется) вот такую жесть:

$ ... | awk '{ print $2 "\t" $1 }' | gnuplot -p -e "set term png; set
xtic rotate; plot '-' using (column(0)):2:xtic(1) smooth freq with
boxes" > 123.png

Про awk я расскажу подробнее чуть дальше (к тому же тут все просто: мы меняем местами колонки и ставим разделителем между ними табуляцию), а вот про gnuplot читай сам. По функциональности он немногим уступает matplotlib’у (это навороченная библиотека для построения графиков из Python), но позволяет задавать практически любые параметры напрямую через консоль. Я не спорю, на каком-нибудь Pandas всю эту катавасию тоже можно написать в несколько строк, но это будет намного длиннее, и еще большой вопрос, что быстрее отработает.

9ed63e17399ff41c946b406bf92ade5c

Кстати, у тебя наверняка возникли вполне обоснованные сомнения насчет частого использования команды sort, и не зря: в случае больших объемов данных ее стоит применять крайне осторожно, так как память она съедает за один присест и без хлеба. Но у нее есть и несомненный плюс — она разбивает входные данные на блоки и пытается не засовывать в память все целиком. Именно поэтому даже на очень больших файлах и маленькой оперативке она когда-нибудь успешно закончит работать и выведет результат. Когда-нибудь.

3SED и AWK

Чаще всего изнеженные макбуками и мышкой студенты забывают эти две команды сразу после лекции, потому что считают их неоправданно сложными и ненужными. Но одно дело — проникаться благоговейным ужасом при взгляде на тетрис, написанный на sed, и совсем другое — использовать несомненные удобства этих команд в каждодневной работе. Серьезно, давай вспомним базовые случаи и перестанем наконец их бояться.

Итак, sed — это потоковый редактор, то есть еще одна команда, которая принимает что-то на вход, обрабатывает определенным образом и выдает на выход. Так вот, в чем sed’у нет равных — это в замене строк по шаблону, для чего, собственно, его чаще всего и используют:

$ cat file.txt | sed 's/First/Second/g'

Стоит обязательно обратить внимание на букву g в конце команды, что расшифровывается как greedy, то есть жадный. Хитрость здесь в том, что по умолчанию (без этой буквы) sedзаменит только одно, самое первое вхождение подстроки в строку, даже если оно встречается несколько раз. Сложно сказать, почему так было сделано изначально, но предупрежден — значит вооружен.

Другая проблема, с которой часто сталкиваются при попытке использовать sed, — это так называемый синдром забора. При попытке использовать пути в качестве аргументов для подмены пользователи упираются в то, что надо экранировать каждый прямой слеш в путях, поскольку он является разделителем полей в самом sed, то есть получается что-то подобное:

$ cat paths.txt | sed 's/\/usr\/bin/\/usr\/share\/bin/g'

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

$ cat paths.txt | sed 's:/usr/bin:/usr/share/bin:g'

Еще одна хитрость состоит в использовании регулярных выражений. Как и в случае с grep, можно подключить расширенные регулярки с помощью -r или -E (первое — в *nix-системах, второе — в BSD, включая OS X). Причем второе поле (результат замены) может ссылаться на первое и частично его переиспользовать. Например, так мы загоним все слова в нижнем регистре в скобки:

$ echo "foo bar OMG" | sed -r "s/[a-z]+/(&)/g"
(foo) (bar) OMG

Амперсанд просто подставляет в результат то, что у нас заматчилось с регуляркой в первом аргументе. Если же хочется именно «частичности» — можно вспомнить, что в регулярках есть так называемые группы, которые выделяются фигурными скобками и позволяют ссылаться на себя. Например, можно сделать так:

$ echo "Fooagra Foozilly" | sed -r "s/(Foo)[a-z]*/\1fel/g"
Foofel Foofel

Здесь \1 ссылается на то, что находится в круглых скобках в первом аргументе, то есть на строку «Foo». Вот такой вот фуфел.

Не умаляя остальных возможностей sed, давай пока на этом остановимся (кому интересно, что там у Гюльчатай под капюшоном, может почитать этот документ). Для большинства задач по замене значений этого уже достаточно.

Теперь про awk. У него другая сильная сторона — это извлечение данных из полей, они же колонки или секции. По сути, все, что от нас надо, — это знать разделитель (а по умолчаниюawk пытается угадать его сам, и для TSV, например, это отлично действует), а дальше мы просто работаем со значениями в каждом из этих полей, как с переменными.

Чтобы пояснить, что я имею в виду, давай глянем на структуру программы на awk (да, awk — это вполне себе целый язык программирования, быстрый и удобный):

BEGIN {} {} END {}

Несложно заметить, что у нас здесь три секции. Та, которая запускается в самом начале (BEGIN), обычно служит для инициализации переменных, а та, что в конце (END), — для вывода результата. В самых тривиальных программах, наподобие той, что была в одном из примеров выше, первая и последняя секции опускаются, а используется только центральная, которая принимает по строчке из входа за раз и автоматически раскладывает поля из нее по переменным с именами $1, $2 и так далее (нумерация с единицы).

$ cat file.txt | awk -F':' '{ print $2 }' # Выводим значение из второго поля для каждой строки
$ cat file.txt | awk '{ print $2 "," $1 }' # Берем поля, меняем местами и выводим через запятую
$ cat file.txt | awk '{ print $1,$3,$2 }' OFS='\t' # Меняем разделитель для всех полей в выводе (будет там, где запятые в print) 
$ cat file.txt | awk '{ x+=$2 } END { print x }' # Суммируем значения из второго поля и в конце выводим результат

Как видно из последнего примера, в awk имеет место «утиная типизация», то есть тип переменной определяется исходя из того, какое значение мы ей пытаемся присвоить. Кроме того, по умолчанию числовые переменные равны нулю.

Есть еще пара встроенных переменных, которыми удобно пользоваться, — это NF и NR, соответственно, число полей в текущей строке и число строк, прочитанных на данный момент. В секции END второе, что логично, будет равно общему количеству строк на вводе, так что дополнительно это считать не нужно. Так что, пожалуй, вот последний пример — я там выше, кажется, что-то говорил про среднее арифметическое:

$ echo "4 8 15 16 23 42" | sed 's:\s:\n:g' | awk '{ s += $1 } END { print s / NR }'

Думаю, тебе должно быть вполне понятно, что же именно здесь происходит.

4А как же cut?

Кто сказал «cut»? Да, есть такая довольно простая команда для извлечения полей из входных данных. Но, во-первых, у нее очень уж неочевидный интерфейс (попробуй распарсить хоть вывод ps aux — натерпишься), а во-вторых, на больших объемах данных вопреки разумным предположениям она работает в полтора раза медленнее, чем awk-скрипт по выборке тех же полей. Вроде бы это связано с какими-то хитрыми проверками кодировок внутри, но из моего личного опыта — проще взять awk и не париться.

5Полезные команды SSH

Мы запомнили множество команд, освоили grep и awk и разобрались, как обходить подводные камни, но еще не можем сказать, что заставляем комп работать на все сто процентов. Однако это легко исправить, если всего лишь научиться использовать несколько ядер процессора и, например (один из стандартных подходов), создать очередь для параллельной обработки данных воркерами.

Не надо напрягаться, я не буду сейчас рассказывать про тонкости многопоточного программирования и примитивы синхронизации. Мы же договорились, что все будет в консоли, — так и поступим. А поможет в этом квесте, как ни странно, еще одна стандартная утилита командной строки, про которую ты наверняка слышал, — xargs. Да, ее основная задача — это подставлять результат выполнения одной команды в качестве аргумента другой, но этим ее возможности не исчерпываются. Освежим память:

$ find . -name "*.sh" -print0 | xargs -0 -I'{}' mv '{}'
    ~/backup_scripts

Здесь все просто: находим в текущем каталоге все файлы с расширением sh и скармливаем их поочередно команде mv, которая швыряет их в бэкап. Ключи -print0 и -0 здесь указывают на то, что данные, поступающие из выхода find на вход в xargs, будут по-сишному разделены null-байтом. Параметр -I, в свою очередь, задает шаблон, который будет использоваться при подстановке значений в управляемую команду.

Казалось бы, при чем здесь очередь сообщений и воркеры? Сейчас это просто обычный цикл, в котором на каждой итерации команда mv что-то куда-то копирует. А хитрость вот в чем: никто не говорил, что xargs умеет и обязан запускать ровно один экземпляр команды. Представим, например, такую структуру каталогов (да, это вывод еще одной полезной консольной команды):

Что получится, если мы запустим такую (абсолютно бесполезную, но все же) команду?

$ ls | xargs -P3 -n1 ls

Или (а это даже интереснее) что будет, если мы запустим ее несколько раз?

$ ls | xargs -P3 -n1 ls
pikachu.avi
1_bar.txt  1_foo.txt
unforgivable_pics.zip
$ ls | xargs -P3 -n1 ls
1_bar.txt  1_foo.txt
pikachu.avi
unforgivable_pics.zip

Что-то эта ситуация напоминает, не правда ли? Несколько независимых процессов, пишущих в одну консоль, например? Да-да, параметр -P здесь задает количество процессов для запуска (можно поставить число ядер процессора, например ;), а -n указывает, сколько строк из входа одновременно передавать каждому процессу. В результате запускается не один, а сразу три экземпляра команды ls, каждый из которых начинает разбирать строчки из очереди, организованной xargs. Кто первый встал — того и тапки, причем команда не завершается, пока не отработает последний воркер, то есть барьер здесь тоже есть.

Дальше — больше. Понадобилось мне как-то скопировать приличное количество файлов с HDFS. Делать это в большом цикле — очень долго, писать навороченные скрипты специально для этой задачи — как-то уныло. В итоге через несколько минут появилось вот такое детище Франкенштейна:

$ cat file.txt | xargs -l bash -c 'echo hdfs dfs -get $0 $1' |
    xargs -I'{}' -d '\n' -n1 -P8 -t bash -c "eval {}"

Сейчас объясню. На входе был файл, в котором на каждой строчке два пути — адрес файла внутри HDFS и место, куда его надо скопировать локально. Чтобы скопировать данные быстро, необходимо было запустить операции копирования в несколько потоков. Первый вызов xargs превращает поток из пар адресов в поток команд по копированию данных (file1 file2 становится echo hdfs dfs -get file1 file2), также разделенных переносом строки (-d во втором xargs как раз для обработки такого случая). После этого поток передается второму xargs, который выполняет сформированные на предыдущем шаге команды в восемь потоков. Громоздко? Да, можно так сказать. Но на отлично решает задачу и сильно экономит время.

Есть еще одна команда, специально для параллелизации, с довольно неожиданным названием parallel. Она сама определяет, сколько процессов запустить (хоть это можно и задавать явно, как в примере ниже), а кроме того, позволяет буквально «разветвить трубопровод» и разбрасывать данные, поступающие с одного pipe’а по нескольким другим pipe’ам. Так, с ее помощью можно организовать параллельное конвертирование WAV-файлов в MP3:

$ ls *.wav | parallel lame {} -o {}.mp3

Если ты злишься на меня за cURL из предыдущей части статьи, то вот пример с wget:

$ cat urls.txt | parallel -j+2 'wget "{}" -O | python parse.py'

Здесь у нас на входе файл с большим числом урлов, а parallel запускает количество воркеров, равное числу ядер CPU плюс 2. Каждый воркер скачивает страничку и передает ее на стандартный вход скрипту на Python, который, возможно, ее парсит и делает еще какую-нибудь хакерскую магию.

6Форматы вокруг нас

Думаю, не погрешу против истины, если скажу, что два самых популярных формата данных, с которыми приходится сталкиваться при анализе (да и при программировании вообще), — это CSV (включая подвиды с другими разделителями) и JSON. В последнее время также становится популярен YAML, но аналитические данные в нем обычно не хранят, это скорее из стана конфигурационных файлов и прочих декларативных описаний.

Если ты думаешь, что с CSV в общем случае работать легко и просто, то это только потому, что тебе не попадались такие строки:

1,2, "vova,dima",7
3,, "lenin",0

Это абсолютно корректный CSV, однако что получится, если мы попробуем разбить верхнюю строчку по запятой? Или какое значение у нас окажется во втором поле нижней строки? Погоди рвать волосы, решение есть. И оно, как ни странно, реализовано в виде модуля для Python, который, в свою очередь, предоставляет набор консольных команд для разных задач. Модуль называется csvkit и включает в себя несколько интересных утилит:

  • in2csv — «не знаю, что это, но я хочу преобразовать это в CSV», работает, например, с файлами Excel;
  • csvcut — позволяет корректно манипулировать колонками, в том числе используя их имена;
  • csvlook— выводит CSV как красивую табличку в терминале, по аналогии с консольными клиентами к БД;
  • csvjson — конвертирует CSV в JSON в виде списка объектов с полями и значениями;
  • csvsql — всего-навсего позволяет делать SQL-запросы к CSV-документам. Not a big deal;
  • csvsort — позволяет сортировать по колонкам, в том числе используя их имена.

Неплохо, да? Целая инфраструктура. Если хочется осознать всю прелесть — можно, например, взять файл imdb-250-1996-2011-lists-only.xlsx, а потом сделать так:

$ in2csv imdb-250-1996-2011-lists-only.xlsx | csvsql --query
    "select Title,Year from stdin where Year<2009" | csvsort -r
    -c Year | head -n 10 | csvlook

Мне кажется, даже объяснять, что конкретно здесь происходит, не требуется — после прочтения текста выше это должно быть довольно очевидно. Попробуй понять и прочувствовать сам.

«Ну да, с CSV-то и простыми текстовыми форматами это все работает, но в случае JSON мне ничто такое не поможет», — подумал ты.

И напрасно. Утилита jq позволяет делать с JSON-файлами чуть менее, чем все. Если учесть, что парой абзацев выше упоминалась команда csvjson, то простор для действия практически неограничен.

Самый простой способ начать работу с ней — это подать на вход какой-нибудь JSON-файл и получить выдачу в консоли с красивыми отступами и подсветкой синтаксиса (!). Поскольку в JSON все — либо объект, либо список объектов, то мы обращаемся ко всему, используя либо точку, либо квадратные скобки:

$ echo '{"first_name": "Paul", "last_name": "McCartney"}' | jq "."
{
  "first_name": "Paul",
  "last_name": "McCartney"
}
$ echo '{"first_name": "Paul", "last_name": "McCartney"}' |
    jq ".first_name"
"Paul"
$ echo '[{"name": "John"}, {"name":"Paul"}, {"name":"George"},
    {"name": "Ringo"}]' | jq '.[] | select(.name | contains("o"))'
{
  "name": "John"
} {  "name": "George"
}
{
  "name": "Ringo"
 
}

Чтобы полнее описать потенциал этой утилиты, приведу один совсем суровый пример, который реально использовался в продакшене:

$ cat file.csv | csvjson —stream | jq -c 'if .createdDate != ""
then .createdDate = (.standardRegCreatedDate | split(" ") |
.[0:2] | join("T") + "Z" ) else .createdDate = "9999-01-01T00:00:00Z"
| to_entries | map(select(.key | contains("rawText") | not ) ) |
from_entries' | ...

Предыстория здесь такова. У меня была прорва CSV-файлов, в которых в колонке createdDate было полно всякой дряни, не имеющей отношения к датам. Но в поле standardRegCreatedDate даты стояли обычно верные, хотя в не совсем корректном формате. Да еще попадались поля с нечищеными данными — в их названии значились слова rawText, и их можно было вообще выкинуть. Для того чтобы все это разрулить, пришлось написать вот такую довольно громоздкую конструкцию с манипуляцией строками, преобразованием в пары ключ — значение и обратно.

Но и это еще не все. Приведенный мегаконвейер не полный, ведь данные надо было не только обработать, но и залить в Elasticsearch, и не просто залить, а в виде HTTP bulk-запросов в определенном формате (если очень интересно — об этом можно почитать здесь). И вот здесь я как раз и задействовал описанную выше технику распараллеливания:

$ ... | awk '{ print "{\"index\": {} }","\n" $0 }' | parallel
    --pipe -N500 curl -s -XPOST localhost:9200/items/entry/_bulk
    --data-binary @> /dev/null

jq можно опробовать и не устанавливая на комп

С awk все просто: формируем запросы определенной структуры. А вот что происходит дальше. Parallel запускает одновременно столько процессов curl, сколько у нас есть ядер CPU, открывает с каждым из них pipe и отдает каждому на stdin по 500 строк из ввода. Каждый curl принимает все данные с stdin и швыряет их сразу в базу, используя запросы к bulk API. Такими темпами я за несколько часов залил более 10 Гбайт данных, что в целом неплохо — по крайней мере куда быстрее, чем обычный цикл.

Консольные монстры

Данных вокруг нас становится все больше, и работать над понижением уровня энтропии во вселенной все сложнее. Я надеюсь, что использование описанных трюков поможет тебе при выполнении рутинных задач с файлами, данными и процессами, будь ты data scientist’ом, инженером, волшебным гномом или даже сисадмином. В конце концов, можно даже придумать, как подобными трюками впечатлить девушку.