Тема 08

Окружение и настройка оболочки

Окружение и настройка оболочки

Предыдущие темы показали, как устроена командная строка: команда, опции, аргументы, файловая система, права, потоки ввода-вывода и конвейеры. Во всех примерах оболочка работала «как есть», с настройками по умолчанию. Однако каждая оболочка получает от своего родителя набор переменных, ищет программы в списке каталогов, читает собственные файлы настроек при запуске и хранит историю команд. Эти механизмы определяют, какая команда будет выполнена при вводе python, почему скрипт работает в терминале, но не работает из планировщика, и почему слово sort даёт разный порядок строк на разных компьютерах.

Тема рассматривает окружение оболочки как управляемую среду. Сначала различаются переменные оболочки и переменные окружения, объясняется, как окружение передаётся дочерним процессам. Затем разбирается переменная PATH и порядок поиска команд, в том числе своя программа в каталоге ~/bin. Третий раздел посвящён файлам запуска: в каком порядке и при каких условиях bash читает свои настройки, как работает команда source, зачем нужны алиасы. Четвёртый раздел описывает приглашение PS1, историю команд и локаль, которая влияет на сортировку. Пятый раздел знакомит с современными утилитами для интерактивной работы — fzf, rg, fd, bat, ncdu и tldr — и объясняет, почему им не место в скриптах. Завершает тему терминальный мультиплексор tmux: он позволяет сохранять сеанс работы при разрыве связи, что особенно важно при работе по SSH, о которой пойдёт речь в теме 11.

Переменные оболочки и переменные окружения

Переменная оболочки

Переменная оболочки (англ. shell variable) — это имя, которое хранит значение внутри текущей оболочки. Присваивание записывается без пробелов вокруг знака равенства, а значение, содержащее пробелы, берётся в кавычки:

X=1
echo "$X"
1

Имя переменной, как правило, пишется латинскими буквами, цифрами и знаком подчёркивания, а сама переменная видна только в том процессе, где создана. Эта область видимости — ключевое отличие от переменной окружения, и её удобно проверить небольшим опытом. Создадим переменную в текущей оболочке и запустим новую копию bash через -c, чтобы она вывела значение:

X=1
bash -c 'echo "X=[$X]"'
X=[]

Новая копия bash не получила X. Квадратные скобки в выводе добавлены здесь, чтобы показать пустую строку; они присутствуют в команде только внутри кавычек.

Экспорт и наследование окружения

Чтобы переменная стала доступна программам, которые запускает оболочка, её нужно экспортировать командой export. Экспортированная переменная попадает в окружение (англ. environment) — набор пар «имя — значение», который операционная система передаёт новому процессу при его создании 1:

X=1
export X
bash -c 'echo "X=[$X]"'
X=[1]

Теперь дочерний процесс получил копию значения. Экспорт можно совместить с присваиванием: export X=1 делает то же самое в одну строку. Переменная, экспортированная один раз, остаётся экспортированной в текущей оболочке до её завершения или до команды unset.

Существует и третий способ передать значение одной команде — записать присваивание перед самой командой. Такое присваивание действует только на время её выполнения и не меняет переменную в текущей оболочке:

X=2 bash -c 'echo "X=[$X]"'
echo "после: X=[$X]"
X=[2]
после: X=[1]

Этот приём часто используется для одноразовых настроек, например LC_ALL=C sort file.txt, о котором пойдёт речь в разделе о локали.

Дочерний процесс не меняет окружение родителя

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

X=1
export X
bash -c 'export X=99; echo "внутри: $X"'
echo "в родителе: $X"
внутри: 99
в родителе: 1

Причина в модели процессов. Когда оболочка запускает программу, операционная система создаёт новый процесс и копирует в него окружение; у родителя есть своя копия, и ни одна из сторон не видит памяти другой. Из этого следует важное правило: программа, запущенная из оболочки, не может изменить её переменные. Поэтому, например, обычная программа не может сменить текущий каталог оболочки. Команда cd — встроенная (англ. builtin), то есть выполняется самой оболочкой, а не отдельным процессом, поэтому она и меняет каталог. Тот же принцип объясняет, почему скрипт, который выполнен как bash script.sh, не может задать переменную в текущей оболочке: чтобы это сделать, его нужно выполнить через source, о котором речь пойдёт ниже.

Отдельная проверка показывает, что окружение действительно собирается из экспортированных переменных и ничего более. Запуск с пустым окружением env -i оставляет только несколько переменных, которые оболочка создаёт сама:

env -i bash --norc -c 'env'
PWD=/home/student
SHLVL=0
_=/usr/bin/env

Здесь HOME, PATH, USER и LANG отсутствуют: их некому было передать. Значение PWD здесь заменено на домашний каталог пользователя, остальные строки выведены без изменений.

Просмотр переменных: env, printenv, export, declare

Для просмотра переменных используются несколько команд, которые отличаются по охвату. Команда env и команда printenv показывают окружение, то есть только экспортированные переменные. Команда export -p выводит экспортированные переменные в виде, пригодном для повторного ввода. Команда declare -p показывает переменную со всеми её атрибутами, включая то, экспортирована ли она. Сравним вывод для переменной X, экспортированной ранее:

X=1
export X
printenv X
declare -p X
1
declare -x X="1"

Флаг -x в выводе declare -p означает экспорт. Для переменной без экспорта флаг отсутствует, поэтому по выводу declare -p удобно проверять, попала ли переменная в окружение. Полный список переменных текущей оболочки выдаёт команда set, но её вывод содержит и функции, поэтому на практике удобнее сочетать её с grep.

