Linux tools that make life easier. Первая часть

Расшифровка семинара Григория Серебрякова (Xperience AI), который состоялся в мае 2020 года. Видеоверсия доступна для просмотра на нашем youtube-канале.

Небольшой дисклеймер – здесь будут довольно простые вещи, никакого rocket science. Я по жизни работаю только в линуксовых средах целиком и полностью. Windows я последний раз близко видел в университете. Поэтому у меня выработалось какое-то количество привычек и тулов, которые я использую или использовал каждый день. Они все довольно известные, какие-то из них – это стандартные Unix утилиты, какие-то – чуть более экзотические. Тем не менее, не все о них знают, особенно когда начинают свой путь в качестве разработчика или дата-саентиста.

Если кратко пробежаться по темам, то сначала я покажу консольные тулы для поиска внутри файлов, поиска файлов и узнавания какой-то информации о них; дальше немножко посмотрим на YAML и JSON и как с ними работать из консоли. Далее рассмотрим типичную проблему – кто-то сожрал память на моем диске, как узнать, кто? Потом поговорим о том, как же копировать, синхронизировать и перекладывать большие датасеты между машинами.

Для начала, давайте немного вспомним историю. Когда-то давно, когда компьютеры были большими, программисты исключительно бородатыми и работали в университетах и всяких исследовательских лабораториях, основной ОС был Unix. Потом у него появилась куча всяких вариантов. Линукс – один из них. Другой вариант, еще более популярный, особенно если мы говорим о настольных машинах – это MacOS. Таким образом из ОС, которые не Unix, остается только Windows. Windows шел своим путем, но фишка в том, что на серверах, на встраиваемых устройствах, всегда было что-то Unix подобное. Эта культура создала множество утилит, которые работают во всех ОС. Люди ими пользуются, перетаскивают за собой какие-то конфиги, настройки и привычки.

Утилиты для поиска

Давайте для разминки начнем с простых вещей. Что нам обычно нужно делать, когда мы находимся в консоли? Нам нужно что-нибудь найти и посмотреть на файлы. Для того, чтобы посмотреть, что нас окружает, есть замечательная команда ls, которая расшифровывается, как list. Все про нее знают, она показывает список файлов. У нее есть куча ключей. Типичный вариант, в котором я использую ls, это ls -lht, который нам показывает файлы не врассыпную, а упорядоченным списком. Кроме этого, он показывает владельца, размер, дату создания, права, сортирует по времени, а главное – размеры показывает в читаемом формате. Другой ключ – это -A. -a все знают, он показывает все файлы, которые начинаются с точки, включая скрытые. При этом он еще показывает два специальных сервисных файла, точка и две точки, которые говорят нам про текущий директорий и директорий выше. -A их не показывает, что чрезвычайно удобно. Вспоминаем основные ключи. -ls показывает файлы, ls -t их сортирует по modification time, -ls -R показывает структуру подкаталогов, ls -l показывает дополнительные атрибуты. Все просто и понятно.

Проблемы начинаются, если вы скачали ImageNet, свалили его в один каталог и попытались натравить на него ls в Ubuntu. Это будет долго. Иногда очень долго, зависит от вашего диска. Дело в том, что в Ubuntu и во многих дистрибутивах по умолчанию ls – это alias на ls --color, который говорит ls "а раскрась мне пожалуйста файлы и каталоги в зависимости от их атрибутов, чем это является". Соответственно, вместо того, чтобы просто спросить у файловой системы, какие есть файлы, утилите нужно пройтись по каждому файлу, попросить у него атрибуты, принять решение. Если файлов много – это долго. alias можно обойти, позвав по явному пути /bin/ls, либо можно явно передать ключ ls --color=never. В этом случае ls ничего не будет раскрашивать и просто выкинет некрасивую серую портянку файлов. Зато быстро.

