Что каждый специалист по данным должен знать о оболочке

Сейчас ему 50 лет, а мы до сих пор не можем придумать, как его назвать. Командная строка, оболочка, терминал, bash, подсказка или консоль? Мы будем называть это командной строкой, чтобы все было согласованно.

В статье основное внимание будет уделено командной строке в стиле UNIX (Linux и Mac) и для ясности проигнорировано все остальное (например, командный процессор Windows и PowerShell). Мы заметили, что в наши дни большинство специалистов по обработке и анализу данных используют системы на базе UNIX.

Что это такое?

Командная строка представляет собой текстовый интерфейс для вашего компьютера. Вы можете думать об этом как о «раскрытии капота» операционной системы. Некоторые ошибочно принимают его за пережиток прошлого, но не дайте себя обмануть. Современная командная строка качается, как никогда раньше!

Раньше текстовый ввод и вывод были всем, что у вас было (то есть после перфокарт). Как и у самых первых автомобилей, у первых операционных систем даже не было крышки, которую можно было бы открыть. Все было на виду. В этой среде так называемая методология REPL (цикл чтения-оценки-печати) была естественным способом взаимодействия с компьютером.

REPL означает, что вы вводите команду, нажимаете ввод, и команда немедленно оценивается. Он отличается от циклов редактирования-запуска-отладки или редактирования-компиляции-запуска-отладки, которые вы обычно используете для более сложных программ.

Зачем мне это использовать?

Командная строка обычно следует философии UNIX: «Заставьте каждую программу хорошо выполнять одну задачу», поэтому основные команды очень просты. Основная предпосылка заключается в том, что вы можете делать сложные вещи, комбинируя эти простые программы. Старые шейные бороды UNIX означают «разговор с компьютером».

Почти любой язык программирования в мире мощнее, чем командная строка, и большинство графических интерфейсов типа «укажи и щелкни» проще в освоении. Зачем вам вообще что-то делать в командной строке?

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

Вторая причина – агностицизм. Какой бы стек, платформу или технологию вы ни использовали в настоящее время, вы можете взаимодействовать с ними из командной строки. Это как клей между всеми вещами. Это также повсеместно. Везде, где есть компьютер, где-то есть и командная строка.

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

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

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

Как это работает?

В работе командной строки есть примерно четыре уровня:

Терминал = приложение, которое захватывает ввод с клавиатуры, передает его запущенной программе (например, оболочке) и возвращает результаты обратно. Поскольку в наши дни все современные компьютеры имеют графические пользовательские интерфейсы (GUI), терминал является необходимым интерфейсным слоем GUI между вами и остальной частью текстового стека.

Оболочка = программа, которая анализирует нажатия клавиш, передаваемые терминальным приложением, и обрабатывает запущенные команды и программы. Его работа в основном состоит в том, чтобы найти, где находятся программы, позаботиться о таких вещах, как переменные, а также обеспечить причудливое завершение с помощью клавиши TAB. Существуют различные варианты, такие как Bash, Dash, Zsh и Fish, и это лишь некоторые из них. Все с немного разными наборами встроенных команд и опций.

Команда = компьютерная программа, взаимодействующая с операционной системой. Типичными примерами являются такие команды, как ls, mkdir и rm. Некоторые из них встроены в оболочку, некоторые представляют собой скомпилированные двоичные программы на вашем диске, некоторые представляют собой текстовые скрипты, а некоторые представляют собой псевдонимы, указывающие на другую команду, но, в конце концов, все это просто компьютерные программы.

Операционная система = программа, которая выполняет все остальные программы. Он обеспечивает прямое взаимодействие со всем оборудованием, таким как ЦП, жесткий диск и сеть.

Подсказка и тильда

Командная строка имеет тенденцию выглядеть немного по-разному для всех.

Однако обычно есть одна общая черта: приглашение, вероятно, представленное знаком доллара ($). Это визуальная подсказка, где заканчивается статус и где вы можете начать вводить свои команды.