Таблица сводит команды просмотра.

КомандаЧто показывает
envэкспортированные переменные; env NAME=значение команда запускает команду с временным значением
printenv [имя]значение одной переменной окружения или всё окружение
export -pэкспортированные переменные в формате declare -x
declare -p имяпеременную с атрибутами, в том числе -x для экспортированной
setвсе переменные оболочки и функции

Все перечисленные команды, кроме set, описаны в справочных страницах 7. Выходной формат printenv и env совпадает: отличие лишь в том, что printenv без аргумента печатает окружение, а с аргументом — значение одной переменной.

Поиск команд и переменная PATH

Порядок поиска команды

Когда пользователь вводит имя команды, оболочка должна решить, что именно запустить. Ответ зависит от порядка поиска: сначала bash раскрывает алиасы (если они раскрываются в данном контексте), затем распознаёт ключевые слова оболочки (например, if), после них ищет функции, встроенные команды и, наконец, программы в каталогах из переменной PATH. Этот порядок совпадает с порядком строк в выводе type -a. Первый найденный вариант побеждает. Этот порядок позволяет переопределить внешнюю программу алиасом или функцией, но на практике он редко становится источником проблем. Гораздо чаще встречается с другим: программа есть, но оболочка её не находит.

Переменная PATH содержит список каталогов, разделённых двоеточием. Оболочка просматривает их слева направо и запускает первый файл с нужным именем, для которого у пользователя есть право выполнения. Посмотрим на список каталогов, разбив его построчно:

echo "$PATH" | tr ':' '\n' | head -n 4
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin

Это первые четыре каталога PATH свежей установки Ubuntu, заданного в файле /etc/environment; дальше следуют /sbin, /bin, /usr/games, /usr/local/games и /snap/bin. Установщики программ и файлы запуска часто добавляют в начало списка свои каталоги, например ~/.local/bin.

Порядок важен: если две программы с одинаковым именем лежат в разных каталогах, побеждает программа из каталога, который встречается раньше. Именно поэтому системные программы, установленные администратором в /usr/local/bin, находятся раньше /usr/bin. Каталоги /usr/local описаны стандартом иерархии файловой системы 5 как место для программ, установленных локально, вне пакетного менеджера.

Пусть команда ls находится в каталоге /usr/bin. Выясним, какая программа будет запущена:

type ls
command -v ls
ls is /usr/bin/ls
/usr/bin/ls

Команда type сообщает, как будет интерпретировано имя: как встроенная команда, алиас, функция или внешний файл. Команда command -v печатает только путь или имя, что удобно в скриптах. Для встроенных команд результат иной:

type -a echo
echo is a shell builtin
echo is /usr/bin/echo
echo is /bin/echo

Здесь показаны все три варианта echo: встроенный в оболочку, и два внешних файла. Встроенный echo будет выполнен, потому что он найден раньше внешних программ. Внешние версии нужны, когда вызывается /usr/bin/echo напрямую или когда запускается другая оболочка.

Каталог ~/bin и собственная команда

Пользователь может создать собственную программу и вызывать её по имени, если её каталог есть в PATH. Традиционное место для личных программ — каталог ~/bin. Дистрибутивы Ubuntu добавляют его в PATH в файле ~/.profile автоматически, но только если каталог существует на момент входа в систему. Проверим на примере.

Создадим каталог для эксперимента и простой скрипт, который печатает строку:

mkdir -p /tmp/demo-bin
printf '#!/bin/bash\necho "hello from mycmd"\n' > /tmp/demo-bin/mycmd
chmod +x /tmp/demo-bin/mycmd
mycmd
echo "код возврата: $?"
bash: mycmd: command not found
код возврата: 127

Каталог /tmp/demo-bin не входит в PATH, поэтому оболочка не находит программу и возвращает код 127, который стандартно означает «команда не найдена». Добавим каталог в PATH на время одной команды:

PATH=/tmp/demo-bin:$PATH mycmd
echo "код возврата: $?"
hello from mycmd
код возврата: 0

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

export PATH="$HOME/bin:$PATH"

Заметим порядок: новый каталог добавлен в начало. Так собственная программа с тем же именем, что и системная, будет иметь приоритет. Если поставить каталог в конец, системная программа найдётся первой. Оба варианта встречаются на практике, и выбор зависит от того, нужно ли переопределять системные команды. Главная ошибка здесь — писать PATH=$HOME/bin:PATH без знака доллара. Такая строка присваивает переменной буквальную строку /home/student/bin:PATH, после чего ни одна команда, которой нет в этом каталоге, не будет найдена. Эта ошибка включена в лабораторную работу.

Команды type, command и hash

Оболочка запоминает найденные пути в специальной таблице, чтобы не просматривать PATH при каждом вызове. Команда hash показывает эту таблицу, а команда hash -r её очищает. После того как программа установлена или перенесена, таблица может указывать на старый путь. Проверим:

hash -r
ls > /dev/null
hash
hits	command
   1	/usr/bin/ls

Столбец hits показывает число обращений к записи. Если программа переустановлена в другой каталог, достаточно выполнить hash -r или открыть новую оболочку. Команда type показывает, какой файл будет вызван при обычном поиске; при сомнениях в том, что оболочка вызывает именно эту программу, полезно сравнить вывод type с выводом hash.

Файлы запуска оболочки

Login и non-login, interactive и non-interactive