С ls закончили, переходим с find. Опять же, всем известная утилита, которая позволяет что-нибудь найти. Куча ключей, синтаксис, придуманный марсианами. Тем не менее. Допустим, мы хотим найти все .md файлы, которые я редактировал не позже, чем вчера, в текущей директории. Вот команда: find . -name "*.md" -user $USER -mtime +1. Если мы хотим что-то с этими файлами сделать, у find есть ключ -exec, туда можно написать простую команду, например, find . -type f -name "\*.md" -exec rm {} \;, и вместо фигурных скобок вставятся имена найденных файлов, по одному за раз. Типичная проблема с exec – это забытый \; в конце команды. Если это забыть, то find ругнется странным образом и ничего не сделает. Обычно простого exec не хватает, хочется что-то более сложное. Для этого можно воспользоваться мощью пайпов. В шэллах есть возможность строить пайплайны из команд, передавая выход одной команды на вход другой. Делается это при помощи штуки, которая называется пайп. Синтаксически это выглядит как вертикальная палка. Вот пример команды: find . -type f -print 0 | xargs -0. На уровне ОС создаются специальные файловые объекты, через которые передаются данные. Написали палку, аргументы, и выход того, что слева от палки, стало входом того, что справа. Хитрость с find состоит в том, что файлы могут быть с пробелом в именах. Если вы хотите обработать такие файлы, хочется их обозначить каким-то разделителем, который не пробел – иначе мы не поймем, где заканчивается имя одного файла и начинается имя другого файла. На этот случай у find есть ключ -print0, который просто разделяет его output символом /0. Очень удобно, никогда не перепутаем с пробелами. А отправляю я это все на вход в утилиту xargs, которая входные данные различными способами. У нее тоже есть ключ -0, который говорит, что входные данные разделены /0, и куча всяких ключей. Можно запускать команды параллельно, можно группировать входные данные кусочками по несколько единиц. Итого, запоминаем – find позволяет нам что-нибудь поискать. Простой вариант – find путь с опциями. Полезные опции – это -name, который говорит, какие имена могут быть, ну и -user, -atime, а также -d, который ограничивает, насколько глубоко по структуре директорий ищет find и много-много других ключей, которые можно посмотреть в справке.

Перейдем к grep. find ищет в файловой системе файлы, grep ищет текст внутри файлов. Примеры из жизни программиста – на питоне хотим найти, где же у нас в программе определена entry point. Делаем вот так: grep -rinH "__main__", и нам показывается типичная строчка if __name__ == "__main__", но рядом присутствует дополнительная магия. Что это за магия, я сейчас объясню. Ключ -r говорит искать рекурсивно по файловой системе, -i говорит не обращать внимания на регистр, то есть если бы у меня где-то было написано MAIN, то тоже бы нашлось. -n говорит, что нужно написать номер строки, а -H говорит, что нужно написать имя файла. Дальше эту строчку с двоеточием очень удобно открыть в каком-нибудь консольном текстовом редакторе и сразу прыгнуть к ней. Однобуквенные ключи можно соединить в один ключ и не писать каждый раз минус. То есть у нас есть отдельные ключи -r -i -n -H, я их все соединил в одну команду и получилось -rinH. Другой пример – если мы хотим поискать что-нибудь в списке установленных пакетов, в данном случае я поискал gimp. Здесь я опять воспользовался мощью пайпов и передал выход apt на вход grep. Получилось apt search gimp | grep installed. apt, конечно, будет долго ругаться в консоль, что у него нет стабильного интерфейса, но мы скрипты не пишем, поэтому нам пофиг, берем и пользуемся. Помимо того, что grep позволяет поискать просто какое-то слово, он позволяет искать с помощью регулярных выражений (regular expressions). По умолчанию, там шэлловские выражения и они выглядят очень непривычно большинству людей. К счастью, есть ключ -P, который говорит, что нужно использовать синтаксис регулярных выражений, совместимых с Perl. Такие регулярные выражения используются в большинстве языков программирования и скорее всего, вы с ними знакомы, если вы умеете писать регулярные выражения.

У grep есть типичный недостаток – он медленный. Типичный пример, когда это проявляется – вы сидите, пишите какой-нибудь computer vision код и у вас в одном каталоге и код, и здесь же папка с данными, в которой куча файлов с изображениями. grep будет усиленно пытаться поискать по картинкам. У него ничего не получится, но он будет долго и мучительно их перебирать. Вам от этого будет больно, потому что это будет медленно. Естественно, мы не одиноки с такими проблемами, поэтому есть несколько улучшений поверх базового grep.

