BashTex | Linux
2.44K subscribers
43 photos
7 videos
264 links
Авторский канал для тех, кто хочет глубже погрузиться в мир Linux.

Подойдет для разработчиков, системных администраторов и DevOps

Реклама: @dad_admin
Download Telegram
Оптимизация вложенных циклов и массивов

Вложенные циклы в bash - частая причина медленных скриптов при работе с большими массивами и файлами. Особенно если ты обрабатываешь 10000+ элементов и каждый проход делает grep, awk, cut, cat, sed...

▪️ Антипаттерн


for i in "${list1[@]}"; do
for j in "${list2[@]}"; do
if [[ "$i" == "$j" ]]; then
echo "Match: $i"
fi
done
done


Это O(N²). Если массивы по 10к строк - будет 100 млн сравнений.

▪️ Оптимизация через associative array (Bash 4+)


declare -A lookup

# Заполняем хеш
for item in "${list2[@]}"; do
lookup["$item"]=1
done

# Ищем быстро
for i in "${list1[@]}"; do
if [[ ${lookup["$i"]+found} ]]; then
echo "Match: $i"
fi
done


Это уже O(N). И в 1000 раз быстрее.

▪️ Ускорение чтения данных. Избавляемся от cat в цикле:

Плохо:


while read line; do
cat "$line"
done < files.txt


Лучше:


mapfile -t files < files.txt

for f in "${files[@]}"; do
cat "$f"
done


▪️ Убираем лишние циклы. Когда можно - переноси логику внутрь awk, grep, join, sort -m и т.д.

Пример: пересечение двух файлов без bash-циклов:


sort file1.txt file2.txt | uniq -d


BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9
Использование readarray и mapfile для групповой обработки строк

Когда обрабатываем многострочный вывод - часто используем while read, но есть более быстрый способ: mapfile (синоним - readarray).

Что делает mapfile? Читает строки из stdin или команды сразу в массив, построчно:


mapfile -t lines < input.txt
# теперь ${lines[0]}, ${lines[1]} и т.д. - отдельные строки


Аналог:


readarray -t lines < input.txt


Флаг -t удаляет \n на конце строк.

▪️ Обработка вывода команд:


mapfile -t users < <(cut -d: -f1 /etc/passwd)

for user in "${users[@]}"; do
echo "Пользователь: $user"
done


▪️ Фильтрация и трансформация. Комбинируем с grep, awk, sort, uniq:


mapfile -t services < <(systemctl list-units --type=service | awk '/running/ {print $1}')

for svc in "${services[@]}"; do
echo "Активный сервис: $svc"
done


▪️ Срезы и подмассивы. Работа с частями:


mapfile -t logs < <(tail -n 100 /var/log/syslog)

# последние 5 строк
printf '%s\n' "${logs[@]: -5}"


▪️ Использование с read-only входом:


some_func() {
local lines=()
mapfile -t lines
echo "Прочитано строк: ${#lines[@]}"
}

cat /etc/passwd | some_func


BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
Упаковка структуры каталогов с фильтрацией

Когда нужно архивировать не весь каталог, а только часть - с фильтрацией и даже переименованием путей внутри архива - на помощь приходят find, tar и его флаг --transform.

▪️ Базовый пример: фильтруем по маске и архивируем


find project/ -type f -name "*.conf" > list.txt
tar -czf config_backup.tar.gz -T list.txt


Тут мы архивируем только .conf-файлы.

▪️ Добавим переименование путей внутри архива. Архивируем все из src/, но хотим, чтобы внутри архива это лежало как app/.


tar -czf app.tar.gz \
--transform='s|^src|app|' \
-C ./ src


--transform использует sed-подобные выражения. Тут s|^src|app| означает: заменить в начале пути src на app.

▪️ Упаковка через find + --transform + относительные пути. Предположим, хотим архивировать только .sh и .py-файлы, заменив scripts/ на bin/ внутри архива:


find scripts/ -type f \( -name "*.sh" -o -name "*.py" \) > filelist.txt

tar -czf bin_scripts.tar.gz \
--transform='s|^scripts|bin|' \
-T filelist.txt


▪️ Упрощенный однострочник:


find scripts/ -type f -name "*.sh" \
| tar -czf archive.tar.gz --transform='s|^scripts|bin|' -T -


▪️ Продвинутый пример: исключения + трансформация + директория


find ./src -type f ! -name "*.tmp" \
| tar -czf src_clean.tar.gz \
--transform='s|^./src|clean_src|' \
-T -


BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9
Планировщик задач с временными окнами: только ночью, только по будням, только в выходные

Иногда cron не подходит:

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

Тут пригодится встроенная проверка временных окон в скрипте, а cron пусть просто каждые 10 минут запускает "умный планировщик".

🛠 Пример: выполнение задачи только с 02:00 до 05:00 по будням


#!/bin/bash

#настройки временного окна
start_hour=2
end_hour=5

# день недели (1..5 = пн-пт)
dow=$(date +%u)

#часы сейчас
hour=$(date +%H)

#проверка: будний день и нужное время
if (( dow >= 1 && dow <= 5 )) && (( hour >= start_hour && hour < end_hour )); then
echo "[$(date)] Временное окно открыто. Выполняем задачу."

#здесь реальная задача:
/opt/backup/backup-db.sh

else
echo "[$(date)] Вне временного окна. Пропуск."
fi


▪️ Расширение: исключение праздников (по файлу)


holiday_file="/etc/holidays.txt"
today=$(date +%Y-%m-%d)

if grep -q "$today" "$holiday_file"; then
echo "Сегодня праздник. Задача не выполняется."
exit 0
fi


▪️ Альтернатива: запуск только по выходным с 3 до 6 утра


(( dow == 6 || dow == 7 )) && (( hour >= 3 && hour < 6 )) && run_task


▪️ Запуск через cron


*/10 * * * * /usr/local/bin/task_scheduler.sh >> /var/log/task.log 2>&1


BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥10
Бинарные флаги и побитовые операции

Bash - не только про строки. В нtм легко использовать побитовую арифметику (&, |, ^, ~, <<, >>) для управления флагами, что бывает полезно в админских скриптах.

▪️ Базовая логика. Каждый бит числа можно трактовать как флаг (0 - выключено, 1 - включено):


FLAG_READ=1 # 0001
FLAG_WRITE=2 # 0010
FLAG_EXEC=4 # 0100
FLAG_DELETE=8 # 1000


▪️ Установка флага (OR |)


rights=0
rights=$(( rights | FLAG_READ | FLAG_WRITE ))
echo $rights # 3 (0001 + 0010 = 0011)


▪️ Проверка флага (AND &)


if (( rights & FLAG_WRITE )); then
echo "Есть право записи"
fi


▪️ Сброс флага (AND + NOT &~)


rights=$(( rights & ~FLAG_READ ))
echo $rights # теперь без read


▪️ Инверсия (XOR ^)


rights=$(( rights ^ FLAG_EXEC )) # переключение: если был - уберется, если не был - включится


▪️ Практические кейсы
1️⃣ Система разрешений в скриптах. Можно задавать доступ к операциям через биты вместо кучи if:


if (( user_flags & FLAG_DELETE )); then
rm "$file"
fi


2️⃣ Упаковка нескольких состояний в одно число. Например, код статуса сервиса:


STATUS_RUNNING=1
STATUS_RESTARTING=2
STATUS_FAILED=4

status=$(( STATUS_RUNNING | STATUS_RESTARTING ))

(( status & STATUS_FAILED )) && echo "Ошибка!"


3️⃣ Быстрые флаги для CLI-опций


OPT_VERBOSE=1
OPT_DRYRUN=2
OPT_FORCE=4

opts=0
[[ $1 == "-v" ]] && opts=$(( opts | OPT_VERBOSE ))
[[ $1 == "-f" ]] && opts=$(( opts | OPT_FORCE ))

(( opts & OPT_VERBOSE )) && echo "[VERBOSE MODE]"


4️⃣ Флаги как компактная замена массивов. Вместо массива enabled_features=(...) можно хранить все в одной переменной. Это особенно ценно при передаче значений между процессами.

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10
Создание оффлайн инсталлятора из .deb зависимостей

Иногда сервер или рабочая машина не имеет прямого доступа в интернет. Но поставить нужный софт все же нужно. Решение: собрать локальный оффлайн инсталлятор из уже установленных пакетов и их зависимостей.

▪️ Установка dpkg-repack. На машине с интернетом:


sudo apt install dpkg-repack


▪️ Репак пакета в .deb. Если пакет уже стоит в системе:


dpkg-repack htop
# создаст htop_3.0.5-1_amd64.deb


▪️ Репак с зависимостями. Чтобы вытащить пакет + его зависимости:


mkdir offline-installer && cd offline-installer