На моем компьютере в командной строке написано:

juha@ubuntu:~/hello$

juha — это мое имя пользователя, ubuntu — имя моего компьютера, а ~/hello — мой текущий рабочий каталог.

А что не так с этой тильдой (~)? Что вообще означает, что текущий каталог ~/hello?

Тильда — это сокращение от домашнего каталога, места для всех ваших личных файлов. Мой домашний каталог — /home/juha, поэтому мой текущий рабочий каталог — /home/juha/hello, что сокращается до ~/hello. (Соглашение ~username относится к чьему-либо домашнему каталогу в целом; ~juha относится к моему домашнему каталогу и так далее.)

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

Анатомия команды

Ранее мы описывали команды просто как компьютерные программы, взаимодействующие с операционной системой. Хотя правильно, давайте будем более конкретными.

Когда вы вводите что-то после приглашения и нажимаете ввод, программа оболочки попытается проанализировать и выполнить это. Скажем:

$ generate million dollars 
generate: command not found

Программа оболочки берет первое полное слово generate и считает его командой.

Два оставшихся слова, million и dollars, интерпретируются как два отдельных параметра (иногда называемых аргументами).

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

В нашем примере такая команда с именем generate не найдена, и мы получаем сообщение об ошибке (это ожидаемо).

Давайте запустим команду, которая действительно работает:

$ df --human-readable

Filesystem      Size  Used Avail Use% Mounted on
sysfs              0     0     0    - /sys
proc               0     0     0    - /proc
udev             16G     0   16G   0% /dev
. . .

Здесь мы запускаем команду «df» (сокращение от disk free) с опцией «--human-readable».

Обычно используется - (тире) перед сокращенным вариантом и - (двойное тире) для полной формы. (Эти соглашения со временем менялись; см. эту публикацию в блоге для получения дополнительной информации.)

Например, это одно и то же:

$ df -h 
$ df --human-readable

Как правило, вы также можете объединить несколько сокращенных вариантов после одного тире.

df -h -l -a 
df -hla

Примечание. Форматирование в конечном итоге зависит от каждой команды, поэтому не считайте эти правила универсальными.

Поскольку некоторые символы, такие как пробел или обратная косая черта, имеют особое значение, рекомендуется заключать строковые параметры в кавычки. Однако для bash-подобных оболочек существует разница между одинарными (‘) и двойными кавычками («). Одинарные кавычки воспринимают все буквально, а двойные кавычки позволяют программе интерпретировать такие вещи, как переменные. Например:

$ testvar=13 
$ echo "$testvar" 
13 
$ echo '$testvar' 
$testvar

Если вы хотите узнать все доступные параметры, вы обычно можете получить список с параметром --help:

Совет. Обычно в командную строку вводится длинный путь к файлу. Большинство программ-оболочек предлагают клавишу TAB для автоматического завершения путей или команд, чтобы избежать повторного ввода. Попробуйте!

Различные типы команды

Существует пять различных типов команд: двоичные, скриптовые, встроенные, функциональные и псевдонимы.

Мы можем разделить их на две категории: файловые и виртуальные.

Двоичные и скриптовые команды основаны на файлах и выполняются путем создания нового процесса (концепция операционной системы для новой программы). Команды на основе файлов имеют тенденцию быть более сложными и тяжеловесными.

Встроенные модули, функции и псевдонимы являются виртуальными и выполняются в рамках существующего процесса оболочки. Эти команды в основном простые и легкие.

двоичный файл – это классический исполняемый программный файл. Он содержит двоичные инструкции, понятные только операционной системе. Вы получите тарабарщину, если попытаетесь открыть его с помощью текстового редактора. Двоичные файлы создаются путем компиляции исходного кода в исполняемый двоичный файл. Например, команда интерпретатора Python python является двоичным исполняемым файлом.