Первый из них - это git grep. У git есть команда grep, это все тот же старый добрый grep, но он ищет только по файлам, которые находятся под контролем git. Если мы свалили наши данные в каталог с кодом, то git grep по ним искать не будет. Из-за этого он будет работать сильно быстрее. Дополнительная фишка, которая есть у grep – можно показать не только текущую строчку, но и соседние. Например, нужно показать три строки и три строки после. Это делается с помощью команды git grep -iIC3 "__main__". означает после, то есть after, -B – before, до, – и до, и после. Еще он может искать по всяким гитовым штукам, git index, git blob и так далее.

Человеческая мысль пошла дальше, и люди решили, что grep написан давно, он медленный, а мы сейчас напишем что-нибудь быстрое и современное. В результате появилась утилита, которая называется silver-searcher-ag, которая в консоли после установки отзывается просто на команду ag. И есть ripgrep, который в консоли отзывается просто на rg. Первая появилась пораньше. На чем написан silver searcher, я не помню, ripgrep написан на Rust. Обе сильно многопоточны, grep работает только в один поток. Получается действительно быстрее, чем grep, просто тупо утилизируют весь ваш CPU и в процессе все файлы, какие могут найти. Здесь бывает обратная сторона этой быстроты, на которую я напоролся, работая с Visual Studio Code. У него стандартное средство для поиска по всем файлам – это ripgrep. Когда он запускался, он просто выжирал весь input output с моего диска, потому что процессов было слишком много, а диск был HDD. Вещь, придуманная для ускорения, сама себя убила. Но это специфический случай, просто имейте его в виду.

И, наконец, fzf. Все вот эти grep подобные штуки - это хорошо, но наши усилия по поиску чего-нибудь ограничены тем, что нужно либо знать точное совпадение, что мы точно ищем, либо какие-то регулярные выражения придумать. В какой-то момент появились утилиты, которую ищут по нечеткому совпадению, без какой-либо регулярки. Условно – хотели мы поискать слово cat, но вместо этого ввели ct, опечатались. Нечеткий поиск их найдет. cat совпадет с ct. Классический пример такой утилиты – это fzf. Крутость этой штуки состоит в том, что fzf встраивается, куда только может.

Файлы с данными/конфигурацией

Когда-то давно типичным форматом хранения данных в больших бизнес-предложениях был .xml. Кто сталкивался с .xml в реальной жизни? Я сталкивался. Это боль и страдания. Если вам не нужно поддерживать большой enterprise legacy приложение, то лучше им не пользоваться, потому что формат очень избыточный. С одной стороны, он вроде бы текстовый, а с другой стороны, человек его читает с большим трудом. Человечество изобрело формат попроще и покомпактнее. Один пришел из Java Script и он называется JSON, Java Script Object Notation. А другой называется YAML - Yet Another Markup Language. Они очень похожи, википедия мне подсказала, что на самом деле с точки зрения формата JSON является подтипом YAML. JSON очень простой, вы можете найти парсер JSON практически в любом языке программирования, который приходит вам в голову. Но у него есть некоторое количество недостатков. Большой недостаток – там нельзя написать комментарий. Иногда хочется, особенно если это конфиг. У YAML есть комментарии, это уже достижение. У него есть в принципе все, что умеет JSON и еще куча разного сверху. А именно – там можно делать якоря, ссылки, подстановку кусков документа в самого себя и так далее. На YAML можно развесистую систему в конфигах написать, не написав ни строчки кода. Никто, кроме вас, правда, ее не прочитает, но это дело житейское. С YAML можно столкнуться, пойдя и попробовав отредактировать CI пайплайн в GitLab. Я когда-то писал пайплайны по-простому – по шагам. Потом я в какой-то момент увидел, что это YAML, в котором можно видеть подстановки. Больше того, в доке у GitLab написано, как это можно делать. Оказалось, что не все и не везде подставляется, есть куча ограничений. Но какие-то повторяющиеся вещи, отличающиеся только переменными среды, писать таким образом очень удобно. Вы описываете какой-то набор шагов, который везде одинаковый, а дальше, в зависимости от выставленных переменных окружений, меняете поведение всей системы, просто подставляете один и тот же кусок конфига везде. yaml не такой простой, как JSON из-за этой всей магии, поэтому нужно быть очень аккуратным.