# пример для nginx
for pkg in $(apt-cache depends --recurse --no-recommends --no-suggests \
--no-conflicts --no-breaks --no-replaces --no-enhances \
nginx | grep "^\w"); do
dpkg-repack "$pkg"
done


▪️ Итог - "папка-инсталлятор". В каталоге будут все нужные .deb:


ls *.deb
nginx_1.24.0-1_amd64.deb
libpcre3_2:8.39-13_amd64.deb
libssl1.1_1.1.1n-0+deb11u5_amd64.deb
...


▪️ Установка на оффлайн серваке. Переносим папку, потом:


sudo dpkg -i *.deb
sudo apt -f install # подтянет зависимости из локальных .deb


Можно превратить папку в локальный APT-репозиторий:


dpkg-scanpackages . /dev/null | gzip -9c > Packages.gz


и подключить его через sources.list.

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍13🔥2
Использование coproc для работы с асинхронными потоками

Многие знают про & и wait, но есть куда более элегантный инструмент для асинхронной работы - coproc. Это встроенная команда, которая запускает процесс в фоне и автоматически подключает к нему двусторонний канал (pipe).

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

coproc создает процесс, у которого:

stdin доступен через дескриптор ${COPROC[1]} (запись в процесс);
stdout доступен через ${COPROC[0]} (чтение из процесса);
имя coproc можно задавать, чтобы управлять несколькими одновременно.


▪️ Простейший пример


#!/bin/bash

# Запускаем фоновый bc как "асинхронный калькулятор"
coproc CALC { bc -l; }

# Отправляем в stdin команды
echo "2+3" >&"${CALC[1]}"
echo "s(1)" >&"${CALC[1]}"

# Читаем ответы из stdout
read -u "${CALC[0]}" result1
read -u "${CALC[0]}" result2

echo "Результат 1: $result1"
echo "Результат 2: $result2"


Процесс bc живет, пока вы его не закроете. То есть можно посылать команды и читать ответы несколько раз.

▪️ Практические кейсы
📍 Асинхронные задачи с обратной связью. Можно запускать ping, tcpdump или tail -f в coproc, а потом считывать поток строк построчно:


coproc PINGER { ping -O 8.8.8.8; }
while read -ru "${PINGER[0]}" line; do
echo "PING: $line"
done


📍 Фоновый обработчик. Например, пишем строки в процессинг-скрипт:


coproc HANDLER { while read line; do echo ">> $line"; done; }
echo "Hello" >&"${HANDLER[1]}"
echo "World" >&"${HANDLER[1]}"


📍 Несколько процессов. Coproc можно называть:


coproc P1 { ping -c2 8.8.8.8; }
coproc P2 { ping -c2 1.1.1.1; }


▪️ Подводные камни

Каналы буферизуются, иногда нужно sleep или stdbuf -oL для построчной работы.
Закрывайте дескрипторы exec {fd}>&-, иначе процесс может висеть.
coproc работает только в bash ≥ 4.


BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
Встраивание мини-HTTP-сервера на netcat + bash (для локальных API)

Иногда хочется быстро поднять легковесный API без Nginx/Apache, чтобы протестировать интеграцию или выдавать системные данные в JSON. Для этого достаточно bash + netcat.

Общая идея такова:

📍netcat слушает порт (например, 8080);
📍bash скрипт парсит запрос и отвечает заголовками + данными;
📍результат можно использовать для локальных API-запросов (например, мониторинг).


🛠 Минимальный пример


#!/bin/bash
PORT=8080

while true; do
# Принимаем одно соединение
{
# Читаем первую строку HTTP-запроса
read request
echo ">>> $request"

# Отправляем HTTP-ответ
echo -e "HTTP/1.1 200 OK\r"
echo -e "Content-Type: application/json\r"
echo -e "\r"
echo -e '{"status": "ok", "time": "'$(date +%T)'"}'
} | nc -l -p $PORT -q 1
done


▪️ Запуск


chmod +x mini-http.sh
./mini-http.sh


Теперь можно открыть в браузере: https://localhost:8080

▪️ Возможности

📍отдавать системные метрики:


echo -e '{"load": "'$(uptime | awk "{print \$10}")'"}'


📍простой healthcheck для Docker: curl localhost:8080/health
📍мини-API для локальных скриптов (например, статус бэкапа).

Такой подход позволяет собрать сверхлегкий REST-like API прямо из bash - без сторонних веб-фреймворков.

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7🔥5
systemd timers: сценарии, которые cron не умеет