Поведение bash при запуске определяется двумя независимыми признаками. Первый — login или non-login. Login-оболочка (англ. login shell) — это та, с которой начинается сеанс пользователя: вход на консоль, по SSH, в WSL-дистрибутив при первом запуске. Вторая оболочка, запущенная из первой командой bash, — non-login. Признак login проверяется переменной $0 (имя начинается с дефиса) или опцией --login. Второй признак — interactive или non-interactive. Интерактивная оболочка получает команды с терминала, а неинтерактивная выполняет скрипт или команду из аргументов -c. Документация bash определяет оба признака в разделе INVOCATION 2.

Различие важно, потому что каждый случай читает свой набор файлов. Пользователь, который открыл терминал в графическом окружении, получает обычно non-login interactive оболочку: это окно эмулятора терминала, которое запускает bash без входа в систему. Пользователь, который подключился по SSH, получает login interactive оболочку. Скрипт, запущенный из cron, получает non-login non-interactive оболочку. Все три случая различаются набором файлов, и именно поэтому настройка, сделанная в одном месте, может не работать в другом.

Порядок чтения файлов

Правила чтения документированы в справочном руководстве bash 2. Для login-оболочки bash сначала читает системный файл /etc/profile, затем ищет в домашнем каталоге ~/.bash_profile, ~/.bash_login и ~/.profile и выполняет первый существующий и читаемый файл из этого списка. Остальные не читаются. При выходе из интерактивной login-оболочки выполняется ~/.bash_logout. Для interactive non-login оболочки bash читает /etc/bash.bashrc и ~/.bashrc. Схема порядка показана на рисунке.

Порядок чтения файлов запуска bash: login-оболочка читает /etc/profile, затем первый из файлов ~/.bash_profile, ~/.bash_login, ~/.profile; interactive non-login читает ~/.bashrc
Порядок чтения файлов запуска bash для четырёх вариантов запуска

Сравнение порядка чтения удобно провести опытом. Создадим домашний каталог во временном месте и в каждый файл запуска поместим команду echo, которая печатает имя файла:

T=$(mktemp -d)
for f in .profile .bashrc .bash_profile .bash_login .bash_logout; do
    echo "echo 'читается $f'" > "$T/$f"
done
touch "$T/.sudo_as_admin_successful"
HOME=$T bash -l -c 'true'
читается .bash_profile

Login-оболочка прочитала .bash_profile и не трогала .profile и .bash_login, поскольку первый файл из списка уже существует. Проверим остальные варианты, сохранив тот же домашний каталог:

HOME=$T bash -i </dev/null 2>/dev/null
echo "== login interactive"
HOME=$T bash -li </dev/null 2>/dev/null
читается .bashrc
== login interactive
читается .bash_profile
читается .bash_logout

Первая строка относится к interactive non-login оболочке: она прочитала .bashrc. Во второй части login-оболочка прочитала .bash_profile и при выходе выполнила .bash_logout. Файл .bashrc в этом случае не был прочитан, потому что .bash_profile его не подключает. Вопрос, почему обычно .bashrc всё-таки работает и в SSH-сеансе, решается файлом ~/.profile в Ubuntu. Посмотрим его содержимое в домашней директории стандартного пользователя:

grep -n 'bashrc' ~/.profile
13:    # include .bashrc if it exists
14:    if [ -f "$HOME/.bashrc" ]; then
15:	. "$HOME/.bashrc"

В Ubuntu ~/.profile подключает ~/.bashrc, поэтому при отсутствии ~/.bash_profile login-оболочка тоже получает настройки из .bashrc. Если создать собственный ~/.bash_profile, этот механизм перестанет работать автоматически: .profile больше не будет прочитан. Эта особенность — частая причина, по которой алиасы «пропадают» при входе по SSH.

Заметим также, что системный файл /etc/bash.bashrc читается каждой интерактивной оболочкой до ~/.bashrc. В Ubuntu в нём, например, печатается подсказка о команде sudo для пользователей группы sudo, пока не создан файл ~/.sudo_as_admin_successful. Поэтому в опыте выше этот файл создан заранее, чтобы подсказка не мешала выводу.

Команда source и повторная загрузка настроек

Файл запуска — обычный скрипт, но выполняется он особым образом. Обычный запуск bash file создаёт новый процесс, и переменные, которые в нём задаются, остаются там. Команда source file (в POSIX-записи — точка: . file) выполняет команды этого файла в текущей оболочке 4. Сравним два способа:

printf 'SRC_VAR=from_file\n' > /tmp/demo_src.sh
bash -c 'bash /tmp/demo_src.sh; echo "после bash: [${SRC_VAR-}]"'
bash -c 'source /tmp/demo_src.sh; echo "после source: [${SRC_VAR-}]"'
после bash: []
после source: [from_file]

Команда source используется после редактирования ~/.bashrc: вместо закрытия терминала достаточно выполнить source ~/.bashrc. Важно помнить, что повторная загрузка добавляет команды к уже существующему состоянию. Строка PATH="$HOME/bin:$PATH", выполненная дважды, даст каталог ~/bin в переменной PATH два раза. Для одноразовых проверок это безвредно, но в длинных файлах стоит следить за такими повторами.

Алиасы и функции в ~/.bashrc

Алиас (англ. alias) — короткое имя для команды с заранее заданными аргументами. Он объявляется командой alias и раскрывается оболочкой до разбора команды:

alias ll='ls -l'
alias
alias ll='ls -l'

Алиасы не раскрываются в неинтерактивной оболочке по умолчанию. Скрипт, в котором есть алиас, сообщит об ошибке, если алиас не включён явно:

printf 'alias ll="ls -l"\nll /tmp/words.txt\n' > /tmp/alias_demo.sh
bash /tmp/alias_demo.sh
/tmp/alias_demo.sh: line 2: ll: command not found

Для скриптов правильнее использовать функции, а алиасы оставить для интерактивной работы. Функция (англ. function) — именованный блок команд, который может принимать аргументы. Подробный синтаксис функций рассматривается в теме 14. Здесь достаточно одной строки, которая часто встречается в ~/.bashrc:

mkcd() { mkdir -p "$1" && cd "$1"; }
mkcd /tmp/demo-project
pwd
/tmp/demo-project

Здесь mkcd создаёт каталог и переходит в него. Функция работает, потому что переход cd выполняется внутри текущей оболочки; внешняя программа не смогла бы это сделать. Такие короткие функции в ~/.bashrc заменяют алиасы, которым нужны аргументы.

Приглашение, история и локаль

Приглашение PS1

Приглашение (англ. prompt) — строка, которую bash печатает перед вводом команды. Его текст хранится в переменной PS1 (primary prompt). В строке могут быть escape-последовательности, которые bash заменяет на значения: имя пользователя, каталог, время, код возврата. Полный перечень приведён в руководстве bash 2. Основные из них собраны в таблице.

ПоследовательностьЗначение
\uимя пользователя
\hимя хоста до первой точки
\wтекущий каталог, ~ вместо домашнего
\Wпоследний компонент текущего каталога
\t, \Aвремя в формате ЧЧ:ММ:СС или ЧЧ:ММ
\dдата
\$# для root, $ для остальных
\jчисло фоновых заданий
\!номер команды в истории
\eсимвол ESC, начало цветовой последовательности
\[ и \]обрамление непечатаемых последовательностей

Код возврата последней команды в таблицу не вошёл: bash не имеет отдельной escape-последовательности для него. Вместо неё используется $?. Строка PS1 подвергается раскрытию параметров, поэтому $? в ней работает. Для проверки можно раскрыть строку вручную с помощью оператора @P, который появился в bash версии 4.4:

p='[$?] \u@\h:\W\$ '
false
echo "${p@P}"
true
echo "${p@P}"
[1] student@ubuntu:tmp$ 
[0] student@ubuntu:tmp$ 

Ровно так работает приглашение, которое настраивается в лабораторной работе: цифра в начале показывает код возврата предыдущей команды.