Но мы сейчас про консольные тулы говорим. В какой-то момент жизни мне приходилось очень много работать с JSON. У меня большие датасеты, которые приходили от людей, которые занимались разметкой, там были специальные веб-тулы. Ребята занимались серьезным продакшеном, они подумали, как сделать все поменьше. JSON у них был записан без переноса строк. Действительно, так получается компактнее, но мне хотелось посмотреть на него глазами. Приходит мне JSON файл весом около 150 мегабайт. Минутка рекламы консольных текстовых редакторов – они с таким справляются и открывают. Графические большая часть отваливается, Sublime кряхтит, но справляется. Какой-нибудь PyCharm или VSCode – я не советую засовывать туда такой JSON, будете, скорее всего перезагружать компьютер, когда память закончится. Тем не менее, что делать, если хочется читать файл глазами, смотреть отдельные строчки, но пришел вот такой неотформатированный json? Тут нужно вспомнить, что у питона есть JSON парсер и он имеет консольный API в виде команды json.tool, которая ничего не умеет, но может красиво отформатировать файл. Таким образом, можно получить файл, в котором каждый новый элемент массива будет на новой строчке, правда из 150 мегабайт он увеличится в два раза, но зато вы сможете с ним удобно работать.

Но это будет не очень быстро. В какой-то момент я для себя открыл, что есть специальные утилиты, которые умеют работать с JSON в командной строке. Собственно, утилита называется JQ - Command Line JSON Processor. Она сильно умнее, чем питоновский json.tool, потому что она позволяет работать с JSON именно как с JSON. Можно прописывать фильтры, делать запросы, выдирать элементы. Она понимает структуры, она понимает, что есть массивы, она поддерживает синтаксис, у нее есть свой DSL, который позволяет пройтись по массиву, выдрать из массива все элементы, у которых дочернее поле равно чему-то, или наоборот – получить значение дочернего поля. Очень могучая штука, которую я использовал исключительно для того, чтобы получить красивый json, на который я мог посмотреть своими глазами при помощи ключа -S. Специально для вас я нашел пример. У нас есть JSON файл, в нем лежит массив и мы хотим из элементов этого массива в диапазоне с 10-го по 15-й получить значения поля file name. Выглядит это вот так: -jq ".[10-15] filename" <input.json>.

Что делать с YAML. Для YAML стандартного модуля для питона нет, но всегда можно доставить из PyPI. Это небольшая проблема, благо еще и OpenCV нативно и YAML, и JSON. И естественно есть консольная утилита. Называется она yq, потому что она для YAML делает примерно то же самое, что и JQ для JSON. Больше того, она поддерживает как и YAML, так и JSON файлы, потому что как мы выяснили, JSON - это подмножество YAML. Синтаксис, естественно, другой.

Общий совет – не пользуйтесь xml и ini конфигами, это такие из Windows пришедшие варианты. Если у вас есть опции и вы можете сами выбрать стиль конфигов или дата файлов – выберите yaml или json и живите с ними.

Утилиты для работы с памятью

Стандартная проблема – комп тупит, тормозит. Окошки не двигаются, частая причина – мы качали очередной датасет и место кончилось. Что делать?