Часто cron хватает для простого раз в день, но когда расписание становится хитрым - systemd timers выигрывают. Покажу несколько кейсов, которые на cron делать больно, а здесь - просто.

1️⃣ Только в рабочие дни, с 9:00 до 18:00. Хотим, чтобы скрипт мониторинга нагрузки запускался каждые 15 минут в будни, но не ночью и не в выходные: /etc/systemd/system/load-check.timer


[Timer]
OnCalendar=Mon..Fri *-*-* 09..18/15:00


Mon..Fri - только будни;
09..18/15:00 - каждые 15 минут с 9:00 до 18:00.

Аналог на cron выглядел бы как несколько строк с костылями.

2️⃣ Persistent=true - выполнение пропущенных задач. Задача должна выполняться раз в день, даже если сервер был выключен ночью.


[Timer]
OnCalendar=daily
Persistent=true


Если сервак в оффлайне, при старте systemd увидит пропущенное выполнение и запустит задачу.
У cron такого поведения нет - выключил машину, задача пропала.

3️⃣ Замороченные расписания - раз в час, но не в обед. Допустим, хотим запускать скрипт синхронизации каждые 60 минут, но исключить обеденный перерыв (с 12 до 13):


[Timer]
OnCalendar=Mon..Fri *-*-* 09..11:00,13..18:00


Здесь мы задали диапазоны часов с дыркой.

4️⃣ Несколько расписаний для одной задачи. Хочется иметь и ночной запуск (2:00), и дополнительный в пятницу вечером:


[Timer]
OnCalendar=*-*-* 02:00:00
OnCalendar=Fri *-*-* 19:00:00


В одном таймере можно указать несколько OnCalendar.

5️⃣ Контроль за пропусками и сбоями. Если критичный скрипт не отработал, мы хотим это видеть. Добавим OnFailure: /etc/systemd/system/backup.service


[Unit]
Description=Nightly backup job
OnFailure=alert.service


Если бэкап упадет, то сразу вызовется alert.service (например, отправка сообщения в телегу).

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥13👍7
Перехват stdout/stderr отдельных функций и подпрограмм

Обычно мы перенаправляем вывод глобально: myfunc >out.log 2>err.log. Но что, если нужно внутри скрипта гибко ловить stdout/stderr отдельных функций и даже подпрограмм, не ломая общий вывод? Тут могут помочь динамические файловые дескрипторы через exec {fd}>.

▪️ Пример 1. Логирование только stdout функции


logfile="stdout.log"

myfunc() {
echo "info: started"
echo "debug: internal"
echo "error: fail" >&2
}

exec {fd}> "$logfile" # создаем FD
myfunc 1>&$fd # stdout -> в файл, stderr остаётся на экран
exec {fd}>&- # закрываем FD


stdout (info, debug) уйдет в stdout.log;
stderr (error) появится в терминале.

▪️ Пример 2. Перехват stderr подпрограммы


errlog="stderr.log"

exec {fd}> "$errlog"
{ ls /root; echo "done"; } 2>&$fd
exec {fd}>&-


Ошибки от ls /root попадут в stderr.log, а «done» останется в терминале.

▪️ Пример 3. Отдельный канал для «отладки» внутри функции. Иногда хочется иметь третий тип вывода, помимо stdout/stderr.


exec {dbg}> debug.log # отдельный канал

mycalc() {
echo "42" # обычный результат
echo "step1 ok" >&$dbg
echo "step2 ok" >&$dbg
}

result=$(mycalc)
echo "result = $result"
exec {dbg}>&-


Так можно вести невидимый debug-лог параллельно с обычной работой.

▪️ Пример 4. Подмена stdout на время вызова


mycmd() { echo "normal out"; }

{
exec {fd}> redirected.log
mycmd >&$fd
exec {fd}>&-
}
echo "done"


Прием позволяет временно заменить stdout и потом вернуть его обратно.

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9
Множественные команды одной строкой

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

▪️ Пример с файлами


touch file-{1,2,3}.md


Создаст сразу три файла:


file-1.md
file-2.md
file-3.md


▪️ Полезные варианты

1️⃣ Диапазоны чисел


mkdir backup-{2022..2025}


Создаст папки:


backup-2022 backup-2023 backup-2024 backup-2025


2️⃣ Диапазоны букв


touch part-{a..d}.txt