Для двоичных команд программа оболочки отвечает за поиск фактического двоичного файла в файловой системе, который соответствует имени команды. Однако не ожидайте, что оболочка будет искать команду повсюду на вашем компьютере. Вместо этого оболочка полагается на переменную среды с именем $PATH, которая представляет собой список путей, разделенных двоеточием (:), для повторения. Всегда выбирается первое совпадение.

Чтобы проверить текущий $PATH, попробуйте следующее:

Если вы хотите выяснить, где находится двоичный файл для определенной команды, вы можете вызвать команду which.

$ which python 
/home/juha/.pyenv/shims/python

Теперь, когда вы знаете, где найти файл, вы можете использовать утилиту file, чтобы выяснить общий тип файла.

$ file /home/juha/.pyenv/shims/pip
/home/juha/.pyenv/shims/pip: Bourne-Again shell script text executable, ASCII text
$ file /usr/bin/python3.9
/usr/bin/python3.9: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, stripped

Скрипт — это текстовый файл, содержащий удобочитаемую программу. Сценарии Python, R или Bash — это некоторые распространенные примеры, которые вы можете выполнять как команду.

Обычно мы не выполняем наши скрипты Python как команды, а используем интерпретатор следующим образом:

$ python hello.py 
Hello world

Здесь python — это команда, а hello.py — просто ее параметр. (Если вы посмотрите на то, что говорит python --help, вы увидите, что это соответствует варианту «файл: программа, считанная из файла сценария», что действительно имеет смысл здесь.)

Но мы также можем выполнить hello.py напрямую, как команду:

$ ./hello.py 
Hello world

Чтобы это работало, нам нужны две вещи. Во-первых, первая строка hello.py должна определить интерпретатор сценария с использованием специальной нотации #!.

#!/usr/bin/env python3 
print("Hello world")

Обозначение #! сообщает операционной системе, какая программа знает, как интерпретировать текст в файле, и имеет много крутых прозвищ, таких как shebang, hashbang или мое самое любимое hash-pling!

Второе, что нам нужно, это чтобы файл был помечен как исполняемый. Вы делаете это с помощью команды chmod (изменить режим): chmod u+x hello.py установит флаг исполняемого файла для пользователя-владельца.

Встроенная — это простая команда, жестко запрограммированная в самой программе-оболочке. Такие команды, как cd, echo, alias и pwd, обычно являются встроенными.

Если вы запустите команду help (которая также является встроенной!), вы получите список всех встроенных команд.

Функция похожа на дополнительную встроенную функцию, определяемую пользователем. Например:

$ hello() { echo 'hello, world'; }

Может использоваться как команда:

$ hello 
hello, world

Если вы хотите перечислить все функции, доступные в настоящее время, вы можете вызвать (в Bash-подобных оболочках):

Псевдонимы похожи на макросы. Сокращенное или альтернативное название более сложной команды.

Например, вы хотите, чтобы новая команда showerr выводила список последних системных ошибок:

$ alias showerr="cat /var/log/syslog"
$ showerr
Apr 27 10:49:20 juha-ubuntu gsd-power[2484]: failed to turn the kbd backlight off: GDBus.Error:org.freedesktop.UPower.GeneralError: error writing brightness
. . .

Поскольку функции и псевдонимы не являются физическими файлами, они не сохраняются после закрытия терминала и обычно определяются в так называемом файле профиля ~/.bash_profile или файле ~/.bashrc, которые выполняются при запуске новой интерактивной оболочки или оболочки входа в систему. Некоторые дистрибутивы также поддерживают файл ~/.bash_aliases (который, скорее всего, вызывается из файла профиля — это все сценарии!).

Если вы хотите получить список всех псевдонимов, активных в данный момент для вашей оболочки, вы можете просто вызвать команду alias без каких-либо параметров.

Объединение команд вместе

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