Для начала неплохо бы проверить, что место действительно кончилось. Для этого есть утилита df. Говоря русским языком, disk free. У нее ключ -h, который вместо байтов покажет нам нормальные мегабайты и гигабайты. Запущенная на корень, она нам выдаст информацию по всем подмонтированным устройствам. В моем случае, она говорит, что у моего устройства /dev/nvme0n1p2 свободен 51% памяти в корне, что нормально. Если у вас home лежит на отдельном диске, то здесь вы это увидите и увидите, что место кончилось. Или не кончилось. Тем не менее, кто-то использовал 112 гигабайт из моего 234-гигабайтного диска. Хотелось бы найти паршивца, призвать к ответу и удалить. Дальше начинается черная магия. В паре с командой df есть команда du, disk usage. Она нам покажет, сколько каждая папка занимает места на диске. В данном случае я опять использую ключ -h. -h во многих утилитах предлагает показать help, а вот в тех утилитах, которые про память и про файлы, он предлагает показать результат в читаемом виде. Опять видим ключ -d, такой же, как у find, но он здесь работает по-другому. У find -d говорит, насколько глубоко копать по каталогам. Если мы ему скажем -d1, то он сходит только в подкаталоги текущего каталога. А у du -d говорит покажи мне информацию о каталогах, которые находятся не дальше, чем на один уровень ниже, чем текущий. Он посчитает статистику по всем файлам, которые лежат и ниже, но сгруппирует ее для каждого каталога. Поскольку я ищу от корня, я могу понять, какие высокоуровневые каталоги занимают больше всего места и в них поискать отдельно. Еще можно отдельно пользоваться ключами --exclude, чтобы убрать, например, ваши диски с данными, которые дополнительно подмонтированы. Зачем вам знать, сколько у вас гигабайт датасетов на HDD лежит, если вы пытаетесь почистить место на SSD. Также полезно делать --exclude на устройства /loop и /proc, потому что они системные, вы там все равно ничего полезного вы не найдете, если только не положили туда файлы по какой-то странной прихоти. Опять пользуемся пайпом, чтобы передать в команду sort, получается sudo du -h -d1 --exclude='/media*' --exclude='/loop' --exclude='/proc' /| sort -h. Ну а sort сортирует нам вывод, причем с ключом -h она понимает килобайты, мегабайты и понимает, что 96К меньше, чем 4М. Получили какой-то результат. Так жить можно, но сложно и долго, потому что я поискал, заметил, что у меня в /home 61G, мне нужно будет пойти в /home и поискать еще раз и так далее.

Есть совершенно чудесная утилита, про которую я очень долго не знал, называется ncdu. du = disk usage, nc = консольный графический интерфейс к нему. Вы его ставите, запускаете, она что-то делает, а потом показывает вам вот такой интерфейс. Казалось бы, все примерно то же самое, что мы видели с обычным du, только в текстовом интерфейсе. Вся прелесть состоит в том, что, во-первых, вы можете походить по этой структуре. Например, я могу нажать на /home и на Enter, провалиться внутрь и походить под каталогом. Работает, как в Midnight Commander. А во-вторых – я прямо отсюда могу что-нибудь удалить. Жмем кнопочку d и удаляем. Существенно быстрее, чем искать что-то при помощи du.

Утилиты для загрузки

Все, конечно же, знают про wget. Чаще всего я пользуюсь RSYNC. Она подходит для передачи данных между машинами внутри одной сети, для передачи данных внутри одной машины – скопировать, чтобы клонировать файловую систему и так далее. RSYNC – универсальная штука по копированию, у которой, как обычно, есть куча опций, но базовая комбинация -a, -u и –z. Под -a скрывается «сохрани владельца», modification time и так далее. -z говорит, что неплохо бы их сжать. Полезно, когда передаем по сети. -u говорит, что если такие файлы уже есть, то их передавать их не надо. Как это работает. RSYNC – многокомпонентная система. Она, насколько я понимаю, запускает на удаленной машине такой же RSYNC и они между собой обмениваются информацией о том, что нужно передавать, а что нет. Основная проблема RSYNC, на мой взгляд, состоит в том, что информацию о прогрессе вашей задачи копирования к нему приделали как-то криво, потому что стандартный ключ -progress показывает информацию о прогрессе данного конкретного файла, который копируется прямо сейчас и ничего не говорит о том, когда же, собственно, скопируется весь ваш гигантский датасет. Такое ощущение, что нормальную информацию о прогрессе задачи к нему прикручивали сбоку и потом, поэтому она выглядит вот так: --info=progress2. Почему два – не знаю.

И вот здесь пример, про который я уже заикнулся. Периодически я страдаю таким приколом – мне нужно взять уже существующую машину и клонировать у нее rootfs, чтобы сделать такую же, но новую. Вот эта комбинация ключей позволяет это сделать и все работает. Несколько машин я уже таким образом сочинил.