part-a.txt part-b.txt part-c.txt part-d.txt


3️⃣ Комбинации (картезианское произведение)


echo {dev,staging,prod}-{us,eu,asia}

dev-us staging-us prod-us dev-eu staging-eu prod-eu dev-asia staging-asia prod-asia


4️⃣ Множественные команды


cp config.{yml,json} /etc/myapp/


Скопирует оба файла за раз.

▪️ Фишки для автоматизации

1️⃣ Генерация тестовых данных:


touch user-{001..100}.log


сразу 100 файлов

2️⃣ Быстрое клонирование директорий:


cp -r src{,-backup}


создаст копию src-backup без лишнего ввода.

3️⃣ Комбинации шаблонов:


mv report_{2022..2024}_{01..12}.csv /data/reports/


сразу раскладывание отчётов по месяцам и годам.

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14
Архивация только новых или измененных файлов

Как архивировать только измененные файлы без ведения отдельного списка и без сложных скриптов. Решение на самом деле простое - связка find + tar + gzip.

▪️ Проблема

Обычный tar -czf backup.tar.gz /data каждый раз сжимает все файлы → при больших каталогах это очень долго.
Хотелось бы архивировать только то, что изменилось за последний день/час.

▪️ Решение: find + tar


find /data -type f -mtime -1 -print0 | tar --null -czf backup-$(date +%F).tar.gz --files-from=-


-mtime -1 - ищет файлы, измененные за последние 24 часа
-print0 + --null - защита от пробелов в именах
--files-from=- - tar берет список файлов прямо из stdin

▪️ Вариант с часами (например, за 2 часа)


find /data -type f -mmin -120 -print0 | tar --null -czf backup-$(date +%F_%H%M).tar.gz --files-from=-


▪️ Инкрементальные архивы (только изменения с момента последнего бэкапа)


touch /tmp/last-backup
find /data -type f -newer /tmp/last-backup -print0 | \
tar --null -czf backup-$(date +%F).tar.gz --files-from=-
touch /tmp/last-backup


BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
Расширенные шаблоны

Есть фишки, про которые многие говорят: «я только недавно узнал, что так можно!» Одна из таких - расширенные шаблоны.

▪️ Включаем расширенные шаблоны


shopt -s extglob


Теперь доступны конструкции:

?(pattern) - 0 или 1 совпадение
*(pattern) - 0 или больше
+(pattern) - 1 или больше
@(pattern) - ровно одно из
!(pattern) - всё, кроме


▪️ Полезные примеры

1️⃣ Исключить расширение


for f in !(*.bak); do
echo "Обрабатываю $f"
done


Пробегаем по всем файлам, кроме .bak.

2️⃣ Сгруппировать несколько расширений


for img in *.@(jpg|png|gif); do
echo "Найдено изображение: $img"
done


Ловим только картинки, не заморачиваясь с длинными условиями.

3️⃣ Лестница через case


case $var in
+([0-9])) echo "Это число" ;;
?(http)://*) echo "Это URL" ;;
*.@(sh|bash)) echo "Это Bash-скрипт" ;;
*) echo "Что-то другое" ;;
esac


Можно строить почти регулярочные проверки, но быстрее и нагляднее.

4️⃣ Исключение нескольких типов


for f in !(*.log|*.tmp); do
echo "Чистый файл: $f"
done


Берём всё, кроме .log и .tmp.

5️⃣ Валидация формата прямо в if


if [[ $user == +([a-zA-Z0-9._-]) ]]; then
echo "Имя пользователя валидно"
else
echo "Некорректное имя"
fi


Короткая проверка без grep и regex.

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11
Сравнение конфигов на разных серверах через md5sum

▪️ Сравнение директорий по контрольным суммам


# На сервере A:
find /etc -type f -print0 \
| sort -z \
| xargs -0 md5sum > /tmp/etc-hash.txt

# На сервере B:
find /etc -type f -print0 \
| sort -z \
| xargs -0 md5sum > /tmp/etc-hash.txt


Затем можно скопировать файлы на локальный хост и сравнить:


diff -u serverA/etc-hash.txt serverB/etc-hash.txt


Отличия будут видны построчно: какие файлы отличаются и какие отсутствуют.

▪️ Более гибкий вариант

Хэшируем сразу несколько директорий и добавляем префикс хоста:


for h in server1 server2; do
ssh $h 'find /etc -type f -print0 \
| sort -z \
| xargs -0 md5sum' \
| sed "s|^|$h |"
done > all-hashes.txt