Процесс — это концепция операционной системы для запуска экземпляра команды (программы). Каждый процесс получает идентификатор, собственное зарезервированное пространство памяти и привилегии безопасности для работы в вашей системе. Каждый процесс также имеет стандартный ввод (stdin), стандартный вывод (stdout) и стандартный поток ошибок (stderr).

Что это за потоки? Это просто произвольные потоки данных. Кодировка не указана, значит может быть что угодно. Текст, видео, аудио, азбука Морзе, все, что автор команды сочтет нужным. В конечном счете, ваш компьютер — это просто прославленная машина для преобразования данных. Таким образом, имеет смысл, что у каждого процесса есть вход и выход, как и у функций. Также имеет смысл отделить поток вывода от потока ошибок. Если ваш выходной поток представляет собой видео, то вы не хотите, чтобы байты текстовых сообщений об ошибках смешивались с вашими байтами видео (или, в 1970-х годах, когда стандартный поток ошибок был реализован после того, как ваш фотонабор был испорчен сообщения об ошибках набираются вместо того, чтобы отображаться на терминале).

По умолчанию потоки stdout и stderr передаются обратно на ваш терминал, но эти потоки можно перенаправить в файлы или передать по конвейеру, чтобы они стали входными данными для другого процесса. В командной строке это делается с помощью специальных операторов перенаправления (|,>,<,>>).

Начнем с примера. Команда curl загружает URL-адрес и направляет стандартный вывод обратно в терминал по умолчанию.

$ curl https://filesamples.com/samples/document/csv/sample1.csv
"May", 0.1, 0, 0, 1, 1, 0, 0, 0, 2, 0, 0, 0
"Jun", 0.5, 2, 1, 1, 0, 0, 1, 1, 2, 2, 0, 1
"Jul", 0.7, 5, 1, 1, 2, 0, 1, 3, 0, 2, 2, 1
"Aug", 2.3, 6, 3, 2, 4, 4, 4, 7, 8, 2, 2, 3
"Sep", 3.5, 6, 4, 7, 4, 2, 8, 5, 2, 5, 2, 5

Допустим, нам нужны только первые три строки. Мы можем сделать это, объединив две команды вместе, используя оператор конвейера (|). Стандартный вывод первой команды (curl) передается как стандартный ввод второй (head). Стандартный вывод второй команды ( head) остается выводом на терминал по умолчанию.

$ curl https://filesamples.com/samples/document/csv/sample1.csv | head -n 3
"May", 0.1, 0, 0, 1, 1, 0, 0, 0, 2, 0, 0, 0
"Jun", 0.5, 2, 1, 1, 0, 0, 1, 1, 2, 2, 0, 1
"Jul", 0.7, 5, 1, 1, 2, 0, 1, 3, 0, 2, 2, 1

Обычно вам нужны данные на диске, а не на вашем терминале. Мы можем добиться этого, перенаправив стандартный вывод последней команды (head) в файл с именем foo.csv с помощью оператора >.

$ curl https://filesamples.com/samples/document/csv/sample1.csv | head -n 3 > foo.csv

Наконец, процесс всегда возвращает значение, когда он завершается. Когда возвращаемое значение равно нулю (0), мы интерпретируем это как успешное выполнение. Если он возвращает любое другое число, это означает, что выполнение имело ошибку и завершилось преждевременно. Например, любое исключение Python, которое не перехватывается командой try/except, имеет выход интерпретатора Python с ненулевым кодом.

Вы можете проверить, какое значение возвращала ранее выполненная команда, используя переменную $?.

$ curl http://fake-url
curl: (6) Could not resolve hostmm
$ echo $?
6

Раньше мы передавали две команды вместе с помощью потоков, что означало, что они выполнялись параллельно. Возвращаемое значение команды важно, когда мы объединяем две команды вместе с помощью оператора &&. Это означает, что мы ждем, пока предыдущая команда завершится успешно, прежде чем переходить к следующей. Например:

cp /tmp/apple.png /tmp/usedA.png && cp /tmp/apple.png /tmp/usedB.png && rm /tmp/apple.png