Поскольку RSYNC универсален, он работает не только в пределах одной машины, но и в пределах нескольких. А между несколькими машинами мы ходим при помощи ssh. Если у вас стандартный ssh на стандартном порту, то можно просто отправить RSYNC на user@remotemachine и там написать путь до файлов. А если у вас порт не стандартный, то можно воспользоваться командой -e, которая скажет, как же пользоваться вашим ssh. Ну и не забываем про интересный ключ--ignore-existing, который пропускает файлы, которые у вас уже есть.

Самая типичная проблема с RSYNC – он по-разному обрабатывает две ситуации - когда мы не написали слэш в конце пути до источника файлов (source destination) и когда написали (source/ destination). В первом варианте он нам скопирует папочку source внутрь папки destination и все ее содержимое, а вот во втором варианте он скопирует только содержимое. В остальном RSYNC замечательный.

Часто нам нужно скачать что-то не из локальной сети, а из интернета. Вот здесь обычно говорится про wget. wget – замечательная утилита, но она умеет качать только в один поток. Я всем обычно в этот момент рекламирую утилиту ARIA2, которая в консоли почему-то представляется в виде команды aria2c, которая умеет ходить на один и тот же сервер за одним и тем же файлом в кучу потоков, устанавливая кучу параллельных соединений, и выкачивать этот файл кусочками, параллельно, комбинируя. Таким образом, вместо скорости в 500 килобайт, которую добрые создатели файла с датасетом вам предоставили, вы можете получить вполне себе приличные 1,5 мегабайта и сэкономить изрядное количество времени. По поводу ключей – я обычно пишу -x5 -s5 -j5 и оно работает, что меня устраивает. Но на самом деле все эти ключи значат немного разные вещи. Более того, у некоторых из них есть значения по умолчанию. Помимо того, чтобы указать ARIA2 один URL, который нужно скачать, ей можно дать при помощи ключа -i файл, в котором на каждой строчке будет написан URL для нового файла. И в этом случае комбинация этих ключей начинает работать немножко по-другому, потому что -s нам теперь говорит, сколько соединений будет открыто, -j говорит, сколько будет приходиться на каждый элемент списка. -x всегда имеет смысл ставить достаточно большим, потому что он ограничивает количество соединений к одному серверу. В целом, если вам нужно скачать один большой файл, можно выставить все три ключа в -5 или -15 и все будет работать сильно лучше, чем с wget. Если вам нужно скачать длинный список файлов, потратьте две минуты на справку и посмотрите, в чем разница между ключами.

Утилиты для работы с облаком

Помимо ситуаций, когда нужно что-то клонировать в локальной сети и ситуаций, когда нужно что-то забрать из интернета, существует еще одна ситуация. Иногда тебе хочется в свой или компанейский Google Диск, Яндекс.Диск, Dropbox, Google Cloud и так далее что-то записать или прочитать оттуда. Кажется, что с Google Диском эта задача не выполнима, потому что они стремятся к тому, чтобы все было через браузер. На самом деле нет. Есть утилита RCLONE, вдохновленная RSYNC. Ее нужно установить, у них есть подробный мануал, как ее присоединить к каждому типу хранилища. Если у вас есть такая необходимость, например, вы хотите бекапить какие-нибудь данные регулярно в Google Диск, но у вас Linux и нет нормального официального клиента. Вы можете это делать через rclone. Настраиваете авторизацию, ставите его в регулярный запуск и все замечательно.

Q&A

Q: find известен своей высокой скоростью работы. Как так вышло?

A: Линукс использует остатки RAM как дисковый кэш, соответственно, дерево файловой системы чаще всего лежит в RAM, поэтому это получается быстро и эффективно. А если мы туда уже сходили, оно точно лежит в RAM, поэтому find работает быстро.

Q: Можно ли в JQ делать запросы типа Query по значениям полей? Если да, то как?

А: По-моему, можно. Вот, например, jq "if . ==0 then "zero" elif . ==1 then "one" else "many". Он много чего умеет.