Теперь легко фильтровать:


awk '{print $2,$1}' all-hashes.txt | sort | uniq -c


Если файл совпадает на всех хостах - будет одинаковая хэш-сумма.
Если разный - сразу видно у какого сервера отличается.

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9
Копирование между хостами без SCP

Сегодня поделюсь с вами альтернативным способом копирования между хостами, когда SCP недоступен или файлы лежат в приватной сети.

▪️ Отправка каталога по SSH

На источнике:


tar cz dir_to_copy \
| base64 \
| ssh user@remote "base64 -d | tar xz"


dir_to_copy упаковывается в tar.gz, кодируется base64, передается по SSH и на удаленном хосте раскодируется и распаковывается.

▪️ Копирование через буфер (без SCP и SSH)

На источнике:


tar cz myfile.txt | base64


Копируем вывод в буфер и затем на приемнике:


base64 -d | tar xz


Вставляем скопированное содержимое и файл восстановится
Работает даже там, где SCP недоступен.

▪️ Обмен папками между хостами без сети

Через tmux buffer или screen можно передавать данные вообще без файлов и SSH, просто через терминал. Пример в tmux:

На сервере А:


tar cz project | base64 | tmux load-buffer -


На сервере Б:


tmux save-buffer - | base64 -d | tar xz


🌟 Для больших архивов можно добавить pv и тогда будет видно прогресс:


tar cz bigdir | pv | base64 | ssh host "base64 -d | tar xz"


BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11🗿1
PROMPT_COMMAND

Недавно на форуме arch linux увидел такую штуку:


export PROMPT_COMMAND='[[ $curdir != $PWD ]] && ls -a; curdir=$PWD'


при каждом входе в новую директорию автоматически вызывается ls -a.

А все дело в переменной окружения PROMPT_COMMAND - bash выполняет ее перед выводом нового приглашения. Если разобраться, можно превратить её в инструмент автоматизации.

▪️ Базовые сценарии

📍 Автолистинг при смене директории


PROMPT_COMMAND='[[ $lastdir != $PWD ]] && ls; lastdir=$PWD'


ls срабатывает только если реально поменяли директорию.

📍 Показ текущей git-ветки


PROMPT_COMMAND='branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null); PS1="[\u@\h \W${branch:+ ($branch)}]\\$ "'


приглашение (PS1) динамически обновляется: добавляет (ветка) только в репозитории.

📍Отображение количества фоновых задач


PROMPT_COMMAND='jobs_count=$(jobs -p | wc -l); PS1="[jobs:$jobs_count] \u@\h:\w\$ "'


рядом с промптом видим, сколько задач выполняется в фоне.

▪️ Продвинутые хаки

📍 Время последней команды


PROMPT_COMMAND='PS1="[\t] \u@\h:\w\$ "'


каждый раз в начале промпта вставляется текущее время.

📍 Звуковой сигнал при ошибке


PROMPT_COMMAND='[ $? -ne 0 ] && echo -ne "\a"'


если последняя команда завершилась ошибкой (exit code != 0), терминал пищит.

📍История + логирование всех команд


PROMPT_COMMAND='history -a; history -c; history -r'


история сохраняется сразу (не только при выходе из shell), что полезно при одновременных сессиях.

▪️ Комбо вариант


PROMPT_COMMAND='
[[ $lastdir != $PWD ]] && ls; lastdir=$PWD;
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null);
jobs_count=$(jobs -p | wc -l);
PS1="[jobs:$jobs_count ${branch:+git:$branch}] \u@\h:\w\$ "
'


- при смене директории автоматом делается ls
- в промпте видно количество фоновых задач
- в git-репозитории показывается текущая ветка

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10
Компактный контроль фоновых процессов

При работе с фоновыми процессами в скриптах часто возникает два вопроса:

1. Как реагировать на завершение первого из них, не дожидаясь всех?
2. Как корректно снять всю пачку, если что-то пошло не так?

В Bash на этот случай имеется удобный механизм: wait -n и управление группами процессов.

▪️ Проблема: несколько фоновых задач. Допустим, мы запускаем несколько параллельных процессов:


#!/bin/bash
long_task_1 &
long_task_2 &
long_task_3 &


Обычно wait ждет завершения всех процессов. Но что если нужно отреагировать на первый фейл или отменить остальных после успешной задачи?