Здесь мы пытаемся скопировать файл /tmp/apple в два разных места и, наконец, удалить исходный файл. Использование оператора && означает, что программа оболочки проверяет возвращаемое значение каждой команды и утверждает, что оно равно нулю (успех), прежде чем двигаться. Это защищает нас от случайного удаления файла в конце.

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

Управляйте проектами по науке о данных как босс

Часто, когда специалист по обработке и анализу данных обращается к командной строке, это происходит потому, что он использует инструмент CLI (интерфейс командной строки), предоставляемый сторонней службой или облачным оператором. Типичные примеры включают загрузку данных из AWS S3, выполнение некоторого кода в кластере Spark или создание образа Docker для производства.

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

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

По умолчанию make используется для создания текстового файла с именем Makefile в корневом каталоге вашего проекта. Вы всегда должны фиксировать этот файл в своей системе контроля версий.

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

Создать файл

hello:
    echo "Hello world!"

Помните, мы говорили, что это классика 1976 года? Что ж, не без своих причуд. Вы должны быть очень осторожны, чтобы отступать от этой инструкции echo с помощью символа табуляции, а не любого количества пробелов. Если вы этого не сделаете, вы получите ошибку «отсутствует разделитель».

Чтобы выполнить нашу цель (или задачу) «hello», мы вызываем:

$ make hello
echo "Hello world!"
Hello world!

Обратите внимание, что make также выводит рецепты, а не только вывод. Вы можете ограничить вывод с помощью параметра -s.

$ make -s hello
Hello world!

Далее давайте добавим что-нибудь полезное, например, загрузку наших обучающих данных.

Создать файл

hello:
    echo "Hello world!"

get-data:
    mkdir -p .data

    curl <https://filesamples.com/samples/document/csv/sample1.csv>
    > .data/sample1.csv
    echo "Downloaded .data/sample1.csv"

Теперь мы можем загрузить наш пример обучающих данных с помощью:

$ make -s get-data
Downloaded .data/sample1.csv

(Кроме того: более опытные мастера Makefile из нашей читательской аудитории заметят, что get-data на самом деле следует называть .data/sample1.csv, чтобы воспользоваться преимуществами сокращений Makefile и зависимостей данных.)

Наконец, мы рассмотрим пример того, как может выглядеть простой Makefile в проекте по науке о данных, чтобы мы могли продемонстрировать, как использовать переменные с make и вдохновить вас:

Создать файл

DOCKER_IMAGE := mycompany/myproject
VERSION := $(shell git describe --always --dirty --long)

default:
    echo "See readme"

init:
    pip install -r requirements.txt
    pip install -r requirements-dev.txt
    cp -u .env.template .env

build-image:
    docker build .
        -f ./Dockerfile
        -t $(DOCKER_IMAGE):$(VERSION)

push-image:
    docker push $(DOCKER_IMAGE):$(VERSION)

pin-dependencies:
    pip install -U pip-tools
    pip-compile requirements.in
    pip-compile requirements-dev.in

upgrade-dependencies:
    pip install -U pip pip-tools
    pip-compile -U requirements.in
    pip-compile -U requirements-dev.in

Этот пример Makefile позволит членам вашей команды инициализировать свою среду после клонирования репозитория, закрепить зависимости при добавлении новых библиотек и развернуть новый образ докера с красивым тегом версии.

Если вы постоянно предоставляете хороший Makefile вместе с хорошо написанным readme в своих репозиториях кода, это даст вашим коллегам возможность использовать командную строку и последовательно воспроизводить всю вашу магию для каждого проекта.

Заключение

В конце концов, кому-то нравится командная строка, а кому-то нет.

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

Если эта тема вас вдохновила, я настоятельно рекомендую ознакомиться с бесплатной книгой Наука о данных в командной строке Йеруна Янссенса.

Первоначально опубликовано на https://valohai.com.