Цветовые последовательности (например, \e[1;32m) не занимают видимых позиций на экране, но bash считает их символами, если они не обрамлены \[ и \]. Ошибка выглядит так: длинная команда при редактировании переносится неправильно, а текст затирает начало строки. Поэтому каждая управляющая последовательность цвета должна стоять внутри \[ \].

История команд

История (англ. history) хранит команды, введённые в интерактивной оболочке, и позволяет повторять их. Команда history показывает нумерованный список, !! повторяет последнюю команду, а Ctrl+R ищет в истории по подстроке. Размер истории задаётся переменной HISTSIZE: это число команд, которое помнится в текущем сеансе, по умолчанию bash устанавливает значение 500 после чтения файлов запуска 2. Размер файла истории задаётся переменной HISTFILESIZE, а сам файл по умолчанию — ~/.bash_history.

Переменная HISTCONTROL определяет, какие команды не попадают в историю. Её значения, перечисленные в руководстве bash, таковы: ignorespace — не записывать команды, начинающиеся с пробела; ignoredups — не записывать команду, совпадающую с предыдущей; ignoreboth — оба правила сразу; erasedups — удалять из истории все прежние совпадения. Значение, не входящее в этот список, игнорируется без предупреждения. Проверим на опыте. Опыт проводится с опцией --norc, чтобы файл ~/.bashrc не переопределил настройки:

printf 'echo one\necho one\n echo secret\necho two\nhistory\nexit\n' | HISTFILE=/tmp/h.txt HISTCONTROL=ignoreboth bash --norc -i 2>/dev/null | tail -4
cat /tmp/h.txt
two
    1  echo one
    2  echo two
    3  history
echo one
echo two
history
exit

Повтор echo one удалён правилом ignoredups, а команда с ведущим пробелом — правилом ignorespace. Строка echo secret не попала ни в историю, ни в файл. Команда echo two выполнена и записана, потому что отличается от предыдущей.

Поведение без HISTCONTROL иное:

printf 'echo one\necho one\n echo secret\nhistory\nexit\n' | HISTFILE=/tmp/h2.txt bash --norc -i 2>/dev/null | tail -4
cat /tmp/h2.txt
    1  echo one
    2  echo one
    3   echo secret
    4  history
echo one
echo one
 echo secret
history
exit

Здесь все команды сохранены, включая команду с пробелом в начале. Поэтому пароли и другие секреты лучше вводить с ведущим пробелом, если установлен ignorespace, либо не вводить в командной строке вовсе.

Переменная HISTSIZE ограничивает число записей. Пример с HISTSIZE=3:

printf 'echo a\necho b\necho c\necho d\nhistory\nexit\n' | HISTFILE=/tmp/h3.txt HISTSIZE=3 HISTFILESIZE=3 bash --norc -i 2>/dev/null | tail -3
cat /tmp/h3.txt
    3  echo c
    4  echo d
    5  history
echo d
history
exit

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

Обычно эти переменные задаются в ~/.bashrc. Ubuntu по умолчанию задаёт HISTCONTROL=ignoreboth, HISTSIZE=1000 и HISTFILESIZE=2000. Значения, которые видит текущая интерактивная оболочка, можно проверить командой bash -ic:

bash -ic 'echo "HISTSIZE=$HISTSIZE HISTFILESIZE=$HISTFILESIZE HISTCONTROL=$HISTCONTROL"'
HISTSIZE=1000 HISTFILESIZE=2000 HISTCONTROL=ignoreboth

Этот вывод получен в интерактивной оболочке, которая прочитала ~/.bashrc. Без --norc значения, которые задаёт ~/.bashrc, перекрывают заданные в командной строке (в Ubuntu он задаёт все три переменные), поэтому при экспериментах с историей полезно помнить о --norc.

Локаль и сортировка

Локаль (англ. locale) — набор правил, который определяет язык сообщений, кодировку символов, формат дат и порядок сортировки. Текущая локаль задаётся переменными LANG и LC_*. Переменная LC_ALL, если задана, переопределяет все остальные. Посмотрим, какие локали доступны в системе:

echo "LANG=$LANG LC_ALL=[$LC_ALL]"
locale -a
LANG=C.UTF-8 LC_ALL=[]
C
C.utf8
POSIX

В этой среде установлены только три локали. Список на другом компьютере может быть иным: на Ubuntu с русским интерфейсом обычно есть ru_RU.utf8. Наличие локали проверяется по выводу locale -a.

Локаль влияет на порядок сортировки, и это наиболее заметное следствие для работы с текстом. Проверим на файле из восьми слов с кириллицей и латиницей:

LC_ALL=C sort /tmp/words.txt
Apple
zebra
Банан
Яблоко
абрикос
апельсин
жук
ёж

В локали C сортировка идёт по числовым значениям байтов: все заглавные латинские буквы идут раньше строчных, а кириллица — после латиницы, и буква ё оказывается последней. Порядок C.UTF-8 для этих слов совпадает с C: эта локаль сортирует по кодам символов. Порядок ru_RU иной, поскольку локаль учитывает правила алфавита. Её можно собрать в любом каталоге пользователя из исходных данных пакета locales, без прав root:

mkdir -p /tmp/locale-demo
localedef -i ru_RU -f UTF-8 /tmp/locale-demo/ru_RU.UTF-8
LOCPATH=/tmp/locale-demo LC_ALL=ru_RU.UTF-8 sort /tmp/words.txt
Apple
zebra
абрикос
апельсин
Банан
ёж
жук
Яблоко

Русские слова теперь упорядочены по алфавиту и не зависят от регистра. Латинские слова в этой локали оказались первыми; правила сравнения, заданные локалью, описаны в руководстве по sort 3.

Если локаль не установлена, bash и sort не прекращают работу, а используют C. Проверим:

LC_ALL=ru_RU.UTF-8 locale 2>&1 | head -n 2
locale: Cannot set LC_CTYPE to default locale: No such file or directory
locale: Cannot set LC_MESSAGES to default locale: No such file or directory

Опасность в том, что скрипт, который сортирует данные, будет давать разный порядок на компьютерах с разными локалями. Поэтому для данных, которые должны совпадать, в скриптах принято явно задавать LC_ALL=C.

Современные утилиты для интерактивной работы

Зачем нужны современные утилиты и где им место

Классические инструменты grep, find, cat, du и man, о которых шла речь в предыдущих темах, присутствуют в любой системе, а их поведение описано в руководствах 3 7. Рядом с ними за последние годы появились программы, которые решают те же задачи удобнее для человека, сидящего за терминалом: быстрее ищут, по умолчанию пропускают ненужное, подсвечивают результат и умеют диалог. Рассмотрим шесть таких утилит обзорно: цель раздела — понимать, что они делают и как их подключить, а не изучать каждую полностью. Все они ставятся из репозитория Ubuntu одной командой (команда требует прав root, поэтому приведена как пример):

sudo apt install fzf ripgrep fd-find bat ncdu tldr

В лабораторной работе допускается установка без root: пакет скачивается командой apt-get download и распаковывается в личный каталог, как описано в задании.

Сразу сформулируем правило, ради которого этот раздел и помещён в тему об окружении. Эти программы предназначены для интерактивной работы и настраиваются в ~/.bashrc. В скриптах, в заданиях cron, в инструкциях для других людей и на чужих серверах опираются на grep, find, cat и du: они гарантированно есть, их поведение не меняется от версии к версии и не зависит от настроек пользователя. Скрипт, который вызывает rg или fd, перестанет работать на машине, где их не установили.

Нечёткий поиск: fzf

Программа fzf (англ. fuzzy finder, «нечёткий поисковик») читает список строк со стандартного ввода, показывает его в интерактивном окне и по мере набора сужает до строк, содержащих введённые символы в заданном порядке, но не обязательно подряд. Стрелки выбирают строку, Enter печатает её на стандартный вывод. Таким образом fzf — ещё один фильтр, пригодный для конвейера: ls | fzf, history | fzf. Ключ --filter (или -f) отключает окно и печатает совпадения сразу, упорядочив их по близости совпадения. Это позволяет показать принцип без интерактивного окна. Пусть в файле hist.txt записано несколько команд:

fzf --filter 'tar gz' < hist.txt
echo ---
fzf --filter 'rsy' < hist.txt
tar xzf backup.tar.gz
tar czf backup.tar.gz ~/docs
---
rsync -av src/ dst/

Запрос из двух слов (tar и gz) означает, что строка должна содержать оба; запрос rsy нашёл rsync, хотя это лишь начало слова. Порядок строк определяется оценкой совпадения: сначала те, где искомые символы стоят ближе друг к другу.

Главная польза fzf проявляется после подключения к оболочке: он заменяет встроенные сочетания клавиш. В Ubuntu пакет устанавливает готовый файл привязок, и подключается он одной строкой в ~/.bashrc, как сообщает документация пакета в /usr/share/doc/fzf/README.Debian. Условие -f защищает от ошибки, если пакета нет на этом компьютере:

if [ -f /usr/share/doc/fzf/examples/key-bindings.bash ]; then
    source /usr/share/doc/fzf/examples/key-bindings.bash
fi

После перезапуска оболочки (или source ~/.bashrc) сочетание Ctrl+R открывает не построчный поиск bash, а окно fzf со всей историей: вводится любой фрагмент команды, а выбранная строка попадает в командную строку для правки. Ctrl+T вставляет в командную строку путь к файлу, выбранный из дерева каталогов, Alt+C переходит в выбранный каталог. Проверить, что привязки установлены, можно командой bind -X, которая перечисляет сочетания клавиш, связанные с функциями оболочки:

source /usr/share/doc/fzf/examples/key-bindings.bash
bind -X
"\C-r": "__fzf_history__"
"\C-t": "fzf-file-widget"

Без подключения файла bind -X в этой оболочке ничего не выводит: Ctrl+R остаётся стандартным обратным поиском readline, о котором говорилось в разделе об истории. Привязки работают только в интерактивной оболочке, поэтому поместить их нужно в ~/.bashrc, а не в ~/.profile. В версиях fzf начиная с 0.48 подключение выполняется командой eval "$(fzf --bash)"; точную строку для установленной версии даёт man fzf. Окно выбора в тексте не воспроизводится, так как это интерактивный экран; его вид зависит от версии, а набор клавиш остаётся прежним.

Быстрый поиск по содержимому и по именам: rg и fd

Программа ripgrep вызывается командой rg. Она ищет регулярное выражение в файлах так же, как grep -r, но по умолчанию обходит каталог рекурсивно, учитывает файл .gitignore, пропускает скрытые файлы и каталоги и двоичные файлы, а на больших деревьях работает заметно быстрее. Разницу показывает небольшой проект с каталогом build, файлом журнала run.log (оба перечислены в .gitignore) и скрытым файлом .env; во всех файлах записана строка TODO:

grep -r 'TODO' . --exclude-dir=.git | sort
echo ---
rg 'TODO' . | sort
./.env:TODO secret
./build/out.txt:TODO generated
./docs/notes.md:TODO: write tests
./run.log:TODO in log
./src/main.py:TODO: fix parser
---
./docs/notes.md:TODO: write tests
./src/main.py:TODO: fix parser

Для человека, который ищет по исходному коду, второй результат удобнее: нет сгенерированных и служебных файлов. Но для задачи «найти везде, включая секреты и журналы» он неверен, и об этом нужно помнить: rg --hidden включает скрытые файлы, а rg --no-ignore отключает .gitignore. Имена опций близки к grep: -i игнорирует регистр, -n показывает номера строк, -c считает строки, -l выводит только имена файлов, -t py ограничивает поиск типом файлов. Есть и ловушка: команда rg шаблон без пути, когда стандартный ввод перенаправлен (в скрипте, фоновом задании или конвейере), читает не текущий каталог, а стандартный ввод и может «зависнуть» в ожидании данных. В скриптах путь указывают явно: rg шаблон ..

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

fdfind .
echo ---
fdfind -H -I . -E .git
./docs
./docs/notes.md
./src
./src/main.py
---
./.env
./.gitignore
./build
./build/out.txt
./docs
./docs/notes.md
./run.log
./src
./src/main.py

Здесь -H включает скрытые файлы, -I отключает учёт .gitignore, -E .git исключает каталог .git. Полезные ключи: -e json — только файлы с расширением, -t f — только обычные файлы, -t d — только каталоги, -x команда — выполнить команду для каждого найденного файла.

Обратите внимание, что в примере вызвана команда fdfind, а не fd. Причина в конфликте имён. Имя fd в Debian и Ubuntu уже занято другим пакетом, поэтому пакет fd-find устанавливает программу как fdfind, а документация пакета (/usr/share/doc/fd-find/README.Debian) сообщает, что /usr/bin/fd принадлежит программе fdclone. Аналогичная история у bat: в Ubuntu она называется batcat. Привычное короткое имя возвращают алиасом или ссылкой. Ссылка в ~/bin удобнее алиаса, так как работает и в скриптах, и в других оболочках; каталог ~/bin и его место в PATH разобраны выше в этой теме:

mkdir -p ~/bin
ln -s "$(command -v fdfind)" ~/bin/fd
ln -s "$(command -v batcat)" ~/bin/bat
type fd
fd --version
fd is /home/student/bin/fd
fd 8.3.1

(Путь /home/student/bin/fd — пример вывода; в учебной проверке ссылка создавалась в другом каталоге.) Алиасный вариант записывается в ~/.bashrc строкой alias fd=fdfind; он действует только в интерактивной оболочке, и скрипт, написанный на таком имени, не заработает. Заметим, что до создания ссылки команда type fd сообщает fd: not found, потому что в каталогах из PATH программы с таким именем нет.

Просмотр, диск и подсказки: bat, ncdu, tldr

Программа bat (в Ubuntu — batcat) похожа на cat: выводит содержимое файла, но добавляет заголовок с именем файла, нумерацию строк, подсветку синтаксиса и при необходимости открывает длинный файл в постраничном просмотрщике. Когда её вывод направлен не на терминал, а в канал или файл, оформление отключается, и она ведёт себя как cat, поэтому конвейеры не ломаются. Опцией --decorations=always оформление можно включить принудительно:

batcat --decorations=always --color=never --style=header,grid,numbers --paging=never src/main.py docs/notes.md | cat
─────┬──────────────────────────────────────────────────────────────────────────
     │ File: src/main.py
─────┼──────────────────────────────────────────────────────────────────────────
   1 │ TODO: fix parser
─────┴──────────────────────────────────────────────────────────────────────────
─────┬──────────────────────────────────────────────────────────────────────────
     │ File: docs/notes.md
─────┼──────────────────────────────────────────────────────────────────────────
   1 │ TODO: write tests
─────┴──────────────────────────────────────────────────────────────────────────

Программа ncdu (англ. NCurses Disk Usage) отвечает на вопрос «куда делось место на диске». Команда du -sh показывает одно число для каталога; ncdu каталог строит интерактивный список подкаталогов, упорядоченный по размеру, по которому можно перемещаться стрелками, входить в каталоги и удалять файлы клавишей d. Опция -x не выходит за пределы одной файловой системы, чтобы не сканировать смонтированные диски. Интерактивный экран в тексте не воспроизводится; убедиться в работе можно и в неинтерактивном режиме: ncdu -o файл сохраняет результат сканирования в файл в формате JSON, то есть в том же формате, который разобран в теме 6 вместе с jq:

ncdu -o - data | head -n 3 | cut -c1-110
du -sh data
[1,1,{"progname":"ncdu","progver":"1.15.1","timestamp":1792342598},
[{"name":"/tmp/t0616/l6/data","asize":4096,"dsize":4096,"dev":2096,"ino":678366},
{"name":"access.jsonl","asize":5311316,"dsize":5312512,"ino":678583},
8.8M	data

(Значения timestamp, dev и ino и абсолютный путь зависят от машины.) Размеры в этой записи даны в байтах: asize — размер содержимого, dsize — место, занятое на диске блоками.

Наконец, tldr отвечает на вопрос «как вызвать команду в типовом случае». Справка man полна, но для редкой команды вроде tar приходится читать страницы, чтобы найти один пример. Проект tldr собирает короткие страницы, написанные сообществом, из нескольких типовых примеров. В Ubuntu 22.04 и 24.04 пакет tldr устанавливает клиент под именем tldr-hs (в других дистрибутивах команда может называться tldr; точное имя показывает dpkg -L tldr). При первом запуске клиент скачивает базу страниц (tldr-hs --update, потребуется доступ в сеть), после чего работает без сети:

tldr-hs tar | head -n 8
tar
Archiving utility.Often combined with a compression method, such as gzip or bzip2.More information: https://www.gnu.org/software/tar/manual/tar.html.

 - [c]reate an archive and write it to a [f]ile:
   tar cf {{path/to/target.tar}} {{path/to/file1 path/to/file2 ...}}

 - [c]reate a g[z]ipped archive and write it to a [f]ile:
   tar czf {{path/to/target.tar.gz}} {{path/to/file1 path/to/file2 ...}}

(В выводе убраны управляющие последовательности цвета; клиент tldr-hs склеил две первые строки описания, так что это его особенность, а не ошибка.) Фрагменты в двойных фигурных скобках — подстановочные места, которые нужно заменить своими значениями. tldr не заменяет man: он не даёт полного перечня опций и не объясняет, почему команда устроена так, как устроена. Но как быстрая «напоминалка» он удобен, а полную справку по ссылке в начале страницы или в man всегда можно открыть следом.

Подведём итог по всем шести утилитам. Они ускоряют интерактивную работу и подключаются к оболочке строками в ~/.bashrc и ссылками в ~/bin, то есть теми же механизмами, что разобраны в этой теме. Они не входят в стандартные наборы, поведение их по умолчанию (игнорирование файлов, оформление) отличается от классического, а имена в Ubuntu не совпадают с привычными. Поэтому всё, что должно работать у других людей и без личной настройки, пишется на классических grep, find, cat, du и man.

Мультиплексор tmux

Сессии, окна и панели

Терминальный мультиплексор (англ. terminal multiplexer) позволяет запустить в одном терминале несколько сеансов оболочки и отсоединиться от них, не завершая процессы. Один из наиболее распространённых — tmux. Его модель состоит из трёх уровней: сессия (англ. session) содержит окна (англ. window), окно делится на панели (англ. pane). Каждая панель — отдельная оболочка. Иерархия показана на рисунке.

Иерархия tmux: сессия содержит окна, окно содержит панели; каждая панель запускает свою оболочку, отсоединение от сессии не останавливает процессы
Структура tmux: сессия, окна и панели

Создадим сессию в фоновом режиме, добавим окно и разделим его на две панели:

tmux new-session -d -s demo -x 80 -y 24
tmux new-window -t demo -n second
tmux split-window -t demo:1 -h
tmux ls
tmux list-windows -t demo
demo: 2 windows (created Mon Oct 12 13:16:28 2026)
0: bash- (1 panes) [80x24] [layout b25d,80x24,0,0,0] @0
1: second* (2 panes) [80x24] [layout 020a,80x24,0,0{40x24,0,0,1,39x24,41,0,2}] @1 (active)

Здесь demo:1 означает окно с индексом 1 в сессии demo. Звёздочка рядом с second показывает текущее окно, а (2 panes) — число панелей. Список панелей выводится так:

tmux list-panes -t demo -F '#{pane_index} #{pane_current_command} #{pane_width}x#{pane_height}'
0 bash 40x24
1 bash 39x24

Фоновый режим -d нужен для скриптов и примеров. В интерактивной работе сессия создаётся без него, и терминал сразу оказывается внутри неё.

Основные клавиши

Все команды tmux, кроме запуска, вводятся после префикса: по умолчанию это Ctrl+B. Нажатие Ctrl+B, затем клавиши команды, передаёт команду tmux. Таблица содержит основные клавиши, которые описаны в справке tmux 6. Список всех привязок выводится командой tmux list-keys -N -T prefix, а справка по сочетаниям — клавишей ? после префикса.

Клавиши (после Ctrl+B)Действие
cсоздать новое окно
n, pследующее и предыдущее окно
0–9выбрать окно с номером
,переименовать окно
%разделить окно на две панели слева и справа
"разделить окно на две панели сверху и снизу
стрелкиперейти к соседней панели
oперейти к следующей панели
zразвернуть панель на всё окно и обратно
xзакрыть текущую панель
&закрыть текущее окно
[режим копирования и прокрутки (выход — клавиша q)
sвыбрать сессию из списка
dотсоединиться от сессии
?список привязок клавиш

Вывод tmux list-keys -N содержит те же привязки с краткими английскими описаниями. Таблица не полна, однако покрывает повседневные действия.

Отключение и подключение

Главная причина использовать tmux — возможность отсоединиться от сессии, не завершив процессы в ней. Если терминал закрыт или связь по SSH оборвалась, сессия продолжает работать на сервере. Чтобы вернуться, достаточно подключиться заново и выполнить tmux attach. Проверим отсоединение на опыте. Эксперимент требует псевдотерминала, поэтому подключение выполняется через программу на Python, которая имитирует клиента:

python3 -I - <<'EOF'
import os, pty, time, select
pid, fd = pty.fork()
if pid == 0:
    os.execvp("tmux", ["tmux", "attach", "-t", "demo"])
def drain(t):
    end = time.time() + t
    while time.time() < end:
        r, _, _ = select.select([fd], [], [], 0.1)
        if r:
            try: os.read(fd, 4096)
            except OSError: break
drain(1.0)
os.write(fd, b"\x02d")   # Ctrl+B, затем d: отсоединение
drain(1.0)
os.waitpid(pid, 0)
print("клиент отсоединён")
EOF
tmux ls
клиент отсоединён
demo: 2 windows (created Mon Oct 12 13:16:28 2026)

Клиент отсоединился, а сессия с двумя окнами осталась. Чтобы подключиться снова в обычном терминале, выполняется tmux attach -t demo. Если сессия одна, имя можно не указывать. Для завершения сессии служит tmux kill-session -t demo.

При работе по SSH сочетание tmux и отсоединения решает практическую задачу: длительная операция, например копирование большого каталога, продолжается на сервере даже после обрыва связи. Ниже в этом курсе, в теме 16, такие операции окажутся частью резервного копирования.

Итоги темы

Окружение оболочки состоит из переменных, которые делятся на переменные оболочки и переменные окружения. Переменная становится частью окружения после экспорта, и только экспортированные переменные получают дочерние процессы. Дочерний процесс работает с копией окружения, поэтому его изменения не возвращаются родителю. Это объясняет и поведение команды cd, которая выполняется самой оболочкой, и различие между обычным запуском файла и source. Переменная PATH задаёт порядок поиска программ; личный каталог ~/bin добавляется в неё через файл запуска, и порядок каталогов определяет, какая программа будет вызвана.

Файлы запуска различаются для login и non-login, interactive и non-interactive оболочек. Порядок их чтения определяет, где нужно задавать настройку: в ~/.bashrc для интерактивной работы, в ~/.profile или ~/.bash_profile для входа в систему. Приглашение PS1, история команд и локаль тоже задаются переменными, которые можно проверить и изменить. Локаль особенно важна при сортировке: результат скрипта может зависеть от неё, поэтому в скриптах принято задавать LC_ALL=C для данных, которые должны совпадать на разных компьютерах.

Современные утилиты fzf, rg, fd, bat, ncdu и tldr ускоряют интерактивную работу и подключаются теми же механизмами — строками в ~/.bashrc и ссылками в ~/bin, но в скриптах по-прежнему используются классические grep, find, cat и du. Терминальный мультиплексор tmux дополняет эти знания: он сохраняет работу при разрыве связи и позволяет держать несколько оболочек в одном окне. Эта возможность понадобится при работе по SSH в теме 11, где удалённый сеанс будет обычным способом администрирования учебного сервера, а затем в теме 16, где резервное копирование должно переживать обрыв связи. Тема 9 покажет, как системные службы запускаются без участия терминала, и это дополнит картину: пользовательская оболочка настраивается файлами запуска, а фоновые службы — юнит-файлами systemd.

Литература

  1. Shotts W. The Linux Command Line: A Complete Introduction. 2nd ed. — No Starch Press, 2019. Открытая версия: https://linuxcommand.org/tlcl.php
  2. Free Software Foundation. GNU Bash Reference Manual. — https://www.gnu.org/software/bash/manual/
  3. Free Software Foundation. GNU Coreutils Manual. — https://www.gnu.org/software/coreutils/manual/
  4. The Open Group. The Open Group Base Specifications Issue 7 (IEEE Std 1003.1-2017), Shell Command Language. — https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
  5. Linux Foundation. Filesystem Hierarchy Standard 3.0. — 2015. https://refspecs.linuxfoundation.org/FHS_3.0/fhs-3.0.html
  6. tmux wiki. — https://github.com/tmux/tmux/wiki
  7. The Linux man-pages project. — https://man7.org/linux/man-pages/