Решение: wait -n. Команда wait -n ждет завершения любого из фоновых процессов и возвращает его код выхода. Как пример: запускаем 3 задачи, как только одна успешно завершилась - гасим остальные:


#!/bin/bash
cmd1 & p1=$!
cmd2 & p2=$!
cmd3 & p3=$!

while true; do
if wait -n; then
echo "Одна из задач завершилась успешно, убиваем остальные..."
kill $p1 $p2 $p3 2>/dev/null
break
fi
done


▪️ Kill group: гасим всю пачку. У kill есть прием: если передать отрицательный PID, сигнал уйдет всей группе процессов.


# запустить группу
( cmd1 & cmd2 & cmd3 & wait ) &

# сохранить PID группы
PGID=$!

# ... позже убить всех одним движением:
kill -- -$PGID


Это удобно, если внутри группы запускаются дочерние процессы, которые обычный kill $PID не заденет.

▪️ Комбо. Представим сценарий: несколько загрузчиков качают один и тот же файл из разных зеркал. Как только первый скачал успешно - убиваем остальных.


#!/bin/bash
(
curl -s -O https://mirror1/file.iso &
curl -s -O https://mirror2/file.iso &
curl -s -O https://mirror3/file.iso &
wait -n # ждем первый завершившийся процесс
kill -- -$$ # убиваем всю группу (все ещё живые curl)
) &
wait
echo "Файл получен!"


BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
Автообновление python-зависимостей в виртуальных окружениях

В проектах на python у меня неоднократно возникала ситуация: виртуальное окружение развернуто давно, а пакеты внутри уже устарели.
Чтобы не держать руками список и не забывать про обновления, можно автоматизировать этот процесс.

1️⃣ Получение списка пакетов. В активированном окружении pip list --outdated --format=freeze вернет пакеты, которые можно обновить:


source venv/bin/activate
pip list --outdated --format=freeze


2️⃣ Скрипт автообновления. Скрипт ищет все устаревшие пакеты и обновляет их до последней версии:


#!/bin/bash
set -euo pipefail

VENV_PATH="./venv"
source "$VENV_PATH/bin/activate"

echo "Проверяем устаревшие пакеты..."
outdated=$(pip list --outdated --format=freeze | cut -d= -f1)

if [[ -z "$outdated" ]]; then
echo "Все пакеты актуальны"
exit 0
fi

echo "Обновляем пакеты:"
for pkg in $outdated; do
echo " - $pkg"
pip install -U "$pkg"
done


3️⃣ Массовое обновление для проектов. Если проектов несколько, можно пройтись по каталогам и обновить все:


