Среда командной строки
Как мы уже говорили на предыдущей лекции, большинство командных оболочек (shell) – это не просто пусковая площадка для других программ: на практике они предоставляют целый язык программирования со своими устоявшимися паттернами и абстракциями. Однако, в отличие от большинства языков программирования, в shell-скриптах всё построено вокруг запуска программ и того, чтобы они просто и эффективно общались друг с другом.
В частности, программирование на shell жёстко связано соглашениями. Чтобы программа с интерфейсом командной строки (command line interface, CLI) хорошо уживалась с остальным окружением оболочки, она должна следовать ряду общепринятых паттернов. Сейчас мы разберём многие концепции, необходимые для понимания того, как устроены программы командной строки, а также повсеместные соглашения о том, как их использовать и настраивать.
Интерфейс командной строки
В большинстве языков программирования функция выглядит примерно так:
def add(x: int, y: int) -> int:
return x + y
Здесь мы явно видим входные и выходные данные программы. Shell-скрипты же на первый взгляд могут выглядеть совсем иначе.
#!/usr/bin/env bash
if [[ -f $1 ]]; then
echo "Target file already exists"
exit 1
else
if $DEBUG; then
grep 'error' - | tee $1
else
grep 'error' - > $1
fi
exit 0
fi
Чтобы как следует понять, что происходит в подобных скриптах, нам сначала нужно ввести несколько понятий, которые часто встречаются, когда программы в оболочке общаются друг с другом или с окружением оболочки:
- Аргументы
- Потоки
- Переменные окружения
- Коды возврата
- Сигналы
Аргументы
Программы в командной оболочке при запуске получают список аргументов.
В shell аргументы – это просто строки, и как их интерпретировать, решает сама программа.
Например, когда мы выполняем ls -l folder/, мы запускаем программу /bin/ls с аргументами ['-l', 'folder/'].
Внутри shell-скрипта к ним обращаются через специальный синтаксис оболочки.
Первый аргумент доступен через переменную $1, второй – через $2 и так далее вплоть до $9. Чтобы получить все аргументы в виде списка, используем $@, а чтобы узнать их количество – $#. Кроме того, через $0 можно получить имя самой программы.
У большинства программ аргументы представляют собой смесь флагов и обычных строк.
Флаг легко опознать: перед ним стоит дефис (-) или двойной дефис (--).
Флаги обычно необязательны, и их задача – изменять поведение программы.
Например, ls -l меняет то, как ls форматирует свой вывод.
Вам будут встречаться флаги с двойным дефисом и длинными именами вроде --all, а также флаги с одинарным дефисом вроде -a, за которым чаще всего следует одна буква.
Одна и та же опция может быть задана в обоих форматах: ls -a и ls --all эквивалентны.
Флаги с одинарным дефисом часто группируют, так что ls -l -a и ls -la тоже эквивалентны.
Порядок флагов обычно тоже не важен: ls -la и ls -al дают одинаковый результат.
Некоторые флаги встречаются очень часто, и по мере знакомства со средой командной оболочки вы начнёте интуитивно тянуться к ним (например, --help, --verbose, --version).
Флаги – первый хороший пример соглашений (conventions), принятых в shell. Сам язык оболочки не требует, чтобы наша программа использовала
-или--именно таким образом. Ничто не мешает нам написать программу с синтаксисомmyprogram +myoption myfile, но это приведёт к путанице, ведь все ожидают, что мы используем дефисы. На практике большинство языков программирования предоставляют библиотеки для разбора флагов CLI (например,argparseв Python для разбора аргументов с дефисным синтаксисом).
Ещё одно распространённое соглашение для CLI-программ – принимать переменное число аргументов одного типа. Получив аргументы в таком виде, команда выполняет одну и ту же операцию над каждым из них.
mkdir src
mkdir docs
# is equivalent to
mkdir src docs
Поначалу этот синтаксический сахар может показаться лишним, но в сочетании с подстановкой (globbing) он становится по-настоящему мощным. Подстановка, или глобы (globs), – это специальные шаблоны, которые оболочка раскрывает до вызова программы.
Допустим, мы хотим удалить все файлы .py в текущей папке, не заходя в подкаталоги. Пользуясь тем, что мы узнали на предыдущей лекции, мы могли бы добиться этого, запустив
for file in $(ls | grep -P '\.py$'); do
rm "$file"
done
Но всё это можно заменить простым rm *.py!
Когда мы набираем rm *.py в терминале, оболочка не станет вызывать программу /bin/rm с аргументами ['*.py'].
Вместо этого она поищет в текущей папке файлы, подходящие под шаблон *.py, где * соответствует любой строке из нуля или более символов любого типа.
Так что если в нашей папке лежат main.py и utils.py, то программа rm получит аргументы ['main.py', 'utils.py'].
Самые распространённые глобы, которые вам встретятся, – это wildcard-символы * (ноль или более чего угодно), ? (ровно один любой символ) и фигурные скобки.
Фигурные скобки {} раскрывают разделённый запятыми список шаблонов в несколько аргументов.
На практике глобы лучше всего понимать на наглядных примерах.
touch folder/{a,b,c}.py
# Will expand to
touch folder/a.py folder/b.py folder/c.py
convert image.{png,jpg}
# Will expand to
convert image.png image.jpg
cp /path/to/project/{setup,build,deploy}.sh /newpath
# Will expand to
cp /path/to/project/setup.sh /path/to/project/build.sh /path/to/project/deploy.sh /newpath
# Globbing techniques can also be combined
mv *{.py,.sh} folder
# Will move all *.py and *.sh files
Некоторые оболочки (например, zsh) поддерживают ещё более продвинутые формы подстановки, такие как
**, которая раскрывается с учётом рекурсивных путей. Так чтоrm **/*.pyудалит все файлы .py рекурсивно.
Потоки
Всякий раз, когда мы запускаем конвейер программ вроде
cat myfile | grep -P '\d+' | uniq -c
мы видим, что программа grep общается и с программой cat, и с программой uniq.
Важное наблюдение: все три программы выполняются одновременно.
То есть оболочка не вызывает сначала cat, потом grep, а потом uniq.
Вместо этого все три программы запускаются сразу, а оболочка соединяет вывод cat со входом grep и вывод grep со входом uniq.
Используя оператор конвейера |, оболочка работает с потоками данных, которые текут от одной программы к следующей в цепочке.
Мы можем продемонстрировать эту параллельность – все команды в конвейере стартуют немедленно:
$ (sleep 15 && cat numbers.txt) | grep -P '^\d$' | sort | uniq &
[1] 12345
$ ps | grep -P '(sleep|cat|grep|sort|uniq)'
32930 pts/1 00:00:00 sleep
32931 pts/1 00:00:00 grep
32932 pts/1 00:00:00 sort
32933 pts/1 00:00:00 uniq
32948 pts/1 00:00:00 grep
Видно, что все процессы, кроме cat, работают сразу же. Оболочка порождает все процессы и соединяет их потоки до того, как хоть один из них завершится. cat запустится только после того, как отработает sleep, а вывод cat будет отправлен в grep, и так далее.
У каждой программы есть входной поток, называемый stdin (от standard input – стандартный ввод). При работе через конвейер stdin подключается автоматически. В скриптах многие программы принимают - в качестве имени файла, что означает «читать из stdin»:
# These are equivalent when data comes from a pipe
echo "hello" | grep "hello"
echo "hello" | grep "hello" -
Аналогично у каждой программы есть два выходных потока: stdout и stderr. Стандартный вывод – тот, который встречается чаще всего, и именно он используется, чтобы передать вывод программы по конвейеру следующей команде. Стандартный поток ошибок – альтернативный поток, предназначенный для предупреждений и прочих сообщений о проблемах: этот вывод не попадает на разбор следующей команде в цепочке.
$ ls /nonexistent
ls: cannot access '/nonexistent': No such file or directory
$ ls /nonexistent | grep "pattern"
ls: cannot access '/nonexistent': No such file or directory
# The error message still appears because stderr is not piped
$ ls /nonexistent 2>/dev/null
# No output - stderr was redirected to /dev/null
Оболочка предоставляет синтаксис для перенаправления этих потоков. Вот несколько наглядных примеров.
# Redirect stdout to a file (overwrite)
echo "hello" > output.txt
# Redirect stdout to a file (append)
echo "world" >> output.txt
# Redirect stderr to a file
ls foobar 2> errors.txt
# Redirect both stdout and stderr to the same file
ls foobar &> all_output.txt
# Redirect stdin from a file
grep "pattern" < input.txt
# Discard output by redirecting to /dev/null
cmd > /dev/null 2>&1
Ещё одна мощная программа, воплощающая философию Unix, – fzf, инструмент нечёткого поиска (fuzzy finder). Он читает строки из stdin и предоставляет интерактивный интерфейс для фильтрации и выбора:
$ ls | fzf
$ cat ~/.bash_history | fzf
fzf можно встроить во множество операций оболочки. Мы увидим и другие способы его применения, когда будем обсуждать настройку оболочки.
Переменные окружения
Чтобы присвоить значение переменной в bash, мы используем синтаксис foo=bar, а затем обращаемся к значению переменной через $foo.
Обратите внимание, что foo = bar – некорректный синтаксис: оболочка разберёт его как вызов программы foo с аргументами ['=', 'bar'].
В shell-скриптах роль пробела – разделять аргументы.
Такое поведение может сбивать с толку, и к нему непросто привыкнуть, так что имейте это в виду.
У переменных оболочки нет типов – все они строки.
Заметьте, что при записи строковых выражений в оболочке одинарные и двойные кавычки не взаимозаменяемы.
Строки в кавычках ' – это буквальные строки: в них не подставляются переменные, не выполняется подстановка команд и не обрабатываются escape-последовательности, тогда как в строках в кавычках " всё это происходит.
foo=bar
echo "$foo"
# prints bar
echo '$foo'
# prints $foo
Чтобы записать вывод команды в переменную, используется подстановка команд (command substitution). Когда мы выполняем
files=$(ls)
echo "$files" | grep README
echo "$files" | grep ".py"
вывод (а точнее, stdout) команды ls помещается в переменную $files, к которой мы можем обратиться позже.
Содержимое переменной $files включает и переводы строк из вывода ls – именно поэтому программы вроде grep понимают, что каждый элемент нужно обрабатывать по отдельности.
Менее известная похожая возможность – подстановка процессов (process substitution): <( CMD ) выполнит CMD, поместит вывод во временный файл и подставит вместо <() имя этого файла.
Это удобно, когда команды ожидают, что значения будут переданы через файл, а не через STDIN.
Например, diff <(ls src) <(ls docs) покажет различия между файлами в каталогах src и docs.
Всякий раз, когда одна программа в оболочке вызывает другую, она передаёт ей набор переменных, которые обычно называют переменными окружения.
Находясь в оболочке, мы можем посмотреть текущие переменные окружения, выполнив printenv.
Чтобы явно передать переменную окружения, можно поставить присваивание переменной перед командой
Переменные окружения по соглашению пишутся ЗАГЛАВНЫМИ_БУКВАМИ (например,
HOME,PATH,DEBUG). Это соглашение, а не техническое требование, но его соблюдение помогает отличать переменные окружения от локальных переменных оболочки, которые обычно пишутся строчными.
TZ=Asia/Tokyo date # prints the current time in Tokyo
echo $TZ # this will be empty, since TZ was only set for the child command
Кроме того, можно воспользоваться встроенной функцией export, которая изменит наше текущее окружение, и тогда все дочерние процессы унаследуют эту переменную:
export DEBUG=1
# All programs from this point onwards will have DEBUG=1 in their environment
bash -c 'echo $DEBUG'
# prints 1
Чтобы удалить переменную, используйте встроенную команду unset, например unset DEBUG.
Переменные окружения – ещё одно соглашение оболочки. С их помощью можно менять поведение многих программ неявно, а не явно. Например, оболочка записывает в переменную окружения
$HOMEпуть к домашнему каталогу текущего пользователя. Программы могут прочитать эту переменную и получить нужную информацию, вместо того чтобы требовать явного--home /home/alice. Другой распространённый пример –$TZ: многие программы используют её, чтобы форматировать даты и время в соответствии с указанным часовым поясом.
Коды возврата
Как мы уже видели, основной результат работы программы в оболочке передаётся через потоки stdout/stderr и через побочные эффекты в файловой системе.
По умолчанию shell-скрипт возвращает код возврата ноль.
По соглашению ноль означает, что всё прошло хорошо, а ненулевое значение – что возникли какие-то проблемы.
Чтобы вернуть ненулевой код возврата, нужно использовать встроенную команду оболочки exit NUM.
Код возврата последней выполненной команды можно получить через специальную переменную $?.
В оболочке есть логические операторы && и ||, выполняющие операции И и ИЛИ соответственно.
В отличие от аналогичных операторов в обычных языках программирования, в оболочке они работают с кодами возврата программ.
Оба этих оператора – короткозамкнутые (short-circuiting).
Это значит, что с их помощью можно запускать команды в зависимости от того, успешно или неуспешно завершились предыдущие, причём успех определяется тем, равен ли код возврата нулю. Несколько примеров:
# echo will only run if grep succeeds (finds a match)
grep -q "pattern" file.txt && echo "Pattern found"
# echo will only run if grep fails (no match)
grep -q "pattern" file.txt || echo "Pattern not found"
# true is a shell program that always succeeds
true && echo "This will always print"
# and false is a shell program that always fails
false || echo "This will always print"
Тот же принцип действует для конструкций if и while – обе они принимают решения на основе кодов возврата:
# if uses the return code of the condition command (0 = true, nonzero = false)
if grep -q "pattern" file.txt; then
echo "Found"
fi
# while loops continue as long as the command returns 0
while read line; do
echo "$line"
done < file.txt
Сигналы
Иногда программу нужно прервать прямо во время выполнения – например, если команда выполняется слишком долго.
Самый простой способ прервать программу – нажать Ctrl-C, и команда, скорее всего, остановится.
Но как это на самом деле работает и почему иногда процесс всё-таки не останавливается?
$ sleep 100
^C
$
Обратите внимание:
^C– такCtrl-Cотображается при вводе в терминале.
Под капотом произошло следующее:
- Мы нажали
Ctrl-C - Оболочка распознала эту специальную комбинацию символов
- Процесс оболочки отправил процессу
sleepсигнал SIGINT - Сигнал прервал выполнение процесса
sleep
Сигналы – это особый механизм взаимодействия. Получив сигнал, процесс приостанавливает выполнение, обрабатывает сигнал и, возможно, меняет ход своей работы в зависимости от того, какую информацию этот сигнал принёс. Поэтому сигналы называют программными прерываниями (software interrupts).
В нашем случае нажатие Ctrl-C заставляет оболочку доставить процессу сигнал SIGINT.
Вот минимальный пример программы на Python, которая перехватывает SIGINT и игнорирует его, из-за чего больше не останавливается. Чтобы убить такую программу, теперь можно воспользоваться сигналом SIGQUIT, нажав Ctrl-\.
#!/usr/bin/env python
import signal, time
def handler(signum, time):
print("\nI got a SIGINT, but I am not stopping")
signal.signal(signal.SIGINT, handler)
i = 0
while True:
time.sleep(.1)
print("\r{}".format(i), end="")
i += 1
Вот что происходит, если отправить этой программе SIGINT дважды, а затем SIGQUIT. Обратите внимание: ^ – так Ctrl отображается при вводе в терминале.
$ python sigint.py
24^C
I got a SIGINT, but I am not stopping
26^C
I got a SIGINT, but I am not stopping
30^\[1] 39913 quit python sigint.py
Хотя и SIGINT, и SIGQUIT обычно связаны с запросами со стороны терминала, более универсальный сигнал, которым процесс просят корректно завершиться, – это SIGTERM.
Отправить его можно командой kill с синтаксисом kill -TERM <PID>.
Сигналы умеют не только убивать процессы. Например, SIGSTOP приостанавливает процесс. В терминале нажатие Ctrl-Z заставит оболочку отправить сигнал SIGTSTP – сокращение от Terminal Stop (то есть терминальная версия SIGSTOP).
Затем приостановленное задание можно продолжить на переднем плане или в фоне с помощью fg или bg соответственно.
Команда jobs выводит список незавершённых заданий, связанных с текущей сессией терминала.
Ссылаться на эти задания можно по их pid (узнать его можно с помощью pgrep).
Более наглядный способ – ссылаться на процесс через знак процента и номер задания (его показывает jobs). Чтобы обратиться к последнему отправленному в фон заданию, можно использовать специальный параметр $!.
Ещё стоит знать, что суффикс & у команды запускает её в фоне и сразу возвращает вам приглашение командной строки, хотя команда по-прежнему будет писать в STDOUT оболочки, что может раздражать (в таком случае используйте перенаправления оболочки). Аналогично, чтобы отправить в фон уже запущенную программу, можно нажать Ctrl-Z, а затем выполнить bg.
Обратите внимание, что фоновые процессы всё равно остаются дочерними процессами вашего терминала и умрут, если вы его закроете (при этом отправляется ещё один сигнал – SIGHUP).
Чтобы этого не произошло, можно запустить программу через nohup (обёртку, игнорирующую SIGHUP) или воспользоваться disown, если процесс уже запущен.
Как вариант, можно использовать мультиплексор терминала – о нём мы поговорим в следующем разделе.
Ниже приведён пример сеанса, демонстрирующий некоторые из этих понятий.
$ sleep 1000
^Z
[1] + 18653 suspended sleep 1000
$ nohup sleep 2000 &
[2] 18745
appending output to nohup.out
$ jobs
[1] + suspended sleep 1000
[2] - running nohup sleep 2000
$ kill -SIGHUP %1
[1] + 18653 hangup sleep 1000
$ kill -SIGHUP %2 # nohup protects from SIGHUP
$ jobs
[2] + running nohup sleep 2000
$ kill %2
[2] + 18745 terminated nohup sleep 2000
Особый сигнал – SIGKILL: процесс не может его перехватить, и этот сигнал всегда немедленно завершает процесс. Однако у него могут быть неприятные побочные эффекты – например, осиротевшие дочерние процессы.
Подробнее об этих и других сигналах можно почитать здесь, а также набрав man signal или kill -l.
В shell-скриптах можно использовать встроенную команду trap, чтобы выполнять команды при получении сигналов. Это удобно для операций очистки:
#!/usr/bin/env bash
cleanup() {
echo "Cleaning up temporary files..."
rm -f /tmp/mytemp.*
}
trap cleanup EXIT # Run cleanup when script exits
trap cleanup SIGINT SIGTERM # Also on Ctrl-C or kill
Удалённые машины
Программистам всё чаще приходится работать с удалёнными серверами в повседневных задачах. Самый распространённый инструмент для этого – SSH (Secure Shell), который позволяет подключиться к удалённому серверу и получить уже знакомый нам интерфейс командной оболочки. К серверу мы подключаемся командой вроде:
ssh alice@server.mit.edu
Здесь мы пытаемся подключиться по ssh как пользователь alice к серверу server.mit.edu.
Часто упускаемая из виду возможность ssh – запуск команд в неинтерактивном режиме. ssh корректно передаёт stdin и принимает stdout команды, поэтому его можно комбинировать с другими командами
# here ls runs in the remote, and wc runs locally
ssh alice@server ls | wc -l
# here both ls and wc run in the server
ssh alice@server 'ls | wc -l'
Попробуйте установить Mosh – замену SSH, которая умеет переживать обрывы соединения, уход в сон и выход из него, смену сети и справляется с каналами с высокой задержкой.
Чтобы ssh позволил нам выполнять команды на удалённом сервере, нужно доказать, что у нас есть на это право.
Сделать это можно с помощью пароля или ssh-ключей.
Аутентификация по ключу использует криптографию с открытым ключом, чтобы доказать серверу, что клиент владеет секретным закрытым ключом, не раскрывая сам ключ.
Аутентификация по ключу одновременно и удобнее, и безопаснее, так что предпочитайте именно её.
Учтите, что закрытый ключ (обычно ~/.ssh/id_rsa, а в последнее время ~/.ssh/id_ed25519) – это фактически ваш пароль, так что относитесь к нему соответственно и никогда никому не показывайте его содержимое.
Чтобы сгенерировать пару ключей, выполните ssh-keygen.
ssh-keygen -a 100 -t ed25519 -f ~/.ssh/id_ed25519
Если вы когда-нибудь настраивали push в GitHub по SSH-ключам, то, скорее всего, уже проделали шаги, описанные здесь, и у вас есть действующая пара ключей. Проверить, есть ли у ключа парольная фраза, и убедиться, что она подходит, можно командой ssh-keygen -y -f /path/to/key.
На стороне сервера ssh заглядывает в .ssh/authorized_keys, чтобы решить, каких клиентов пускать. Скопировать туда открытый ключ можно так:
cat .ssh/id_ed25519.pub | ssh alice@remote 'cat >> ~/.ssh/authorized_keys'
# or more simply (if ssh-copy-id is available)
ssh-copy-id -i .ssh/id_ed25519 alice@remote
Помимо запуска команд, соединение, которое устанавливает ssh, можно использовать для безопасной передачи файлов на сервер и с него. scp – самый традиционный инструмент, его синтаксис: scp path/to/local_file remote_host:path/to/remote_file. rsync идёт дальше scp: он находит одинаковые файлы на локальной и удалённой машине и не копирует их повторно. Кроме того, он даёт более тонкий контроль над символическими ссылками и правами доступа, а также имеет дополнительные возможности вроде флага --partial, который позволяет продолжить прерванное ранее копирование. Синтаксис у rsync похож на scp.
Конфигурация SSH-клиента находится в ~/.ssh/config и позволяет объявлять хосты и задавать для них настройки по умолчанию. Этот файл конфигурации читает не только ssh, но и другие программы: scp, rsync, mosh и т. д.
Host vm
User alice
HostName 172.16.174.141
Port 2222
IdentityFile ~/.ssh/id_ed25519
# Configs can also take wildcards
Host *.mit.edu
User alice
Мультиплексоры терминала
При работе в интерфейсе командной строки вам часто будет нужно запускать несколько программ одновременно. Например, вам может понадобиться держать рядом редактор и свою программу. Этого можно добиться, открыв новые окна терминала, но более гибкое решение – воспользоваться мультиплексором терминала.
Мультиплексоры терминала вроде tmux позволяют мультиплексировать окна терминала с помощью панелей и вкладок, чтобы вы могли эффективно работать с несколькими сессиями оболочки.
Кроме того, мультиплексоры терминала позволяют отсоединиться от текущей сессии терминала и снова подключиться к ней позже.
Благодаря этому мультиплексоры терминала очень удобны при работе с удалёнными машинами: отпадает нужда в nohup и подобных трюках.
Самый популярный мультиплексор терминала на сегодня – tmux. tmux очень гибко настраивается, и с помощью связанных с ним сочетаний клавиш можно создавать множество вкладок и панелей и быстро перемещаться между ними.
tmux ожидает, что вы знаете его сочетания клавиш, и все они имеют вид <C-b> x, что означает: (1) нажать Ctrl+b, (2) отпустить Ctrl+b, а затем (3) нажать x. В tmux есть следующая иерархия объектов:
- Сессии – сессия – это независимое рабочее пространство с одним или несколькими окнами
tmuxзапускает новую сессию.tmux new -s NAMEзапускает её с указанным именем.tmux lsвыводит список текущих сессий- Внутри
tmuxнажатие<C-b> dотсоединяет текущую сессию tmux aподключается к последней сессии. С помощью флага-tможно указать, к какой именно
- Окна – аналог вкладок в редакторах или браузерах: визуально отдельные части одной и той же сессии
<C-b> cСоздать новое окно. Чтобы закрыть его, достаточно завершить оболочки, нажав<C-d><C-b> NПерейти к N-му окну. Обратите внимание, что они пронумерованы<C-b> pПерейти к предыдущему окну<C-b> nПерейти к следующему окну<C-b> ,Переименовать текущее окно<C-b> wПоказать список текущих окон
- Панели – как сплиты в vim, панели позволяют держать несколько оболочек на одном экране.
<C-b> "Разделить текущую панель по горизонтали<C-b> %Разделить текущую панель по вертикали<C-b> <direction>Перейти к панели в указанном направлении. Направление здесь задаётся клавишами со стрелками.<C-b> zВключить/выключить увеличение текущей панели<C-b> [Начать прокрутку истории. Затем можно нажать<space>, чтобы начать выделение, и<enter>, чтобы скопировать выделенное.<C-b> <space>Перебирать варианты расположения панелей.
Чтобы узнать больше о tmux, почитайте этот краткий туториал и это более подробное объяснение.
Когда в вашем арсенале есть tmux и SSH, вам захочется, чтобы на любой машине окружение ощущалось как родное. Здесь-то и вступает в игру настройка оболочки.
Настройка оболочки
Огромное множество программ командной строки настраивается с помощью текстовых файлов, известных как dotfiles
(потому что их имена начинаются с ., например ~/.vimrc, и поэтому по умолчанию они
скрыты в списке содержимого каталога, который выводит ls).
Dotfiles – ещё одно соглашение оболочки. Точка в начале имени нужна, чтобы «спрятать» их при выводе списка файлов (да, тоже соглашение).
Оболочки – один из примеров программ, которые настраиваются такими файлами. При запуске ваша оболочка читает множество файлов, чтобы загрузить свою конфигурацию. В зависимости от оболочки и от того, запускаете ли вы login- и/или интерактивную сессию, весь этот процесс может быть довольно сложным. Вот отличный материал на эту тему.
Для bash в большинстве систем достаточно отредактировать .bashrc или .bash_profile.
Вот ещё несколько примеров инструментов, которые можно настраивать через dotfiles:
bash–~/.bashrc,~/.bash_profilegit–~/.gitconfigvim–~/.vimrcи папка~/.vimssh–~/.ssh/configtmux–~/.tmux.conf
Одно из типичных изменений конфигурации – добавление новых мест, где оболочка будет искать программы. Этот приём вы не раз встретите при установке программ:
export PATH="$PATH:path/to/append"
Здесь мы говорим оболочке присвоить переменной $PATH её текущее значение плюс новый путь и передать это новое значение PATH по наследству всем дочерним процессам.
Благодаря этому дочерние процессы смогут находить программы, расположенные в path/to/append.
Настройка оболочки часто означает установку новых инструментов командной строки. Менеджеры пакетов сильно упрощают эту задачу. Они берут на себя скачивание, установку и обновление программ. В разных операционных системах разные менеджеры пакетов: macOS использует Homebrew, Ubuntu/Debian – apt, Fedora – dnf, а Arch – pacman. Подробнее о менеджерах пакетов мы поговорим в лекции об упаковке и доставке кода.
Вот как установить два полезных инструмента с помощью Homebrew на macOS:
# ripgrep: a faster grep with better defaults
brew install ripgrep
# fd: a faster, user-friendly find
brew install fd
После их установки можно использовать rg вместо grep и fd вместо find.
Предупреждение насчёт
curl | bash: вы часто будете видеть инструкции по установке вродеcurl -fsSL https://example.com/install.sh | bash. Этот шаблон скачивает скрипт и тут же его выполняет – удобно, но рискованно: вы запускаете код, который не проверили. Более безопасный подход – сначала скачать, посмотреть, а потом уже выполнять:curl -fsSL https://example.com/install.sh -o install.sh less install.sh # review the script bash install.shНекоторые установщики используют чуть более безопасный вариант:
/bin/bash -c "$(curl -fsSL https://url)"– он хотя бы гарантирует, что интерпретировать скрипт будет именно bash, а не ваша текущая оболочка.
Когда вы пытаетесь запустить команду, которая не установлена, оболочка выведет command not found. Сайт command-not-found.com – полезный ресурс, на котором можно найти любую команду и узнать, как установить её в разных менеджерах пакетов и дистрибутивах.
Ещё один полезный инструмент – tldr: он предоставляет упрощённые man-страницы, построенные вокруг примеров. Вместо того чтобы читать длинную документацию, можно быстро посмотреть типичные варианты использования:
$ tldr fd
An alternative to find.
Aims to be faster and easier to use than find.
Recursively find files matching a pattern in the current directory:
fd "pattern"
Find files that begin with "foo":
fd "^foo"
Find files with a specific extension:
fd --extension txt
Иногда нужна не целая новая программа, а всего лишь короткое имя для существующей команды с определёнными флагами. Тут на помощь приходят алиасы (псевдонимы).
Мы также можем создавать собственные алиасы команд с помощью встроенной команды оболочки alias.
Алиас оболочки – это сокращённая запись другой команды; перед вычислением выражения оболочка автоматически заменит его полной командой.
Например, алиас в bash имеет следующую структуру:
alias alias_name="command_to_alias arg1 arg2"
Обратите внимание, что вокруг знака равенства
=нет пробелов, потому чтоalias– это команда оболочки, принимающая один аргумент.
У алиасов много удобных возможностей:
# Make shorthands for common flags
alias ll="ls -lh"
# Save a lot of typing for common commands
alias gs="git status"
alias gc="git commit"
# Save you from mistyping
alias sl=ls
# Overwrite existing commands for better defaults
alias mv="mv -i" # -i prompts before overwrite
alias mkdir="mkdir -p" # -p make parent dirs as needed
alias df="df -h" # -h prints human readable format
# Alias can be composed
alias la="ls -A"
alias lla="la -l"
# To ignore an alias run it prepended with \
\ls
# Or disable an alias altogether with unalias
unalias la
# To get an alias definition just call it with alias
alias ll
# Will print ll='ls -lh'
У алиасов есть ограничения: они не могут принимать аргументы в середине команды. Для более сложного поведения стоит использовать вместо них функции оболочки.
Большинство оболочек поддерживают Ctrl-R для обратного поиска по истории. Нажмите Ctrl-R и начните печатать, чтобы искать среди ранее выполненных команд. Ранее мы уже познакомились с fzf – инструментом нечёткого поиска; если настроить интеграцию fzf с оболочкой, Ctrl-R превращается в интерактивный нечёткий поиск по всей вашей истории – куда более мощный, чем поиск по умолчанию.
Как организовать свои dotfiles? Они должны лежать в отдельной папке, находиться под контролем версий и расставляться по своим местам скриптом через символические ссылки. Это даёт следующие преимущества:
- Простая установка: если вы заходите на новую машину, применение ваших настроек займёт всего минуту.
- Переносимость: ваши инструменты будут везде работать одинаково.
- Синхронизация: вы можете обновлять свои dotfiles где угодно и держать их все синхронизированными.
- Отслеживание изменений: скорее всего, вы будете сопровождать свои dotfiles на протяжении всей карьеры программиста, а для долгоживущих проектов история версий – приятная вещь.
Что стоит положить в свои dotfiles? О настройках инструмента можно узнать из онлайн-документации или man-страниц. Другой отличный способ – поискать в интернете блог-посты о конкретных программах, где авторы рассказывают о своих любимых настройках. Ещё один способ узнать о возможных настройках – заглядывать в чужие dotfiles: на GitHub можно найти массу репозиториев с dotfiles – самый популярный смотрите здесь (впрочем, мы не советуем слепо копировать чужие конфигурации). Вот ещё один хороший ресурс по этой теме.
Dotfiles всех преподавателей курса открыто доступны на GitHub: Anish, Jon, Jose.
Фреймворки и плагины тоже могут улучшить вашу оболочку. Из популярных универсальных фреймворков – prezto или oh-my-zsh; есть и плагины поменьше, сосредоточенные на конкретных возможностях:
- zsh-syntax-highlighting – подсвечивает корректные и некорректные команды прямо при наборе
- zsh-autosuggestions – по мере набора предлагает команды из истории
- zsh-completions – дополнительные определения автодополнения
- zsh-history-substring-search – поиск по истории в стиле fish
- powerlevel10k – быстрая настраиваемая тема приглашения командной строки
Оболочки вроде fish включают многие из этих возможностей по умолчанию.
Чтобы получить эти возможности, не обязательно ставить громоздкий фреймворк вроде oh-my-zsh. Установка отдельных плагинов часто быстрее и даёт больше контроля. Большие фреймворки могут заметно замедлять запуск оболочки, так что подумайте о том, чтобы ставить только то, чем вы действительно пользуетесь.
ИИ в командной оболочке
Способов встроить ИИ-инструменты в командную оболочку много. Вот несколько примеров с разной глубиной интеграции:
Генерация команд: инструменты вроде simonw/llm помогают генерировать команды оболочки из описания на естественном языке:
$ llm cmd "find all python files modified in the last week"
find . -name "*.py" -mtime -7
Интеграция в конвейеры: LLM можно встраивать в конвейеры команд для обработки и преобразования данных. Они особенно полезны, когда нужно извлечь информацию из неоднородных форматов, где регулярные выражения превратились бы в мучение:
$ cat users.txt
Contact: john.doe@example.com
User 'alice_smith' logged in at 3pm
Posted by: @bob_jones on Twitter
Author: Jane Doe (jdoe)
Message from mike_wilson yesterday
Submitted by user: sarah.connor
$ INSTRUCTIONS="Extract just the username from each line, one per line, nothing else"
$ llm "$INSTRUCTIONS" < users.txt
john.doe
alice_smith
bob_jones
jdoe
mike_wilson
sarah.connor
Обратите внимание: мы пишем "$INSTRUCTIONS" (в кавычках), потому что переменная содержит пробелы, а < users.txt перенаправляет содержимое файла в stdin.
ИИ-оболочки: инструменты вроде Claude Code действуют как метаоболочка, которая принимает команды на английском языке и превращает их в операции оболочки, правки файлов и более сложные многошаговые задачи.
Эмуляторы терминала
Помимо настройки оболочки, стоит потратить немного времени на выбор эмулятора терминала и его настройку. Эмулятор терминала – это GUI-программа, предоставляющая текстовый интерфейс, в котором работает ваша оболочка. Эмуляторов терминала существует великое множество.
Поскольку в терминале вы, возможно, проведёте сотни, а то и тысячи часов, изучить его настройки – выгодное вложение времени. Вот некоторые аспекты терминала, которые может иметь смысл настроить под себя:
- Выбор шрифта
- Цветовая схема
- Сочетания клавиш
- Поддержка вкладок и панелей
- Настройка буфера прокрутки (scrollback)
- Производительность (некоторые новые терминалы, например Alacritty или Ghostty, поддерживают GPU-ускорение).
Упражнения
Аргументы и подстановка (globbing)
-
Вам могут встречаться команды вида
cmd --flag -- --notaflag.--– это специальный аргумент, который говорит программе прекратить разбор флагов. Всё, что идёт после--, считается позиционным аргументом. Чем это может быть полезно? Попробуйте выполнитьtouch -- -myfile, а затем удалить этот файл без--. - Прочитайте
man lsи составьте командуls, которая выводит список файлов следующим образом:- В список входят все файлы, включая скрытые
- Размеры выводятся в человекочитаемом формате (например, 454M вместо 454279954)
- Файлы упорядочены по времени изменения, начиная с самых свежих
- Вывод раскрашен цветом
Пример вывода может выглядеть так:
-rw-r--r-- 1 user group 1.1M Jan 14 09:53 baz drwxr-xr-x 5 user group 160 Jan 14 09:53 . -rw-r--r-- 1 user group 514 Jan 14 06:42 bar -rw-r--r-- 1 user group 106M Jan 13 12:12 foo drwx------+ 47 user group 1.5K Jan 12 18:08 .. - Подстановка процессов (process substitution)
<(command)позволяет использовать вывод команды так, как будто это файл. С помощьюdiffи подстановки процессов сравните выводprintenvиexport. Почему они различаются? (Подсказка: попробуйтеdiff <(printenv | sort) <(export | sort)).
Переменные окружения
- Напишите bash-функции
marcoиpolo, которые делают следующее: каждый раз, когда вы выполняетеmarco, текущий рабочий каталог должен каким-либо образом сохраняться, а когда вы выполняетеpolo– в каком бы каталоге вы ни находились,poloдолжна возвращать вас (cd) в тот каталог, где вы выполнилиmarco. Для удобства отладки можно записать код в файлmarco.shи (пере)загружать определения в оболочку командойsource marco.sh.
Коды возврата
-
Допустим, у вас есть команда, которая сбоит редко. Чтобы отладить её, нужно перехватить её вывод, но дожидаться неудачного запуска может быть долго. Напишите bash-скрипт, который запускает следующий скрипт до тех пор, пока тот не завершится с ошибкой, сохраняет его потоки стандартного вывода и ошибок в файлы и в конце печатает всё это. Дополнительные баллы, если вы к тому же сообщите, сколько запусков понадобилось, чтобы скрипт упал.
#!/usr/bin/env bash n=$(( RANDOM % 100 )) if [[ $n -eq 42 ]]; then echo "Something went wrong" >&2 echo "The error was using magic numbers" exit 1 fi echo "Everything went according to plan"
Сигналы и управление заданиями
-
Запустите в терминале задание
sleep 10000, отправьте его в фон с помощьюCtrl-Zи возобновите его выполнение командойbg. Теперь с помощьюpgrepнайдите его pid, а с помощьюpkillзавершите его, ни разу не набрав сам pid. (Подсказка: используйте флаги-lf). -
Допустим, вы не хотите запускать процесс, пока не завершится другой. Как бы вы это сделали? В этом упражнении ограничивающим процессом всегда будет
sleep 60 &. Один из способов добиться этого – использовать командуwait. Попробуйте запустить команду sleep и заставитьlsподождать, пока фоновый процесс не закончится.Однако эта стратегия не сработает, если запустить её из другой bash-сессии, потому что
waitработает только с дочерними процессами. Одна особенность, которую мы не обсуждали в конспекте: код возврата командыkillравен нулю при успехе и отличен от нуля в противном случае.kill -0не посылает сигнал, но вернёт ненулевой код возврата, если процесс не существует. Напишите bash-функциюpidwait, которая принимает pid и ждёт, пока указанный процесс не завершится. Используйтеsleep, чтобы попусту не тратить процессорное время.
Файлы и права доступа
- (Продвинутое) Напишите команду или скрипт, чтобы рекурсивно найти в каталоге файл, изменённый последним. А в более общем виде: сможете ли вы вывести все файлы в порядке давности изменения?
Мультиплексоры терминала
- Пройдите этот туториал по
tmux, а затем научитесь делать базовые настройки, следуя этим шагам.
Алиасы и dotfiles
-
Создайте алиас
dc, который разворачивается вcd, – на случай опечатки. -
Выполните
history | awk '{$1="";print substr($0,2)}' | sort | uniq -c | sort -n | tail -n 10, чтобы получить топ-10 самых используемых вами команд, и подумайте, не завести ли для них более короткие алиасы. Примечание: это работает в Bash; если вы пользуетесь ZSH, используйтеhistory 1вместо простоhistory. -
Создайте папку для своих dotfiles и настройте контроль версий.
-
Добавьте конфигурацию хотя бы для одной программы, например для своей оболочки, с какой-нибудь настройкой (для начала это может быть что-то совсем простое, вроде настройки приглашения командной строки через переменную
$PS1). -
Придумайте способ быстро (и без ручных действий) устанавливать свои dotfiles на новой машине. Это может быть простой shell-скрипт, вызывающий
ln -sдля каждого файла, а можно воспользоваться специализированной утилитой. -
Проверьте свой скрипт установки на свежей виртуальной машине.
-
Перенесите все текущие конфигурации своих инструментов в репозиторий с dotfiles.
-
Опубликуйте свои dotfiles на GitHub.
Удалённые машины (SSH)
Для этих упражнений установите виртуальную машину с Linux (или используйте уже существующую). Если вы не знакомы с виртуальными машинами, загляните в этот туториал по её установке.
-
Зайдите в
~/.ssh/и проверьте, есть ли у вас там пара SSH-ключей. Если нет, сгенерируйте их командойssh-keygen -a 100 -t ed25519. Рекомендуется использовать пароль иssh-agent, подробнее здесь. -
Отредактируйте
.ssh/config, добавив в него такую запись:Host vm User username_goes_here HostName ip_goes_here IdentityFile ~/.ssh/id_ed25519 LocalForward 9999 localhost:8888 -
С помощью
ssh-copy-id vmскопируйте свой ssh-ключ на сервер. -
Запустите веб-сервер в своей VM, выполнив
python -m http.server 8888. Откройте веб-сервер VM, перейдя по адресуhttp://localhost:9999на своей машине. -
Отредактируйте конфигурацию своего SSH-сервера командой
sudo vim /etc/ssh/sshd_configи отключите аутентификацию по паролю, изменив значениеPasswordAuthentication. Запретите вход под root, изменив значениеPermitRootLogin. Перезапустите службуsshкомандойsudo service sshd restart. Попробуйте снова зайти по ssh. -
(Со звёздочкой) Установите
moshв VM и подключитесь. Затем отключите сетевой адаптер сервера/VM. Сможет ли mosh корректно восстановиться после этого? -
(Со звёздочкой) Разберитесь, что делают флаги
-Nи-fуssh, и составьте команду для проброса портов в фоновом режиме.
Лицензия CC BY-NC-SA.