upd
This commit is contained in:
parent
423468a50b
commit
a3c6d296ab
@ -31,14 +31,13 @@
|
|||||||
PS1='\[\033[0;96m\]$(__git_ps1 "(%s) ")\[\033[0;92m\]\w \[\033[0;31m\]$(__exit_code_ps1 $?)\[\033[0;15m\]\$ '
|
PS1='\[\033[0;96m\]$(__git_ps1 "(%s) ")\[\033[0;92m\]\w \[\033[0;31m\]$(__exit_code_ps1 $?)\[\033[0;15m\]\$ '
|
||||||
fi
|
fi
|
||||||
|
|
||||||
На GitHub можно найти много всяких скриптов или даже целых "фремворков" типа `этого`_
|
На GitHub можно найти много всяких скриптов или даже целых "фремворков" типа
|
||||||
или `отдельные скрипты`_ для Git. А вы знали, что скрипт для PS1 и так `поставляется`_
|
`этого`_ или `отдельные скрипты`_ для Git. Однако, скрипт для PS1 и так
|
||||||
в пакете Git? Не вижу смысла не использовать его, если только вам не надо
|
`поставляется`_ в пакете Git.
|
||||||
как-то иначе работать с PS1.
|
|
||||||
|
|
||||||
Помимо **git-prompt.sh** я использую функцию ``__exit_code_ps1()``, которая просто
|
Помимо **git-prompt.sh** я использую функцию ``__exit_code_ps1()``, которая
|
||||||
печатает число, если оно не равно нулю. Это очень удобно — всегда видишь код выхода
|
просто печатает число, если оно не равно нулю. Это очень удобно — всегда
|
||||||
предыдущей запущенной команды, порой очень помогает при отладке скриптов.
|
видишь код выхода команды. Порой это очень помогает при отладке скриптов.
|
||||||
|
|
||||||
В итоге всё это дело у меня выглядит вот так:
|
В итоге всё это дело у меня выглядит вот так:
|
||||||
|
|
||||||
|
246
content/do_boring_backups.rst
Normal file
246
content/do_boring_backups.rst
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
:title: Делаем скучные бэкапы
|
||||||
|
:date: 8 Oct 22
|
||||||
|
|
||||||
|
=====================
|
||||||
|
Делаем скучные бэкапы
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Бэкапы, в самом деле, нескучные (:
|
||||||
|
|
||||||
|
Мотивацией для написания очередного велосипеда стало то, что даже для, казалось
|
||||||
|
бы, готовых инструментов приходится писать обвязки на шелле.
|
||||||
|
|
||||||
|
В какой-то момент мне надоело писать баш-портянки для бэкапа и я написал одну
|
||||||
|
большую, которая умеет сразу много всего. Знакомьтесь — **boring_backup**.
|
||||||
|
|
||||||
|
> **Пакеты для Debian/Ubuntu, Arch и сорцы**
|
||||||
|
`здесь <https://git.nxhs.cloud/ge/boring_backup/releases>`_.
|
||||||
|
|
||||||
|
Сначала о том, чем он *НЕ* является:
|
||||||
|
|
||||||
|
* Тупым линейным скриптом с вызовом tar и mysqldump.
|
||||||
|
* Полным аналогом `restic <https://restic.net/>`_ и похожих утилит.
|
||||||
|
* POSIX-совместимым скриптом. Хотелось бы, конечно, но это достаточно больно.
|
||||||
|
Может быть когда-нибудь я перепишу код и оно заработает на dash/ash и прочих
|
||||||
|
шеллах, но это будет уже другой скрипт.
|
||||||
|
|
||||||
|
**boring_backup** — это библиотека функций Bash и интерфейс командной строки
|
||||||
|
для скриптов резервного копирования.
|
||||||
|
|
||||||
|
Вот что в ней есть:
|
||||||
|
|
||||||
|
* Обработка ошибок (wow!)
|
||||||
|
* Логирование в файл или syslog (WOW!)
|
||||||
|
* Парсер URI практически целиком совместимый с RFC 3986!
|
||||||
|
* Готовые функции для запаковки файлов (tar) и снятия SQL-дампов (MySQL/MariaDB
|
||||||
|
и PostgreSQL)
|
||||||
|
|
||||||
|
Логика работы утилиты крайне проста. Есть два массива данных — список URI
|
||||||
|
исходных ресурсов, которые надо бэкапить и список URI хранилищ, куда предстоит
|
||||||
|
бэкапиться.
|
||||||
|
|
||||||
|
Поддерживается определённый набор схем URI, каждую схему обрабатывает свой
|
||||||
|
обработчик.
|
||||||
|
|
||||||
|
Cамый простой скрипт резервного копирования, который можно написать с
|
||||||
|
помощью **boring_backup** выглядит так:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sources=(/home/john)
|
||||||
|
targets=(/var/backups)
|
||||||
|
|
||||||
|
Сохраним это в файл под именем `test`.
|
||||||
|
|
||||||
|
Здесь очевидно дира /home/john будет скопирована в /var/backups, однако cперва
|
||||||
|
она будет запакована в архив tar без сжатия. Запускаем бэкап:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
boring_backup test
|
||||||
|
|
||||||
|
Итого получим файл /var/backups/test_john_2022.10.08-1230.tar. Если мы хотим
|
||||||
|
сжать архив, то добавим компрессию:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sources=(/home/john)
|
||||||
|
targets=(/var/backups)
|
||||||
|
compression=xz
|
||||||
|
|
||||||
|
Здесь значение переменной ``compression`` будет интерпретироваться так же как
|
||||||
|
tar интерпретирует расширение архива с опцией ``--auto-compress``. То есть
|
||||||
|
поддерживаются все утилиты, которыми tar умеет сжимать. Если переменная пуста
|
||||||
|
или содержит что-то не то, то сжатие будет отключено.
|
||||||
|
|
||||||
|
Надо исключить лишние файлы из архива? Легко!
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sources=(/home/john)
|
||||||
|
targets=(/var/backups)
|
||||||
|
compression=xz
|
||||||
|
tar_exclude=(.cache foo bar)
|
||||||
|
|
||||||
|
Для передачи опций для tar есть переменная ``tar_options`` в которой можно
|
||||||
|
переопределить умолчание — ``-acf``.
|
||||||
|
|
||||||
|
Теперь добавим базу данных MySQL в бэкап. Записывается URI в формате, который
|
||||||
|
ещё называют DSN (data source name):
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sources=(
|
||||||
|
/home/john
|
||||||
|
mysql://test_usr:%2C%23s%7BRGqH@localhost/test_db
|
||||||
|
)
|
||||||
|
targets=(/var/backups)
|
||||||
|
compression=xz
|
||||||
|
tar_exclude=(.cache foo bar)
|
||||||
|
|
||||||
|
Обратите внимание, что пароль пользователя закодирован, чтобы не возникло
|
||||||
|
коллизий из-за наличия в нём спецсимволов. Закодировать пароль крайне просто,
|
||||||
|
вот пример на Perl, который можно выполнить прямо в командной строке:
|
||||||
|
|
||||||
|
.. code-block:: perl
|
||||||
|
|
||||||
|
echo ',#s{RGqH' | perl -MURI::Escape -wlne 'print uri_escape $_'
|
||||||
|
|
||||||
|
Теперь в /var/backups окажется два файла — test_john_2022.10.08-1230.tar.xz
|
||||||
|
и test_test_db_2022.10.08-1230.sql.xz. Сваливать все файлы в одну директорию
|
||||||
|
может быть неудобно, поэтому давайте сохранять файлы в папки по датам.
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sources=(
|
||||||
|
/home/john
|
||||||
|
mysql://test_usr:%2C%23s%7BRGqH@localhost/test_db
|
||||||
|
)
|
||||||
|
today=$(date +%Y-%m-%d) # в результате даст дату yyyy-mm-dd
|
||||||
|
make_target_dir=yes
|
||||||
|
targets=(/var/backups/$today)
|
||||||
|
compression=xz
|
||||||
|
tar_exclude=(.cache foo bar)
|
||||||
|
|
||||||
|
Теперь пути до файлов будут выглядеть так:
|
||||||
|
/var/backups/2022-10-08/test_john_2022.10.08-1230.tar.xz.
|
||||||
|
|
||||||
|
Теперь поробуем передать эти файлы ещё и на удалённое хранилище. Допустим на
|
||||||
|
S3-совместимое объектное хранилище. Сперва придётся установить и настроить
|
||||||
|
утилиту s3cmd, затем скрипт примет вид:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sources=(
|
||||||
|
/home/john
|
||||||
|
mysql://test_usr:%2C%23s%7BRGqH@localhost/test_db
|
||||||
|
)
|
||||||
|
today=$(date +%Y-%m-%d)
|
||||||
|
make_target_dir=yes
|
||||||
|
targets=(
|
||||||
|
/var/backups/$today
|
||||||
|
s3://my_bucket/$today
|
||||||
|
)
|
||||||
|
compression=xz
|
||||||
|
tar_exclude=(.cache foo bar)
|
||||||
|
|
||||||
|
**boring_backup** сначала сделает бэкап и сохранит его в /var/backups и только
|
||||||
|
затем передаст файлы в S3-хранилище (или любое другое). Можно указывать любое
|
||||||
|
количество URI в targets и также в sources. Особенностью targets является то,
|
||||||
|
что требуется хотя бы один URI со схемой `file`, куда будут сохраняться
|
||||||
|
локальные бэкапы. Просто путь без указания схемы тоже считается за file.
|
||||||
|
Следующие три записи полностью эквивалентны:
|
||||||
|
|
||||||
|
* /var/backups
|
||||||
|
* file:/var/backups
|
||||||
|
* file:///var/backups
|
||||||
|
|
||||||
|
Пойдём дальше.
|
||||||
|
|
||||||
|
Мы можем обрабатывать ошибки. Допишем в скрипт функцию `on_error`, которая
|
||||||
|
будет нам отправлять письмо с фрагментом лога и текстом ошибки.
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sources=(
|
||||||
|
/home/john
|
||||||
|
mysql://test_usr:%2C%23s%7BRGqH@localhost/test_db
|
||||||
|
)
|
||||||
|
today=$(date +%Y-%m-%d)
|
||||||
|
make_target_dir=yes
|
||||||
|
targets=(
|
||||||
|
/var/backups/$today
|
||||||
|
s3://my_bucket/$today
|
||||||
|
)
|
||||||
|
compression=xz
|
||||||
|
tar_exclude=(.cache foo bar)
|
||||||
|
|
||||||
|
email=me@example.com
|
||||||
|
on_error() {
|
||||||
|
local log_fragment err_message
|
||||||
|
err_message="$*"
|
||||||
|
log_fragment="$(grep -n 'Backup started' "$log_file" | tail -1 |
|
||||||
|
cut -d ':' -f 1 | xargs -I {} tail -n +{} "$log_file")"
|
||||||
|
printf \
|
||||||
|
'Текст ошибки:\n%s\n\nЛог последнего бэкапа:\n%s' \
|
||||||
|
"$err_message" "$log_fragment" |
|
||||||
|
mail -s "$HOSTNAME: Ошибка при выполнении бэкапа" "$email"
|
||||||
|
}
|
||||||
|
|
||||||
|
Также мы можем после копирование в удалённое хранилище удалять локальный
|
||||||
|
бэкап. Список всех созданных за текущее выполнение скрипта бэкапов хранится
|
||||||
|
в массиве ``backups``.
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
finalise() {
|
||||||
|
log -p "\tClean up local backups"
|
||||||
|
log -p "\tFiles to delete: ${backups[@]}"
|
||||||
|
rm -rv -- "${backups[@]}"
|
||||||
|
log -p "\tLocal backups deleted"
|
||||||
|
}
|
||||||
|
|
||||||
|
Это лишь небольшая часть того, что можно реализовать. Полная документация
|
||||||
|
есть `в виде мануала </cgi-bin/man.cgi?q=boring_backup&l=ru>`_.
|
||||||
|
|
||||||
|
Отмечу ещё несколько важных фич:
|
||||||
|
|
||||||
|
* Стандартную логику можно полностью переопределить, в том числе выкинуть
|
||||||
|
необходимость сохранять локальный бэкап. Достаточно описать функцию с
|
||||||
|
именем `backup`.
|
||||||
|
* Можно выполнять действия перед бэкапом в функции `prepare` и после бэкапа
|
||||||
|
— `finalise`.
|
||||||
|
|
||||||
|
Есть и вещи, которые вам не понравятся, но решение которых вероятно появится
|
||||||
|
в слудующих версиях **boring_backup**:
|
||||||
|
|
||||||
|
* Концепция репозитория бэкапов не поддерживается. Файлы просто загружается
|
||||||
|
туда куда вы укажете.
|
||||||
|
* Из коробки можно делать только полные бэкапы.
|
||||||
|
* Нет ротации бэкапов. Её нужно реализовывать самостоятельно.
|
||||||
|
* Из коробки нет шифрования. GPG и кастомная функция `backup` вам в помощь.
|
||||||
|
|
||||||
|
В заключение дам ещё один пример скрипта для бэкапа приложения Gitea.
|
||||||
|
Попробуйте сами разобраться что тут происходит.
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sources=(.) # just pass validation
|
||||||
|
targets=(.)
|
||||||
|
today="$(date +%d_%b_%Y)"
|
||||||
|
s3cmd_config=~/.s3cfg
|
||||||
|
prepare() {
|
||||||
|
systemctl stop gitea.service
|
||||||
|
sleep 5
|
||||||
|
}
|
||||||
|
backup() {
|
||||||
|
log -p "Dumping Gitea"
|
||||||
|
su -c "/usr/local/bin/gitea dump -c /etc/gitea/app.ini \
|
||||||
|
-f /home/git/.cache/gitea_dump.zip" - git 2>> "$log_file"
|
||||||
|
backups+=(/home/git/.cache/gitea_dump.zip)
|
||||||
|
tgt_s3cmd s3://mybucket/backups/gitea-$today
|
||||||
|
}
|
||||||
|
finalise() {
|
||||||
|
systemctl start gitea.service
|
||||||
|
rm -rv -- "${backups[@]}"
|
||||||
|
}
|
189
content/posixish_functions.rst
Normal file
189
content/posixish_functions.rst
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
:title: Несколько полезных функций на Bourne Shell
|
||||||
|
:date: 18 Oct 22
|
||||||
|
|
||||||
|
==========================================
|
||||||
|
Несколько полезных функций на Bourne Shell
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
Функции, описанные здесь можно применять в любой POSIX-совместимой оболочке.
|
||||||
|
Протестировано в **dash**. Примеры на этой странице будут обновляться.
|
||||||
|
|
||||||
|
**1. Парсер аргументов в стиле GNU**
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
optval() {
|
||||||
|
# GNU-style command line options parser
|
||||||
|
#
|
||||||
|
# Usage: optval "$1" "$2"
|
||||||
|
#
|
||||||
|
# Set variables:
|
||||||
|
# OPT - option name
|
||||||
|
# VAL - value
|
||||||
|
# SFT - value for shift command
|
||||||
|
|
||||||
|
OPT="${1%%=*}"; VAL="${1#*=}"; SFT=1
|
||||||
|
|
||||||
|
if [ "$OPT" = "$VAL" ]; then
|
||||||
|
if [ -n "$2" ] && [ "${2#"${2%%?}"}" != "-" ]; then
|
||||||
|
VAL="$2"; SFT=2
|
||||||
|
else
|
||||||
|
unset VAL
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -z "$VAL" ]; then
|
||||||
|
echo Missing argument for option "$OPT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
Эта функция позволит надёжно распарсить опции с обязательным аргументом. Ей
|
||||||
|
по силам:
|
||||||
|
|
||||||
|
* **-o** *value*
|
||||||
|
* **--option**\ =\ *value*
|
||||||
|
* **--option**\ =\ *name=value*
|
||||||
|
* **-option** *value*
|
||||||
|
|
||||||
|
..и вариации.
|
||||||
|
|
||||||
|
Непосредственно в POSIX-оболочке работать с парсером будет тяжко из-за
|
||||||
|
отсутсвия массивов. Единственный доступный массив **$@** здесь занят. NULL в
|
||||||
|
качестве разделителя для списка позиционных аргументов использовать не
|
||||||
|
получится, поэтому ниже пример с двоеточием. В **Bash** таких проблем нет.
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
while [ "$#" -ne 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-h|--help)
|
||||||
|
echo "Usage: $0 [-chv] [--] arguments"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-v|--version)
|
||||||
|
echo 1.0.0
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-c|-c=*|--config|--config=*)
|
||||||
|
optval "$1" "$2"
|
||||||
|
config="$VAL"
|
||||||
|
shift "$SFT"
|
||||||
|
;;
|
||||||
|
--) # end of options processing
|
||||||
|
shift
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
echo Unknown option "$1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*) # Save positional arguments
|
||||||
|
# In Bash better use ARGS+=("$1") instead
|
||||||
|
ARGS="$1":"$ARGS"
|
||||||
|
shift
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Set positional arguments. It is not needed in Bash
|
||||||
|
# In Bash you can use ARGS array directly or use set -- "${ARGS[@]}"
|
||||||
|
OLDIFS="$IFS"; IFS=:
|
||||||
|
# $arg must be unquoted!
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
for arg in $ARGS; do set -- $arg "$@"; done
|
||||||
|
IFS="$OLDIFS"
|
||||||
|
|
||||||
|
echo Config: "$config"
|
||||||
|
echo Positional arguments:
|
||||||
|
for arg in "$@"; do; echo : "$arg"; done
|
||||||
|
|
||||||
|
**2. Интерактивный диалог**
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
# Confirmation interactive dialog
|
||||||
|
#
|
||||||
|
# Usage: confirm "message"
|
||||||
|
|
||||||
|
[ -n "$ASSUME_NO" ] && return 1
|
||||||
|
[ -n "$ASSUME_YES" ] && return 0
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
printf '%s (y/n) ' "$*"
|
||||||
|
read -r REPLY
|
||||||
|
case "$REPLY" in
|
||||||
|
[Yy]|[Yy][Ee][Ss]) return 0;;
|
||||||
|
[Nn]|[Nn][Oo]) return 1;;
|
||||||
|
*) echo Please, answer y or n;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
Здесь предусмотрены переменные ``ASSUME_NO`` и ``ASSUME_YES`` для
|
||||||
|
неинтерактивного режима. Автоматическое "Нет" и "Да" соответственно. Пример:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
if confirm 'Proceed?'; then
|
||||||
|
echo OK
|
||||||
|
else
|
||||||
|
echo Abort
|
||||||
|
fi
|
||||||
|
|
||||||
|
**3. Версия Python**
|
||||||
|
|
||||||
|
Просто напечатает в STDOUT версию интерпретатора Python.
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
python_version() {
|
||||||
|
# Get Python interpreter version in format 'major.minor.micro' e.g. '3.10.2'
|
||||||
|
# Works with Python 2.x and 3.x.
|
||||||
|
#
|
||||||
|
# Usage: python_version [<path to interpreter>]
|
||||||
|
|
||||||
|
"${1:-python}" -c "import sys; v=sys.version_info; \
|
||||||
|
print('%s.%s.%s' % (v.major, v.minor, v.micro))"
|
||||||
|
}
|
||||||
|
|
||||||
|
**4. Открыть редактор по умолчанию**
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
default_editor() {
|
||||||
|
# Edit file in user's default editor
|
||||||
|
#
|
||||||
|
# Usage: default_editor file
|
||||||
|
|
||||||
|
if [ -n "$EDITOR" ]; then
|
||||||
|
"$EDITOR" "$@"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if hash select-editor >/dev/null 2>&1; then
|
||||||
|
if [ -f ~/.selected_editor ]; then
|
||||||
|
. ~/.selected_editor
|
||||||
|
else
|
||||||
|
select-editor
|
||||||
|
. ~/.selected_editor
|
||||||
|
fi
|
||||||
|
"$SELECTED_EDITOR" "$@"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if hash editor >/dev/null 2>&1; then
|
||||||
|
editor "$@"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
vi "$@" # fallback to vi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
**5. Простой URL-decoder**
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
unescape_uri() {
|
||||||
|
echo "$1" | sed 's/%/\\\\x/g' | xargs printf
|
||||||
|
}
|
@ -8,7 +8,8 @@
|
|||||||
|
|
||||||
* `rSW </rsw/>`_ -- генератор статических сайтов из reStructuredText
|
* `rSW </rsw/>`_ -- генератор статических сайтов из reStructuredText
|
||||||
* tui.sh -- библиотека элементов TUI для POSIX-совместимых оболочек
|
* tui.sh -- библиотека элементов TUI для POSIX-совместимых оболочек
|
||||||
* boring-backup -- Bash фреймворк для скриптов резервного копирования
|
* `boring-backup <https://git.nxhs.cloud/ge/boring_backup>`_ -- Bash
|
||||||
|
фреймворк для скриптов резервного копирования
|
||||||
* `imgs <https://git.nxhs.cloud/ge/imgs>`_ -- минималистичный хостинг картинок
|
* `imgs <https://git.nxhs.cloud/ge/imgs>`_ -- минималистичный хостинг картинок
|
||||||
* `vk-toot <https://git.nxhs.cloud/ge/vk-toot>`_ -- кросспостер VK -> Mastodon
|
* `vk-toot <https://git.nxhs.cloud/ge/vk-toot>`_ -- кросспостер VK -> Mastodon
|
||||||
* `piglet <https://git.hxhs.cloud/ge/piglet>`_ -- клиент DNS API Porkbun
|
* `piglet <https://git.hxhs.cloud/ge/piglet>`_ -- клиент DNS API Porkbun
|
||||||
|
253
content/terminal_muxers.rst
Normal file
253
content/terminal_muxers.rst
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
:title: Терминальные мультиплексоры
|
||||||
|
:date: 19 Oct 22
|
||||||
|
|
||||||
|
===========================
|
||||||
|
Терминальные мультиплексоры
|
||||||
|
===========================
|
||||||
|
|
||||||
|
Терминальный мультиплексор — это утилита, которая позволяет запускать
|
||||||
|
несколько сессий оболочки в рамках одной сессии и также отсоединять
|
||||||
|
собственную сессию от оболочки.
|
||||||
|
|
||||||
|
Для пояснения зачем это нужно я процитирую русскую Википедию:
|
||||||
|
|
||||||
|
Это полезно для работы с несколькими программами из командной строки, а
|
||||||
|
также для запуска программ на удаленном сервере.
|
||||||
|
|
||||||
|
Возможно звучит сложно, но всё проще чем кажется. В этой заметке я попробую
|
||||||
|
наглядно объяснить концепцию и приёмы работы с мультиплексорами терминала.
|
||||||
|
|
||||||
|
Сейчас распространено два мультиплексора терминала:
|
||||||
|
|
||||||
|
* `GNU screen <https://www.gnu.org/software/screen/>`_
|
||||||
|
* `tmux <https://github.com/tmux/tmux/wiki>`_
|
||||||
|
|
||||||
|
Рано или поздно вы с одним из них столкнётесь.
|
||||||
|
|
||||||
|
Матчасть
|
||||||
|
========
|
||||||
|
|
||||||
|
Вот основные концепции, которые использются в упомянутых утилитах.
|
||||||
|
|
||||||
|
Сессия
|
||||||
|
Непосредственно сессия мультиплексора терминала. Сессий может быть
|
||||||
|
множество, их можно создавать, закрывать, отсоединяться от них или
|
||||||
|
присоединяться, задать имя.
|
||||||
|
|
||||||
|
Окно
|
||||||
|
В любой сессии мультиплекcора есть как минимум одно окно. Если закрыть
|
||||||
|
последнее окно, то закроется вся сессия. В каждом окне запускается новая
|
||||||
|
сессия командной оболочки (Bash, Zsh и пр.).
|
||||||
|
|
||||||
|
Окон также может быть множество. К ним применимы те же действия, что и к
|
||||||
|
сессиям, кроме отсоединения.
|
||||||
|
|
||||||
|
Между окнами можно переключаться с помощью горячих клавиш.
|
||||||
|
|
||||||
|
Область экрана (или панель)
|
||||||
|
Каждое окно можно разбить на несколько областей. В каждой области будет
|
||||||
|
запущена новая сессия оболочки.
|
||||||
|
|
||||||
|
Делить окно можно по вертикали и горизонтали. Размеры областей обычно
|
||||||
|
можно отрегулировать. По умолчанию экран делится пополам.
|
||||||
|
|
||||||
|
Здесь надо отметить, что в **screen** понятия окно и область экрана
|
||||||
|
смешаны. То есть область экрана приравнивается к отдельному окну, хотя
|
||||||
|
они и отображаются на одном экране.
|
||||||
|
|
||||||
|
Итак, я подключаюсь к серверу по SSH с целью запустить там какую-то команду.
|
||||||
|
После запуска команды я намерен отключиться от сервера. При этом команда
|
||||||
|
должна продолжить выполняться. Я также должен увидеть результат её выполнения,
|
||||||
|
когда вновь подключусь к серверу.
|
||||||
|
|
||||||
|
**1.** Подключаюсь по SSH:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
localhost server
|
||||||
|
+------------+ +------------------+
|
||||||
|
| ssh_client |----->| ssh_session |
|
||||||
|
+------------+ +------------------+
|
||||||
|
(you are here)----->| shell_session(0) |
|
||||||
|
+------------------+
|
||||||
|
|
||||||
|
После успешного подключения по SSH я окажусь в сессии оболочки — **(0)**.
|
||||||
|
При этом если оборвётся SSH-сессия, то я потеряю всё что в ней было.
|
||||||
|
|
||||||
|
**2.** Как только я запущу **screen** картинка примет следующий вид:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
localhost server
|
||||||
|
+------------+ +------------------+
|
||||||
|
| ssh_client |----->| ssh_session |
|
||||||
|
+------------+ +------------------+
|
||||||
|
| shell_session(0) |
|
||||||
|
+------------------+
|
||||||
|
| screen_session |
|
||||||
|
+------------------+
|
||||||
|
(you are here)----->| shell_session(1) |
|
||||||
|
+------------------+
|
||||||
|
|
||||||
|
**3.** Для примера запущу **vim**:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
localhost server
|
||||||
|
+------------+ +------------------+
|
||||||
|
| ssh_client |----->| ssh_session |
|
||||||
|
+------------+ +------------------+
|
||||||
|
| shell_session(0) |
|
||||||
|
+------------------+
|
||||||
|
| screen_session |
|
||||||
|
+------------------+
|
||||||
|
(you are here)----->| shell_session(1) |
|
||||||
|
+------------------+
|
||||||
|
| vim |
|
||||||
|
+------------------+
|
||||||
|
|
||||||
|
**4.** Теперь я отключусь (detach) от сессии **screen**:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
localhost server
|
||||||
|
+------------+ +------------------+ +------------------+
|
||||||
|
| ssh_client |----->| ssh_session | | screen_session |
|
||||||
|
+------------+ +------------------+ +------------------+
|
||||||
|
(you are here)----->| shell_session(0) | | shell_session(1) |
|
||||||
|
+------------------+ +------------------+
|
||||||
|
| vim |
|
||||||
|
+------------------+
|
||||||
|
|
||||||
|
Сессия **screen** отсоединилась от оболочки **(0)**. При этом оболочка **(1)**
|
||||||
|
продолжает работать. Сейчас можно отключиться от SSH.
|
||||||
|
|
||||||
|
Когда я подключусь к серверу вновь, то окажусть в ситуации как на схеме 4, но
|
||||||
|
после выполнения команды **screen -r** всё вернётся к состоянию как на схеме 3
|
||||||
|
и я смогу продолжить работу в **vim**.
|
||||||
|
|
||||||
|
.. admonition:: Как набирать комбинации клавиш
|
||||||
|
|
||||||
|
Вначале набрается модификатор, например ``Ctrl+a``, затем надо отпустить
|
||||||
|
клавиши и нажать на клавишу команды, например ``?``.
|
||||||
|
|
||||||
|
screen
|
||||||
|
======
|
||||||
|
|
||||||
|
**screen** просто работает. Он минималистичен и прост в использовании.
|
||||||
|
|
||||||
|
Запуск новой сессии:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
screen [-S имя_сессии]
|
||||||
|
|
||||||
|
Подключиться к запущенной сессии:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
screen -r [имя_сессии]
|
||||||
|
|
||||||
|
=============== ============================================================
|
||||||
|
Комбинация Действие
|
||||||
|
=============== ============================================================
|
||||||
|
Ctrl+a ? Показать справку
|
||||||
|
Ctrl+a : Открыть приглашение для ввода команд screen
|
||||||
|
Ctrl+a " Список окон
|
||||||
|
Ctrl+a 0 Открыть окно номер 0
|
||||||
|
Ctrl+a A Переименовать текущее окно
|
||||||
|
Ctrl+a a Отправить комбинацию Ctrl+a в текущее окно
|
||||||
|
Ctrl+a c Создать новое окно (с оболочкой)
|
||||||
|
Ctrl+a S Разделить текущую область по горизонтали
|
||||||
|
Ctrl+a | Разделить текущую область по вертикали
|
||||||
|
Ctrl+a tab Перевести фокус ввода на следующую область
|
||||||
|
Ctrl+a Ctrl+a Переключиться между текущей и предыдущей областью
|
||||||
|
Ctrl+a Esc Перейти в режим копирования (используйте Enter для выделения
|
||||||
|
текста). Также скроллинг терминала.
|
||||||
|
Ctrl+a ] Вставить текст
|
||||||
|
Ctrl+a Q Закрыть все окна, кроме текущего
|
||||||
|
Ctrl+a X Закрыть текущую область
|
||||||
|
Ctrl+a d Отсоединиться от текущей сессии
|
||||||
|
=============== ============================================================
|
||||||
|
|
||||||
|
При разделении экрана на области появится новая пустая область. Нужно
|
||||||
|
переключить на неё фокус и запустить новое окно с оболочкой ``Ctrl+a`` ``c``.
|
||||||
|
|
||||||
|
Конфигурация **screen** хранится в файле **~/.screenrc**. Я для себя написал
|
||||||
|
совсем простой конфиг, который тем не менее показывает всё что действительно
|
||||||
|
нужно.
|
||||||
|
|
||||||
|
.. code-block:: unixconfig
|
||||||
|
|
||||||
|
startup_message off
|
||||||
|
hardstatus alwayslastline
|
||||||
|
hardstatus string '%S: %-w%>(%n %t)%{-}%+w%<'
|
||||||
|
|
||||||
|
.. image:: https://i.nxhs.cloud/Swl.png
|
||||||
|
:alt: Terminal with screen
|
||||||
|
|
||||||
|
Материалы по **screen**:
|
||||||
|
|
||||||
|
* `GNU screen usage <http://gnuscreen.org/>`_
|
||||||
|
* `GNU screen - ArchWiki <https://wiki.archlinux.org/title/GNU_Screen>`_
|
||||||
|
|
||||||
|
tmux
|
||||||
|
====
|
||||||
|
|
||||||
|
**tmux** — это более новороченное решение. Он умеет почти всё то же что
|
||||||
|
**screen** + имеет дополнительные фичи вроде управления мышью.
|
||||||
|
|
||||||
|
Запустить новую сессию:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
tmux [new -s имя_сессии]
|
||||||
|
|
||||||
|
Подключиться к сессии:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
tmux a [-t имя_сессии]
|
||||||
|
|
||||||
|
=============== ============================================================
|
||||||
|
Комбинация Действие
|
||||||
|
=============== ============================================================
|
||||||
|
Ctrl+b c Создать новое окно
|
||||||
|
Ctrl+b , Переименовать окно
|
||||||
|
Ctrl+b w Список окон
|
||||||
|
Ctrl+b " Разделить текущую область по горизотали
|
||||||
|
ctrl+b % Разделить текущую область по вертикали
|
||||||
|
Ctrl+b x Закрыть текущую панель
|
||||||
|
Ctrl+b [ Перейти в режим копирования текста + скроллинг
|
||||||
|
Ctrl+b d Отсоединиться от сессии
|
||||||
|
=============== ============================================================
|
||||||
|
|
||||||
|
Мой **~/.tmux.conf**:
|
||||||
|
|
||||||
|
.. code-block:: unixconfig
|
||||||
|
|
||||||
|
set -g mouse on
|
||||||
|
set -g history-limit 5000
|
||||||
|
|
||||||
|
# vim style controls
|
||||||
|
bind h select-pane -L
|
||||||
|
bind j select-pane -D
|
||||||
|
bind k select-pane -U
|
||||||
|
bind l select-pane -R
|
||||||
|
|
||||||
|
bind -r H resize-pane -L 10
|
||||||
|
bind -r J resize-pane -D 10
|
||||||
|
bind -r K resize-pane -U 10
|
||||||
|
bind -r L resize-pane -R 10
|
||||||
|
|
||||||
|
# Copy / paste
|
||||||
|
bind b list-buffers
|
||||||
|
bind B show-buffer
|
||||||
|
bind P paste-buffer
|
||||||
|
|
||||||
|
bind-key -T copy-mode-vi v send-keys -X begin-selection
|
||||||
|
bind-key -T copy-mode-vi y send-keys -X copy-selection
|
||||||
|
bind-key -T copy-mode-vi r send-keys -X rectangle-toggle
|
||||||
|
|
||||||
|
bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel \
|
||||||
|
"xclip -i -f -selection primary | xclip -i -selection clipboard"
|
@ -15,13 +15,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<p>
|
|
||||||
{% if page.template == 'index.jinja2' %}
|
|
||||||
{{ site.title }}
|
|
||||||
{% else %}
|
|
||||||
<a href="/">{{ site.title }}</a> / {{ page.title }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
{% for link in site.links %}
|
{% for link in site.links %}
|
||||||
<a href="{{ link.url }}" target="{{ link.target }}">
|
<a href="{{ link.url }}" target="{{ link.target }}">
|
||||||
|
@ -4,10 +4,13 @@
|
|||||||
<ul id="posts">
|
<ul id="posts">
|
||||||
{% for post in aggr.posts %}
|
{% for post in aggr.posts %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ post.path }}">{{ post.title }}</a>
|
<a href="{{ post.path }}">{{ post.title }}</a><br>
|
||||||
<span class="meta"> — {{ post.date }}</span>
|
<span class="meta">{{ post.date }}</span>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
<section>
|
||||||
|
{{ html | safe }}
|
||||||
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -10,28 +10,30 @@ name = 'Ещё один сайт про cисадмиство и програм
|
|||||||
title = 'Nixhacks'
|
title = 'Nixhacks'
|
||||||
datetime_format = "%d %b %y"
|
datetime_format = "%d %b %y"
|
||||||
|
|
||||||
[[site.links]]
|
[[site.links]]
|
||||||
title = '[home]'
|
title = '[home]'
|
||||||
url = '/'
|
url = '/'
|
||||||
[[site.links]]
|
[[site.links]]
|
||||||
title = '[projects]'
|
title = '[software]'
|
||||||
url = '/projects.html'
|
url = '/projects.html'
|
||||||
[[site.links]]
|
[[site.links]]
|
||||||
title = '[code]'
|
title = '[knowledge base]'
|
||||||
url = 'https://git.nxhs.cloud/ge/'
|
url = '/kb/'
|
||||||
target = '_blank'
|
target = '_blank'
|
||||||
[[site.links]]
|
[[site.links]]
|
||||||
title = '[kb]'
|
title = '[manpages]'
|
||||||
url = '/kb/'
|
url = '/cgi-bin/man.cgi'
|
||||||
target = '_blank'
|
[[site.links]]
|
||||||
[[site.links]]
|
title = '[code]'
|
||||||
title = '[markdown]'
|
url = 'https://git.nxhs.cloud/ge/'
|
||||||
url = '/writing/'
|
target = '_blank'
|
||||||
target = '_blank'
|
[[site.links]]
|
||||||
[[site.links]]
|
title = '[markdown]'
|
||||||
title = '[my ip]'
|
url = '/writing/'
|
||||||
url = '/cgi-bin/ip.cgi'
|
target = '_blank'
|
||||||
target = '_blank'
|
[[site.links]]
|
||||||
|
title = '[my ip]'
|
||||||
|
url = '/cgi-bin/ip.cgi'
|
||||||
|
|
||||||
[site.footer]
|
[site.footer]
|
||||||
text = ''
|
text = ''
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Inconsolata-LGC';
|
||||||
|
src: local('Inconsolata-LGC'),
|
||||||
|
url(/fonts/Inconsolata-LGC.woff2) format('woff2'),
|
||||||
|
url(/fonts/Inconsolata-LGC.woff) format('woff');
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
:root {
|
:root {
|
||||||
--b: #000;
|
--b: #000;
|
||||||
--w: #fff;
|
--w: #fff;
|
||||||
@ -10,12 +17,17 @@
|
|||||||
--a: #0b6adc;
|
--a: #0b6adc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media screen and (max-width: 840px) {
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
color: var(--b);
|
color: var(--b);
|
||||||
background-color: var(--w);
|
background: var(--w);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
max-width: 79ch;
|
max-width: 79ch;
|
||||||
font-family: 'Source Code Pro', monospace;
|
font-family: 'Inconsolata-LGC', monospace;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
@ -54,19 +66,16 @@ section#posts {
|
|||||||
ul#posts li {
|
ul#posts li {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: .5rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
ul#posts {
|
ul#posts {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
dt { /* Defenition list title */
|
dt {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
span.meta {
|
span.docutils.literal {
|
||||||
font-size: 80%;
|
background: #d0d7de;
|
||||||
}
|
|
||||||
span.docutils.literal { /* Inline literal */
|
|
||||||
background-color: #d0d7de;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0 4px 2px 4px;
|
padding: 0 4px 2px 4px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
@ -89,7 +98,7 @@ pre > code {
|
|||||||
}
|
}
|
||||||
blockquote {
|
blockquote {
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
border-left: .25em solid #d0d7de;
|
border-left: .25em solid var(--b);
|
||||||
}
|
}
|
||||||
table, th, td {
|
table, th, td {
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
@ -99,7 +108,7 @@ table, th, td {
|
|||||||
}
|
}
|
||||||
th {
|
th {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background-color: #d0d7de;
|
background: #ebeff2;
|
||||||
}
|
}
|
||||||
td p {
|
td p {
|
||||||
margin: .5rem 0;
|
margin: .5rem 0;
|
||||||
@ -107,12 +116,20 @@ td p {
|
|||||||
hr {
|
hr {
|
||||||
color: #d0d7de;
|
color: #d0d7de;
|
||||||
}
|
}
|
||||||
|
img {
|
||||||
|
max-width: 79ch;
|
||||||
|
}
|
||||||
.admonition {
|
.admonition {
|
||||||
padding: .5rem 1rem;
|
padding: .5rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ebeff2;
|
||||||
}
|
}
|
||||||
.admonition-title {
|
.admonition-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
.admonition-title::before {
|
||||||
|
content: '🛈 ';
|
||||||
|
}
|
||||||
.highlight {
|
.highlight {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
BIN
static/fonts/Inconsolata-LGC.woff
Normal file
BIN
static/fonts/Inconsolata-LGC.woff
Normal file
Binary file not shown.
BIN
static/fonts/Inconsolata-LGC.woff2
Normal file
BIN
static/fonts/Inconsolata-LGC.woff2
Normal file
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 293 KiB After Width: | Height: | Size: 87 KiB |
Loading…
Reference in New Issue
Block a user