#!/bin/bash
for dir in ~/projects/*; do
if [[ -d "$dir/venv" ]]; then
echo "=== $dir ==="
source "$dir/venv/bin/activate"
pip list --outdated --format=freeze | cut -d= -f1 | xargs -n1 pip install -U
deactivate
fi
done


4️⃣ Фиксируем версии. Перед обновлением полезно сохранить текущие версии, чтобы в случае проблем легко откатиться:


pip freeze > requirements-$(date +%F).txt


BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
Создание резервных копий БД с шифрованием

Правильный подход к шифрованию БД, чтобы не оставлять персональные данные открытыми это: создать дамп → зашифровать его GPG → сохранить/отправить.

▪️ Подготовка GPG. На сервере, который будет создавать бэкапы, импортируйте публичный ключ:


gpg --import backup_public.key


Проверьте наличие ключа:


gpg --list-keys


Использовать будем шифрование public key → private key, так что дешифровать можно только на безопасном ПК/сервере.

🛠 Скрипт универсального бэкапа PostgreSQL/MySQL с шифрованием GPG


#!/usr/bin/env bash
set -euo pipefail

# === Настройки ===
BACKUP_DIR="/var/backups/db"
RETENTION_DAYS=7
GPG_RECIPIENT="[email protected]" # email из публичного ключа
TS=$(date +"%F_%H-%M-%S")

DB_TYPE="$1" # postgres | mysql
DB_NAME="$2" # имя базы
DB_USER="$3" # пользователь

mkdir -p "$BACKUP_DIR"

DUMP_FILE="$BACKUP_DIR/${DB_TYPE}_${DB_NAME}_${TS}.sql"
ENC_FILE="$DUMP_FILE.gpg"
LOG_FILE="$BACKUP_DIR/backup.log"

# === Логгер ===
log() {
echo "[$(date +%F_%T)] $*" | tee -a "$LOG_FILE"
}

log "=== Старт резервного копирования $DB_TYPE:$DB_NAME ==="

# === Дамп базы ===
case "$DB_TYPE" in
postgres)
log "Выполняю pg_dump…"
pg_dump -U "$DB_USER" "$DB_NAME" > "$DUMP_FILE"
;;
mysql)
log "Выполняю mysqldump…"
mysqldump -u "$DB_USER" "$DB_NAME" > "$DUMP_FILE"
;;
*)
log "Неизвестный тип БД: $DB_TYPE"
exit 1
;;
esac

log "Дамп создан: $DUMP_FILE"

# === Шифрование GPG ===
log "Шифрую дамп для получателя: $GPG_RECIPIENT"
gpg --yes --encrypt --recipient "$GPG_RECIPIENT" "$DUMP_FILE"

log "Шифрованный файл: $ENC_FILE"

# === Удаление оригинального дампа ===
rm -f "$DUMP_FILE"
log "Оригинальный файл удалён"

# === Очистка старых бэкапов ===
log "Удаление файлов старше $RETENTION_DAYS дней"
find "$BACKUP_DIR" -type f -name "*.gpg" -mtime "+$RETENTION_DAYS" -delete

log "Готово"


Скрипт:

делает бэкап PostgreSQL или MySQL;
шифрует его GPG-ключом;
сохраняет с датой;
удаляет старые копии.
логирует все происходящее


▪️ Запуск

PostgreSQL:


./backup.sh postgres mydb dbuser


MySQL/MariaDB:


./backup.sh mysql mydb root


▪️ Как восстановить?


gpg -d backup_postgres_mydb_2025-11-01_03-00-00.sql.gpg > restore.sql


PostgreSQL:


psql -U postgres mydb < restore.sql


MySQL:


mysql -u root mydb < restore.sql


BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9
Использование printf -v для динамического формирования переменных

Бывает, что нужно создавать или заполнять переменные с динамическими именами: item_1, item_2, user_alice и т.д. Вместо eval попробуем использовать встроенный printf -v, который позволяет записать форматированный строковый результат прямо в переменную по имени.

Почему printf -v лучше, чем eval?

безопаснее (без спаивания и выполнения произвольного кода),
дает форматирование (числа, нули, float),
удобно в функциях (можно создавать глобальные переменные через declare -g).


▪️ Базовый пример: простая запись


name="user_alice"
printf -v "$name" "%s" "id=42"
echo "$user_alice"

id=42


Здесь мы записали строку в переменную user_alice без eval.

▪️ Нумерация с форматированием (zero-padding)


for i in {1..5}; do
printf -v "item_%03d" "$i" "val_$i"
done

# показать созданные переменные
for i in {1..5}; do
var="item_$(printf '%03d' "$i")"
echo "$var = ${!var}"
done

item_001 = val_1
item_002 = val_2


printf -v "item_%03d" "$i" "val" - формат имени и значение удобно комбинировать.

▪️ Создаем набор конфигов из CSV (пример)


while IFS=, read -r key value; do
# нормализовать имя (латиница, цифры, _)
safe_key=$(echo "$key" | tr -c '[:alnum:]_' '_')
printf -v "cfg_%s" "$safe_key" "%s" "$value"
done <<'CSV'
db.host,localhost
db.port,5432
feature.enable,true
CSV

echo "Host: $cfg_db_host"
echo "Port: $cfg_db_port"
echo "Feature: $cfg_feature_enable"


▪️ Инспекция созданных переменных


# показать все переменные с префиксом cfg_
for v in ${!cfg_*}; do
echo "$v = ${!v}"
done


(indirect expansion) позволяет получить значение переменной по имени.

▪️ Комбинация с declare -n (nameref)


printf -v "user_alice" "%s" "[email protected]"
ref_var="user_alice"
declare -n userref="$ref_var"
echo "Email via nameref: $userref"


declare -n дает ссылку на переменную, это удобнее, чем постоянно подставлять ${!name}.

▪️ Как избежать проблем с именами переменных. Переменная должна соответствовать правилам имени в bash. Можно валидировать:


sanitize() {
printf '%s' "$1" | sed 's/[^a-zA-Z0-9_]/_/g'
}

raw="user-name@host"
safe=$(sanitize "$raw")
printf -v "$safe" "%s" "value"


Если допустить в имени пробелы или символы ; - printf -v либо даст ошибку, либо поведет себя непредсказуемо.

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6