Bash Days | Linux | DevOps
24K subscribers
202 photos
26 videos
747 links
Авторский блог от действующего девопса

Самобытно про разработку, devops, linux, скрипты, сисадминство, техдирство и за айтишную жизу.

Автор: Роман Шубин
Реклама: @maxgrue

MAX: https://max.ru/bashdays

Курс: @tormozilla_bot
Блог: https://bashdays.ru
Download Telegram
Сопроцессы. Практика. Часть Вторая.

🔤🔤🔥🔤🔤🔤🔤

coproc хорошо подходит для общения в клиент-серверном режиме. Для примера попробуем подключиться к POP3 серверу с шифрованием ssl прямо из bash-скрипта.

Сам ssl несколько сложноват для bash, поэтому в качестве посредника будем использовать openssl s_client.

Протокол и команды POP3 лучше посмотреть на википедии.

1. Cоздим сопроцесс. Для этого запустим openssl в режиме s_client. При этом из дескриптора POP3_CONN[0] можно читать данные от сопроцесса.

В дескриптор POP3_CONN[1] можно писать для сопроцесса.

При записи используем перенаправление >&${POP3_CONN[1] . При чтении тоже можно использовать перенаправление, но поскольку у команды read есть ключ -u красивее воспользоваться им.

2. Аутентифицируемся

3. Закроем сессию и дескрипторы.

# Функция для отправки команд серверу
function SEND_CMD() {
sleep 0.3
echo "$@" >&${POP3_CONN[1]}
sleep 0.3
}

# аутентификация. Обычный логин
function POP3_LOGIN() {
declare REC
declare -a AREC
# проверка соединения
read -ert 2 -u ${POP3_CONN[0]} REC
read -ra AREC <<<${REC//$'\r'/}
if [[ "${AREC[0]}" == "+OK" ]];then
# Отправляем логин
SEND_CMD "USER $USER"
read -ert 2 -u ${POP3_CONN[0]} REC
read -ra AREC <<<${REC//$'\r'/}
if [[ "${AREC[0]}" == "+OK" ]];then
# Отправляем пароль
SEND_CMD "PASS $PASS"
read -ert 2 -u "${POP3_CONN[0]}" REC
read -ra AREC <<<${REC//$'\r'/}
if [[ "${AREC[0]}" == "+OK" ]];then
return 0 # аутентификация успешна
else
return 3 # не правильный пароль
fi
else
return 2 #не правильный login
fi
else
return 1 # ошибка соединения с сервером
fi
}

#Выход и закрытие дескрипторов.
function POP3_QUIT(){
SEND_CMD "QUIT"
# Закрываем coproc
exec ${POP3_CONN[0]}<&-
exec ${POP3_CONN[1]}>&-
}


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

Ошибки -ERR не обрабатывал. В случае чего команда read завершится по таймауту в 2 сек. (-t 2)

${REC//$'\r'/} конструкция удаляет cr, потому что POP3 сервер отвечает c lfcr.


#!/bin/bash

SERVER="server"
PORT=995
USER="user@server"
PASS="StrongPass"

# создаем сопроцесс и соединяемся с сервером pop3
coproc POP3_CONN { openssl s_client -connect "${SERVER}:${PORT}" -quiet 2>/dev/null;}
POP3_LOGIN
POP3_QUIT


help coproc
help read
man openssl
вики POP3

🛠 #bash #linux

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
221
Сопроцессы. Практика. Часть Третья.

🔤🔤🔥🔤🔤🔤🔤

Это уже больше не сопроцессы, а про то, как принять почту в скрипте bash.

Соединение с POP3 сервером есть. Аутентификация тоже. Осталось написать что-нибудь полезное.

# возвращает число писем в ящике
function POP3_STAT(){
declare -a AREC
declare REC
SEND_CMD "STAT"
read -ert 2 -u ${POP3_CONN[0]} REC
read -ra AREC <<<${REC//$'\r'/}
if [[ ${AREC[0]} == "+OK" ]];then
echo ${AREC[1]} # число сообщений
return 0
else
echo 0
return 1
fi
}
#Помечает к удалению указанное письмо
function POP3_DELE(){
declare -i MSG_NUM=${1:-1} # по умолчанию первое
declare -a AREC
declare REC
SEND_CMD "DELE $MSG_NUM" #удаляем указанное сообщение
read -ert 2 -u ${POP3_CONN[0]} REC
read -ra AREC <<<${REC//$'\r'/}
if [[ ${AREC[0]} == "+OK" ]];then
return 0
else
return 1
fi
}
# читает письмо с заголовками
function POP3_RETR(){
declare -i MSG_NUM=${1:-1} # по умолчанию первое
declare -a AREC
declare REC
SEND_CMD "RETR $MSG_NUM" #читаем указанное сообщение
read -ert 2 -u ${POP3_CONN[0]} REC
read -ra AREC <<<${REC//$'\r'/}
if [[ ${AREC[0]} == "+OK" ]];then
while read -r -t 2 -u ${POP3_CONN[0]} REC ; do
REC=${REC//$'\r'/}
echo "$REC"
if [[ "$REC" == "." ]];then
return 0 # msg end
fi
done
else
return 1
fi
}
# читает указанное число строк письма
function POP3_TOP(){
declare -i MSG_NUM=${1:-1} # по умолчанию первое
declare -i STR_NUM=${2:-1} # по умолчанию одна строка
declare -a AREC
declare REC
#читаем указанное сообщение
SEND_CMD "TOP $MSG_NUM $STR_NUM"
read -ert 2 -u ${POP3_CONN[0]} REC
read -ra AREC <<<${REC//$'\r'/}
if [[ ${AREC[0]} == "+OK" ]];then
while read -ert 2 -u ${POP3_CONN[0]} REC ; do
REC=${REC//$'\r'/}
echo "$REC"
if [[ "$REC" == "." ]];then
return 0
fi
done
else
return 1
fi
}

Финальный код
#!/bin/bash

SERVER="server"
PORT=995
USER="user@server"
PASS="StrongPass"

coproc POP3_CONN { openssl s_client -connect "${SERVER}:${PORT}" -quiet 2>/dev/null;}

POP3_LOGIN && echo POP3_LOGIN OK
MSG_NUM=$(POP3_STAT)
#цикл перебора сообщений
while ((MSG_NUM));do
POP3_TOP $MSG_NUM 1 # Заголовки + 1 строку сообщения
# POP3_RETR $MSG_NUM # сообщения целиком
# POP3_DELE $MSG_NUM # помечаем к удалению.
((--MSG_NUM))
done

POP3_QUIT


help coproc
help read
man openssl

🛠 #bash #linux

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
20
Хочешь устроить себе челлендж на знание командной строки? Да пожалуйста, лови тренажер по Linux-терминалу. Правда он на английском, но мы с тобой тоже не пальцем деланные.

Тренажер содержит 77 вопросов. Вполне достаточно чтобы заебаться.


Я даж успешно прошел первую задачку, правда по-олдскульному и затронул все бед-практики, которые только существуют.

Установка простая:

cd /tmp
python3 -m venv textual_apps
cd textual_apps
source bin/activate
pip install cliexercises
cliexercises


Здесь я использовал tmp папку и venv, чтобы систему всяким гавно не засорять, а чисто поиграться.

Что прикольно, если совсем тупой, то можно получить подсказку, будет показано несколько решений. Подсказка открывается по CTRL+S.

Сможешь выбить все 77 вопросов без подсказок?

Исходники этого тренажера лежат тут, а видосик с работой можешь посмотреть тут.

🛠 #linux #bash

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
364
Мне прилетела интересная задача.

🔤🔤🔥🔤🔤🔤🔤

ТЗ: Сервер без GUI, к нему подключают usb-диск с NTFS и начинается автоматическое архивирование. Логин не требуется, сообщения выдаются на tty1.

sudo apt install ntfs-3g # для монтирования ntfs


Использовать будем штатные средства udev + systemd .

1. Подключаем диск, и запоминаем UUID.

lsblk -f


sdc                                                                         
└─sdc1
ntfs FLASH
1234567890ABCDEF

здесь FLASH - метка диска 1234567890ABCDEF - UUID

создадим каталог для монтирования:

mkdir -p /media/usb_ntfs; chmod 777 /media/usb_ntfs


Дальше создаем 4 файла. Чтобы было проще, в каждом файле в начале коммент с названием файла.

После создания:

sudo systemctl daemon-reload


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

При подключении диска usb-диска срабатывает UDEV правило 99-usb-mount.rules и пытается запустить службу autousbbackup.service

autousbbackup.service пытается запустить media-usb_ntfs.mount, поскольку жестко он нее зависит.

Сама media-usb_ntfs.mount запустится только в том случае, если UUID диска будет 1234567890ABCDEF.

Если все условия совпадают, autousbbackup.service запустит скрипт autousbbackup.sh, внутри которого Вы напишите копирование или синхронизацию данных в каталог /media/usb_ntfs.

Если используется архивирование с чередованием дисков - просто сделайте у дисков одинаковые метки:

sudo umount /dev/sdXN # где /dev/sdXN  имя вашего NTFS-раздела.
sudo ntfslabel --new-serial=1234567890ABCDEF /dev/sdXN #задайте UUID


👆 Если в mount не указать опцию nofail система будет тормозить при загрузке.
👆 Запустить скрипт через UDEV даже в фоне не получится, поскольку система вырубит его через 5 сек.

#/etc/udev/rules.d/99-usb-mount.rules

SUBSYSTEM=="block", KERNEL=="sd*", ACTION=="add", TAG+="systemd", ENV{SYSTEMD_WANTS}="autousbbackup.service"


#/etc/systemd/system/media-usb_ntfs.mount

[Unit]
Description=Mount USB NTFS by UUID

[Mount]
What=/dev/disk/by-uuid/1234567890ABCDEF
Where=/media/usb_ntfs
Type=ntfs-3g
Options=defaults,big_writes,nofail,uid=1000,gid=1000

[Install]
WantedBy=multi-user.target


#/etc/systemd/system/autousbbackup.service

[Unit]
Description=Simple service autousbbackup
Requires=media-usb_ntfs.mount
After=media-usb_ntfs.mount

[Service]
Type=simple
ExecStart=/root/work/autousbbackup/autousbbackup.sh

[Install]
WantedBy=multi-user.target


#!/bin/bash
#/root/work/autousbbackup/autousbbackup.sh

declare -x PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
declare MOUNT_POINT=/media/usb_ntfs
declare TTY=/dev/tty1
declare DF='%(%Y%m%d-%H%M%S)T' #date format
declare LOG="$0.log"
exec &>"$LOG" # only one backup log
#exec &>>"$LOG" # all backups log
printf "$DF %s\n" -1 "START BACKUP"|tee "$TTY"
##############################################

sleep 3 # backup emulation

##############################################linux
printf "$DF %s\n" -1 "END BACKUP"|tee "$TTY"

#suicide, because service require mount point
systemd-mount --umount "$MOUNT_POINT"


🛠 #linux #bash

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
680
Смотри какая лялька: ExplainShell

Сервис помогает понять, что делает shell команда и все её параметры и ключи. Вставляешь например команду из прошлого поста:

strace -s 200 -f -e trace=network,recvfrom task sync


И получаешь по ней полный разбор.

Работает достаточно просто, под капотом овер-дохуя ≈30к-man страниц. Штука оупенсорцная и лежит тут.

Логика работы:

1. Ман-страницы (разделы 1 и 8) загружаются и преобразуются в HTML.

2. Параграфы классифицируются – разделяются те, где описаны опции/флаги, и те, где нет.

3. Из отобранных параграфов извлекаются конкретные параметры и их описания.

4. Когда ты вводишь команду, она разбирается на синтаксическое дерево (AST) с помощью библиотеки bashlex.

5. Компоненты команды («узлы» AST) сопоставляются с параметрами, найденными в ман-страницах.

6. Отображаем на фронте.

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

Хотя, кого я обманываю, сейчас каждый первый загоняет непонятную команду в GPT и оно тебе всё по полочкам раскладывает. Да еще и на русском языке.


Ладно, глядишь сгодиться в хозяйстве.

🛠 #services #bash

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
271
Довольно часто нужно быстро загрузить файл построчно в массив.

Как делает мальчик:

lines=()
while IFS= read -r line; do
lines+=("$line")
done < file.txt

echo "Первая строка: ${lines[0]}"
echo "Всего строк: ${#lines[@]}"


Как делает мужчина:

mapfile -t lines < file.txt

echo "Первая строка: ${lines[0]}"
echo "Всего строк: ${#lines[@]}"


В первом варианте много кода, НО, работает везде. Во втором варианте, работает только в bash ≥4.0, но кода в разы меньше и не жрет CPU.

Совет: если пишешь скрипт под bash — всегда используй mapfile. Если нужен кросс-шелл (sh,dash,ash) — оставайся на цикле.


Либо расширь второй вариант и укажи:

#!/usr/bin/env bash


Это гарантирует, что твой скрипт выполнится именно через bash, а не через системный sh (который может быть dash, ash, ksh и т.п.).

env ищет bash в $PATH, так что это более переносимо, чем жёстко указывать #!/bin/bash

Ну и прицепом можешь добавить: set -euo pipefail

Это включение «строгого режима» в баше:

-e — выход из скрипта при любой ошибке (не игнорировать exit code ≠ 0).

-u — ошибка при обращении к неинициализированной переменной (не будет пустых значений «по-тихому»).

-o pipefail — пайплайны возвращают код ошибки первой упавшей команды, а не последней.

По итогу:

- Скрипт точно запустится под bash
- Ошибки не будут замалчиваться
- Сразу ловишь косяки

Удобно в CI/CD, где всё должно падать быстро и без хуйни.

grep foo file.txt | wc -l
echo $? # 0, даже если grep ничего не нашёл


set -o pipefail
grep foo file.txt | wc -l
echo $? # 1, потому что grep ушел по пизде


Такие дела, изучай.

🛠 #bash

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
172
Вопрос с LF, думаю всем полезно будет.

Пишу пайплайн, есть такая конструкция:

platform=$(uname -m)
docker run --platform linux/arm64


Я бы хотел, чтобы параметр --platform linux/arm64 подставлял в том случае, если архитектура платформы == arm64, в других случаях этот параметр подставлять не нужно.

Делаю так, но получается какая-то хуйня:

platform=$(uname -m)
docker run ${platform/arm64/--platform linux/arm64}


Короче смотри. Задачку можно решить несколькими способами.

Способ 1. Заточен под Bash:

platform=$(uname -m)
docker run $([[ $platform = arm64 ]] && echo "--platform linux/arm64")


Этот способ не сработает, если код запустится в /bin/sh, потому что оно не POSIX. Тут уже смотри под чем все это будет запускаться.

Тут можешь почитать чем отличаются [[ ]] от []
А тут что такое POSIX


Способ 2. Универсальный:

platform=$(uname -m)
docker run $( [ "$platform" = "arm64" ] && echo "--platform linux/arm64" )


На 100 % POSIX-совместим. По функционалу эти способы идентичны.

Ну и по итогу имеем:

[[ ... ]] — это расширенная bash-версия, безопаснее и гибче (поддержка pattern, безопасность кавычек при пробелах и пустых строках, поддежка логики с &&)

[ ... ] — POSIX-совместимая, более строгая и требующая аккуратности с кавычками.

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

🛠 #bash #pipeline

💬 Bashdays 📲 MAX 🌐 LF 🔵 Blog
Please open Telegram to view this post
VIEW IN TELEGRAM
148
Туалетная бумага по cron’у

Я короче каждый месяц заказываю на WB туалетную бумагу, сразу беру 32 рулона. В чатике ссылки как-то скидывал на этот товар.

И в сентябре меня эта рутина — заебала! Основная проблема — я забываю вовремя пополнять запасы. И после всех туалетных процедур приходится сидеть и ждать несколько часов пока мой бэкенд подсохнет и остатки артефактов сами отвалятся.

Решено было накидать Bash скрипт, который каждый месяц будет заказывать на WB радость для моей жопы.


Открытой API у WB нет, но реверс-инженерию никто не отменял. Открываем в браузере режим разработчика, натыкиваем нужные действия на сайте и потом смотрим вкладку Network. Выбираем нужные запросы и рожаем скрипт.

Концепт скрипта:

1. Авторизация. С ней я не стал заморачиваться, авторизовался на сайте, выгрузил кукисы в файл. Полюбому можно и это автоматизировать через curl, но чёт пока лень.

2. Скрипт принимает единственный параметр $1, это ссылка на товар WB, мало ли бумага моя закончится и придется другого поставщика искать.

3. Дальше curl подгружает кукисы из файла, и делает несколько запросов к сайту, добавляет бумагу в корзину и оформляет заказ.

Пример добавления в корзину:

RESPONSE=$(curl -s "https://cart-storage-api.wildberries.ru/api/basket/sync?ts=$TS&device_id=$DEVICE_ID" \
-X POST \
-H 'content-type: application/json' \
-H 'origin: https://www.wildberries.ru' \
-b "$COOKIES_FILE" \
--data-raw "$JSON_PAYLOAD"
)


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

Говнокод конечно лютый получился, но работает. А это главное в нашем деле!

4. Ну и всё, в телегу мне приходит уведомление, что заказ оформлен, бабло спишут по факту. Ну и пишут примерное число доставки.

5. Как только товар пришел в пункт выдачи, получаю уведомление в телегу. Аналогично curl запросом проверяется статус в личном кабинете.

6. Закидываем скрипт в крон и забываем про эту рутину.

59 23 25 * * /usr/local/sbin/shit_paper.sh https://wb.ru/296/detail.aspx


Вот и вся наука. Теперь когда я возвращаюсь из гаража, захожу по пути в пункт выдачи и забираю свои 32 рулона. Удобно? Да охуенно!

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


Еще бы научить чтобы чайник сам в себя воду наливал, было бы вообще ништяк.

Давай, увидимся! Хороших тебе предстоящих выходных и береги себя!

🛠 #bash #devops

💬 Bashdays 📲 MAX 🌐 LF 🔵 Blog
Please open Telegram to view this post
VIEW IN TELEGRAM
12298
А у нас сегодня на повестке интересная «темка» — Автоматическая очистка tmp-файлов даже после SIGTERM.

Как говорится — Волков бояться, в лесу не ебаться!


Пишем подопытный скрипт:

#!/usr/bin/env bash
set -e

temp=$(mktemp)
echo "Создал файл: $temp"

false

echo "Удаляю файл"
rm -f "$temp"


Чмодим, запускаем, ага скрипт упал на false, но успел создать временный файл в папке /tmp == /tmp/tmp.oWQ0HiYxSV. И он не удалился. Скрипт вернул статус == 1;

Код выходы проверить так:

echo "код: $?"


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

Снова изобретать велосипед? Неа! Всё это работает из коробки.

#!/usr/bin/env bash
set -e

temp=$(mktemp)
echo "Создал файл: $temp"

trap 'echo "Удаляю $temp"; rm -f "$temp"' EXIT

false


В этом случае скрипт также вернет код 1, но временный файл будет удалён при любом раскладе.

Нихуя непонятно, но очень интересно.

Смотри — trap вызывает rm ПРИ ВЫХОДЕ ИЗ СКРИПТА. И похуй с каким статусом был завершен скрипт. Временный файл будет гарантированно удалён.

Давай подключим strace:

strace -e trace=process,signal bash -c '
tmp=$(mktemp)
trap "rm -f $tmp" EXIT
echo "tmp=$tmp"
'


В выхлопе видим:

openat(AT_FDCWD, "/tmp/tmp.Dn2qG3uK9V", O_RDWR|O_CREAT|O_EXCL, 0600) = 3
rt_sigaction(SIGINT, {sa_handler=0x...}, NULL, 8) = 0
rt_sigaction(SIGTERM, {sa_handler=0x...}, NULL, 8) = 0
unlink("/tmp/tmp.Dn2qG3uK9V") = 0
exit_group(0) = ?


openat(... O_EXCL) — это mktemp, создающий файл атомарно
rt_sigaction()Bash ставит ловушки для сигналов
unlink() — это и есть выполнение trap
exit_group(0) = ? — завершает процесс

То есть Bash вызывает rm/unlink() ДО выхода из процесса.

Почему trap срабатывает?

Если скрипт упал или получил сигнал kill -TERM <pid>, то на уровне Bash:

— в обработчике сигнала вызывается registered trap
— затем Bash падает/выходит
trap УЖЕ успел выполнить команду очистки

trap выполняется ДО того, как процесс отдаст SIGTERM ядру, если только ловушка для этого сигнала была установлена.

Да банально при выполнении скрипта ты нажал CTRL+C, скрипт остановился, НО временные файлы сразу подчистились.

➡️ Про exit codes (коды выхода) писал тут.
➡️ Про сигналы писал ранее тут и тут и тут и тут.


Вот такие пироги!

С пятницей и хороших тебе предстоящих выходных!

🛠 #bash #debug #strace

💬 Bashdays 📲 MAX 🌐 LF 🔵 Blog
Please open Telegram to view this post
VIEW IN TELEGRAM
559
Сегодня затронем довольно сложную тему bash: Использование групп в регулярных выражениях.

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

🔤🔤🔤🔤🔤🔤🔤

Приведу простой пример:

#!/bin/bash

declare PREF="FILE"
#создаем 10 файлов с "плавающим" префиксом
for FILE in "202512"{01..10};do
touch ${PREF:$RANDOM%4}${FILE}".txt"
done

# Выбираем только файлы с префиксом из двух или трех символов и "даты"
for FILE in *;do
if [[ $FILE =~ (.{2,3})([0-9]{4})([0-9]{2})([0-9]{2}) ]];then
# echo $FILE
declare -p BASH_REMATCH
fi
done


Первый for создает 10 тестовых файлов c «случайным» префиксом (1-4 последние буквы слова FILE).

Второй for — основной выбирает нужные нам файлы. А что именно нам нужно — задает регулярное выражение.

Разберем подробнее:

(.{2,3})([0-9]{4})([0-9]{2})([0-9]{2})


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

Напомню:

. - заменяет любой символ, кроме перевода строки
{2,3} - определяет количество захватываемых символов
[0-9] - любой символ в диапазоне 0-9
() - захватываемая группа


Первая группа (.{2,3}) — Захватывает от 2 до 3 любых символов {2,3} указывает минимальное и максимальное количество.​

Вторая группа ([0-9]{4}) — Захватывает ровно 4 цифры (0–9)​.

Третья и четвёртая группы ([0-9]{2})([0-9]{2}): Каждая захватывает ровно 2 цифры.​

➡️ И теперь самое главное:

Если строка (в данном случае имя файла) соответствует regexp - захваченный результат помещается в массив BASH_REMATCH с индексом 0

Все остальные группы помещаются в следующие элементы массива. Получаем что-то типа:

declare -a BASH_REMATCH=([0]="LE20251204" [1]="LE" [2]="2025" [3]="12" [4]="04")
...


По итогу — с помощью одного if мы получили: Префикс файла (1), год (2), месяц (3) и день (4).

Ну, на всякий случай напомню, если вдруг решите дни или месяцы использовать в математических операциях — придется избавиться от ведущих нулей, поскольку числа, начинающиеся на «0» bash считает восьмеричными.

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

Всем кода без багов.

🛠 #bash

💬 Bashdays 📲 MAX 🌐 LF 🔵 Blog
Please open Telegram to view this post
VIEW IN TELEGRAM
49
Привет, сегодня будем учить zsh автоматически перечитывать конфиг после изменения.

Каждый раз заёбисто делать source ~/.zhsrc после очередных изменений, да и плагинов я актуальных найти так и не смог.

По-хорошему можно было бы и плагин для сообщества накидать, но я ленивая скотина и обошелся Bash скриптом.


Поэтому открываем ~/.zshrc и пихаем в него такое:

ZSH_LAST_MOD=$(stat -c %Y ~/.zshrc 2>/dev/null || stat -f %m ~/.zshrc)
precmd() {
local new_mod=$(stat -c %Y ~/.zshrc 2>/dev/null || stat -f %m ~/.zshrc)
if [[ $new_mod != $ZSH_LAST_MOD ]]; then
if zsh -n ~/.zshrc; then
source ~/.zshrc
ZSH_LAST_MOD=$new_mod
echo "🔄 .zshrc auto-reloaded (OK)"
else
echo "⚠️ .zshrc has syntax errors — reload skipped"
fi
fi
}


Теперь после каждого изменения файла ~/.zshrc конфиг будет автоматически перечитан. НО перечитан он будет только после проверки, если ты своими кривыми руками где-то накосорезил — идешь нахуй. Логично? Логично!

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

1. Сохраняет timestamp последней модификации .zshrc

2. precmd() — специальная функция zsh, которая автоматически вызывается перед каждым выводом prompt (после любой команды или Enter). Вызывается незаметно, идеально для фоновых проверок без вмешательства в работу.

3. Дальше логика, сравнивает timestamps — если .zshrc отредактирован и сохранен, переходит к проверке.

4. zsh -n файл — проверяет синтаксис без выполнения (no-execute mode). Возвращает 0 при успехе, > 0 при ошибках (дубликаты, незакрытые скобки и т.п.).

5. Ну а дальше сообщает тебе, все ок или идешь нахуй.​


Нюанс:

echo 'syntax error' >> ~/.zshrc

/home/user/.zshrc:226: command not found: syntax
🔄 .zshrc auto-reloaded (OK)


Проблема в том, что zsh -n проверяет только синтаксис (скобки, конструкции), но не выполнение команд. Имей это ввиду.

Если сделать так:

echo '# syntax error' >> ~/.zshrc
🔄 .zshrc auto-reloaded (OK)


То всё пройдёт замечательно. Ну ты понял к чему я клоню.

Тема прикольная, экспериментируй.

🛠 #bash #linux #shell

💬 Bashdays 📲 MAX 🌐 LF 🔵 Blog
Please open Telegram to view this post
VIEW IN TELEGRAM
546
Синхронизируем настройки Pi-Hole между инстансами.

У меня в сети живет несколько нод с pi-hole, которые раскиданы по разным устройствам (proxmox, raspberry pi и т.п.). И сразу встала необходимость, чтобы все ноды с pi-hole имели одинаковые настройки.

Pi-hole — это сетевой DNS‑фильтр и блокировщик рекламы с открытым исходным кодом. Он работает как свой DNS‑сервер, перехватывает DNS‑запросы от устройств в локальной сети и блокирует домены из списков рекламы, трекеров и вредоносных сайтов, возвращая «пустой» ответ вместо IP‑адреса рекламного ресурса.


Стратегия такая, одна нода будет master, где производятся все основное настройки, затем все эти настройки раскатываются на другие ноды (slave).

Раньше такой кейс разруливали с помощью Orbital Sync, Nebula Sync и т.п. Но одно сдохло, другое работает через хуй-пизда-копыто. Короче нужно рабочее решение.

Пишем свой велосипед

#!/usr/bin/env bash

set -euo pipefail

PRIMARY_PIH_DIR="/etc/pihole"

SECONDARY_USER="root"
SECONDARY_PIH_DIR="/etc/pihole"
SECONDARY_HOSTS=(
"192.168.10.97"
"192.168.10.98"
"192.168.10.99"
)

RSYNC_EXCLUDES=(
"--exclude=pihole-FTL.db"
"--exclude=macvendor.db"
"--exclude=*.log"
)

echo "[pihole-sync] $(date): start"

for host in "${SECONDARY_HOSTS[@]}"; do
echo "[pihole-sync] ---- host ${host} ----"

rsync -az \
"${RSYNC_EXCLUDES[@]}" \
"${PRIMARY_PIH_DIR}/" \
"${SECONDARY_USER}@${host}:${SECONDARY_PIH_DIR}/"

echo "[pihole-sync] ${host}: restart dns"
ssh "${SECONDARY_USER}@${host}" "pihole restartdns >/dev/null 2>&1" || \
echo "[pihole-sync] ${host}: FAILED to restart dns"

done

echo "[pihole-sync] $(date): done"


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

Сохраняем, чмодим, кидаем в крон (а лучше в systemd с таймерами):

Про таймеры подробно писал тут и тут.


crontab -e
*/5 * * * * /usr/local/sbin/pihole-sync.sh >> /var/log/pihole-sync.log 2>&1


Не забываем прокинуть ssh ключи с master на slave, чтобы скрипт не уперся рогом в логин и пароль.

Вроде мелочь, а полезная, да еще и на bash. Хороших тебе предстоящих выходных и береги себя!

🛠 #bash #linux #devops #selfhosting

💬 Bashdays 📲 MAX 🌐 LF 🔵 Blog
Please open Telegram to view this post
VIEW IN TELEGRAM
532
В последних двух моих статьях я рассказывал о том, как я перешел с pfSense на opnSense и попытался заменить arpwatchd самописным скриптом. Здесь я расскажу, с какими «проблемами» пришлось столкнуться.

🔤🔤🔤🔤🔤🔤🔤

Первая статья: https://xn--r1a.website/bashdays/1356
Вторая статья: https://xn--r1a.website/bashdays/1360


Началось с того, что я тупанул. Я знал, что pfSense и opnSense основаны на BSD, и знал, что обычно там отсутствует bash.

И почему-то подумал, что придется писать скрипт на csh (tsch). Я не знаю csh, но принципиальных отличий нет.

Ну, скрипт и скрипт. Небольшие отличия в синтаксисе. if и while присутствуют, значит проблем нет. Это действительно так. Самой большой проблемой стал перенос длинных строк даже в константах:

#!/bin/bash
echo '1
2
3'

#!/bin/csh
echo '1 \
2 \
3'


Это делает однострочники (в моем случае awk) крайне уродливыми. Когда скрипт уже работал, я решил проверить, какие там есть оболочки cat /etc/shells , и с изумлением обнаружил /bin/sh.

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

Были небольшие затыки с программами. Вроде date, как date, но ключики немного не совпадают. Я думал там gawk, но нет, awk. (не нашел разделы BEGINFILE{...} ENDFILE{...}). Но, при большом желании можно обойтись и без них.

Что порадовало, так это наличие man в opnSense. В pfSense его не было. И это очень круто.

Очень жаль, что не нашел ramdisk /dev/shm (в Linux он обычно есть и под него выделено половину оперативки).

Ну, и еще одной проблемой стал редактор vi, который оказался единственным в системе!!!

ed, red, sed не в счет, хотя, если припрет... В pfSense был еще nano.

Я в последнее время использую vim (который начал изучать после рекомендаций Дмитрия Малинина здесь, на BashDays. За что ему спасибо.) Кто еще не начал - ставьте vim, и запускайте vimtutor. Этого будет достаточно, чтобы как-то работать в vi и vim.

Вывод из всего этого - nix это круто. И не важно - Linux или BSD. Они очень похожи, по крайней мере, если знаешь что-то одно - разобраться не проблема. Принципы одинаковые.

Холиваров прошу не устраивать. Просто решил поделиться опытом. Если где-то ошибся - поправьте.

🛠 #networks #linux #bash

💬 Bashdays 📲 MAX 🌐 LF 🔵 Blog
Please open Telegram to view this post
VIEW IN TELEGRAM
42
Фарш обратно не провернёшь

Порой в Bash поделках требуется создать какой-нибудь секрет, для дальнейшей его передачи в CI/CD или куда-то еще. Ежу понятно, это можно сделать «в лоб», но мыж с тобой не волки позорные, поэтому давай сделаем это по всем правилам DevSecOps Best Practice. (* лучшие практики безопасной разработки).

Задача: Создать безопасно секрет, чтобы его не спиздили (например из свапа при форензике).

Форензика — это направление информационной безопасности, связанное с анализом и восстановлением данных для расследования инцидентов


Кто-то извращается со shred и т.п. утилитами, но это избыточно, да и на SSD, Btrfs, ZFS и journal FS оно будет работать хуева и не гарантирует физическое уничтожение данных. Лучше на диск вообще ничего не писать.

Наш вариант это — pipe через printf. Можно конечно усложнить и сделать через анонимный файловый дескриптор, но получаются те же грабли только сбоку, смысла усложнять нет.

#!/usr/bin/env bash

set -euo pipefail

SECRET="$(openssl rand -base64 32)"

printf '%s' "$SECRET" | \
openssl enc -aes-256-cbc -salt -pbkdf2 -iter 200000 \
-in input.txt \
-out output.enc \
-pass stdin

unset SECRET


Что тут происходит?

Ничего необычного, генерим 32 случайных байта, кодируем в base64, получаем пароль высокой энтропии, затем через printf передаем пароль (без \n) через pipe в stdin.

Ну и в конце скармливаем какой-нибудь утилите или в CI/CD, куда нужно передать секрет, в моем случае я передал его в openssl.

По итогу секрет, не попадает в history, не лежит на диске и временно находится в памяти. Но важно понимать, что после unset SECRET переменная удаляется только из таблицы переменных, в памяти эта переменная может по-прежнему храниться и быть уязвима к форензике. Поэтому носи это в голове и по возможности перезатирай память например тем же stress.

А еще оно может попасть в swap и это еще хуже, swap это прям как неочищенная «корзина» с удаленными файлами.

На bash этот момент описать наверное не получится, поэтому покажу как сделать на Сиськах. Будем использовать mlock().

#include <sys/mman.h>
#include <string.h>
#include <stdio.h>

int main() {
char secret[32] = "super_secret_password";

if (mlock(secret, sizeof(secret)) != 0) {
perror("mlock failed");
return 1;
}

printf("Secret in locked memory\n");

// Используем секрет...

memset(secret, 0, sizeof(secret)); // затираем
munlock(secret, sizeof(secret));

return 0;
}


Когда этот код будет вызывать mlock(), указанный диапазон памяти закрепляется в RAM и ядро не имеет права выгружать его в swap.

Получается что сначала mlock() блокирует участок памяти, затирает memset() её перед освобождением и с помощью munlock() снимает блокировку.

Кстати mlock() используют GnuPG, OpenSSH, HashiVault, KeePassXX. Так что вариант надежный, можешь не сомневаться. А еще иногда используется mlockall():

mlockall(MCL_CURRENT | MCL_FUTURE);


Блокируется вся текущая и будущая память процесса, активно применяется демонами в HashiVault.

Такие дела.

Накидай еще своих вариантов в комменты, будет интересно ознакомиться.

🛠 #dev #security #bash

💬 Bashdays 📲 MAX 🌐 LF 🔵 Blog
Please open Telegram to view this post
VIEW IN TELEGRAM
155
Всем привет. Здесь я уже останавливался на sftp. Немного заострю внимание на особенностях применения в скриптах.

🔤🔤🔤🔤🔤🔤🔤

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

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

echo -e "ls\\ncd /tmp\\nget 123.txt\\nbye"|sftp -i keyfile user@host:/path/to/dir


Здесь \\n - это код перевода строки. Не красиво, но работает.

Вот тут есть небольшая тонкость, которую хотелось бы пояснить.

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

Для решения проблемы есть специальный ключ -b (batch mode) который позволяет читать sftp-скрипт из файла. Чтобы (как в нашем случае) читать stdin, нужно указать -b- , или совсем конкретно -b "/dev/stdin".

При использовании данного ключа, если команда не выполнена, sftp завершается с ошибкой, все дальнейшие команды игнорируются. А эту ситуацию можно обработать стандартными средствами bash.

Но и здесь есть исключения. Если команду указать с префиксом "-", то она переходит в разряд опциональных, и в случае ошибки, sftp-скрипт будет продолжен. Например -get 123.txt. Кроме этого префикса есть еще префикс "@", который подавляет печать(вывод) команды при выполнении. Префиксы равнозначны, могут использоваться в любой последовательности.

Знания почерпнуты из man, при попытке перевести обмен между 1c с ftp на sftp. Если тема интересна, могу привести рабочий скрипт обмена, и рассказать, почему я от него в итоге отказался.

man sftp


Всем кода без багов.

🛠 #linux #bash

💬 Bashdays 📲 MAX 🌐 LF 🔵 Blog
Please open Telegram to view this post
VIEW IN TELEGRAM
39
15 функций Bash, которые стоит добавить в .bashrc

Со временем понимаешь, что половину времени в терминале ты пишешь одно и тоже. Пара небольших функций в .bashrc экономит часы. У каждого эти функции обычно индивидуальны, но возможно этот список раскроет тебе глаза на что-то новое.

Создать директорию и сразу в неё перейти

mkcd() {
mkdir -p "$1" && cd "$1"
}


Подняться на несколько уровней вверх

up() {
local d=""
for ((i=1;i<=$1;i++)); do
d+="../"
done
cd "$d"
}


Быстро найти файл

ff() {
find . -type f -iname "*$1*"
}


Найти директорию

fd() {
find . -type d -iname "*$1*"
}


Найти процесс

psg() {
ps aux | grep -i "$1" | grep -v grep
}


Посмотреть последние команды

h() {
history | tail -n "$1"
}


Поиск по истории

hg() {
history | grep "$1"
}


Узнать размер директории

dirsize() {
du -sh "$1"
}


Универсальная распаковка архивов

extract() {
if [ -f "$1" ]; then
case "$1" in
*.tar.bz2) tar xjf "$1" ;;
*.tar.gz) tar xzf "$1" ;;
*.bz2) bunzip2 "$1" ;;
*.rar) unrar x "$1" ;;
*.gz) gunzip "$1" ;;
*.tar) tar xf "$1" ;;
*.tbz2) tar xjf "$1" ;;
*.tgz) tar xzf "$1" ;;
*.zip) unzip "$1" ;;
*.7z) 7z x "$1" ;;
*) echo "unknown archive" ;;
esac
fi
}


Быстрый HTTP-сервер из текущей папки

serve() {
python3 -m http.server "${1:-8000}"
}


Узнать свой внешний IP

myip() {
curl -s ifconfig.me
}


Узнать IP домена

ipinfo() {
dig +short "$1"
}


Показать открытые порты

ports() {
ss -tuln
}


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

cls() {
clear && printf '\e[3J'
}


Безопасный rm

rm() {
ls -FCsd -- "$@"
read -p 'Delete? [y/N] ' ans
if [ "$ans" = "y" ]; then
command rm -rf -- "$@"
fi
}


С удалением еще можно сделать аналог корзины, добавив простое копирование в какой-нибудь временный каталог, который автоматически зачищается спустя какое-то время, например в /tmp.

The end. Кидай в комменты, какие функции используешь ты, будет полезно.

Источник: https://boreal.social/post/15-practical-bash-functions-i-use-in-my-bashrc (обсуждения на реддите)

🛠 #bash

💬 Bashdays 📲 MAX 🌐 LF 🔵 Blog
Please open Telegram to view this post
VIEW IN TELEGRAM
8119
Всем привет.

Я тут с удивлением обнаружил, что в debian 13 исчезли команды last и lastb которые позволяли смотреть удачные/неудачные попытки ssh/sftp

🔤🔤🔤🔤🔤🔤🔤

А я к ним так привык... journalctl — прекрасен, но лазить по логам — так себе занятие.

Решил немного упростить, чтобы вывод был в виде таблички: timestamp ip login

Иногда мальчиши-плохиши подсовывают «пустого» пользователя " ", и чтобы число полей было всегда три, я заменяю его на ":", используется в качестве разделителя /etc/passwd, и поэтому такого логина не должно быть.

journalctl --output=short-unix --unit=ssh |
gawk '{match($0,/.*for( invalid user)? +(.*?) +from (([0-9]{1,3}[.]){3}[0-9]{1,3})/,a)
#^ это одна длинная строка
if(a[2]==""){a[2]=":"} # заменили "пробельного usera" на ":"
if(a[3]!="") print $1, a[3],a[2]}'



#Отображение только неудачных попыток
journalctl --output=short-unix --unit=ssh |
gawk '/error: maximum/{match($0,/.*for( invalid user)? +(.*?) +from (([0-9]{1,3}[.]){3}[0-9]{1,3})/,a)
#^это одна длинная строка
if(a[2]==""){a[2]=":"} # заменили "пробельного usera" на ":"
if(a[3]!="") print $1, a[3],a[2]}'


Если нужно ограничить диапазон дат можно использовать ключ

--since="@TIMESTAMP"

где TIMESTAMP дата вида 1773848322.767923 или 1773848322 или просто --since="-1 day"

Если не нравится дата в timestamp замените последнюю строчку на:

if(a[3]!="") print strftime("%Y%m%d_%H%M%S",$1), a[3],a[2]}'


Или какой-нибудь свой формат. Я, вообще не спец по regexp, поэтому, если кто-нибудь поможет ускорить выражение — буду благодарен.

Всем кода без багов.

🛠 #bash #linux

💬 Bashdays 📲 MAX 🌐 LF 🔵 Blog
Please open Telegram to view this post
VIEW IN TELEGRAM
842
А какие способы хитрой подмены файлов помимо mount --bind ты знаешь?

echo first >first.txt
echo second >second.txt
ln -s first.txt second.txt

# Выведет: ln: cannot create symbolic link from 'first.txt' to 'second.txt': File exists

mount --bind first.txt second.txt
# А так работает.

cat second.txt
#Выведет first.


Наверное можно еще через cgroups замутить, или через eBPF понаделать хуки на системный вызов open, openat, туда же mount overlay...

🛠 #shitcode #bash

💬 Bashdays 📲 MAX 🌐 LF 🔵 Blog
Please open Telegram to view this post
VIEW IN TELEGRAM
23
Всем привет. Что мы все о bash да и bash. Надоело. Сегодня поговорим про awk.

🔤🔤🔤🔤🔤🔤🔤

Не много осталось админов, которые парсят текстовые логи. Вся молодежь и даже пОдростки перешли на JSON. Ну да, это удобно. Но иногда awk быстрее. Особенно на больших файлах и файлах и простых фильтрах.

Но я сегодня не в настроении устраивать холивар.

При использовании логов очень часто возникает проблема — очень большое количество полей. А awk зачастую использует именно номер поля.

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

Сам скрипт:

#!/bin/awk -f
BEGIN{printf "INPUT field separator "
getline
FS=$0
printf "INPUT test line\n"
getline
printf "\n\n"
for (i=1;i<=NF;i++)print i, $i
}


Как обычно сохраняем fields.awk и делаем исполняемым:

chmod +x fields.awk
./fields.awk


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

1. Сначала вводим разделитель.
2. Затем строчку лога и получаем разбивку по полям.

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

Рассмотрим такой пример:

<134>1 2026-04-17T16:51:47+03:00 OPNsense.internal filterlog 18074 - [meta sequenceId="2054702"] 111,,,4b75111111111111111111111111cb1d,eno1,match,block,in,4,0x0,,64,27367,0,DF,6,tcp,60,192.168.2.125,192.168.7.14,60248,22,0,S,1539242113,,65535,,mss;sackOK;TS;nop;wscale


В этом случае, если нужно одновременно вытащить и дату и IP с портами, проще в качестве основного разделителя использовать ",", а потом первое поле:

<134>1 2026-04-17T16:51:47+03:00 OPNsense.internal filterlog 18074 - [meta sequenceId="2054702"] 111


Здесь, если мы разбить по пробелам (split($1,f," ")), то f[2] будет содержать дату. Которую в свою очередь можно путем нехитрых манипуляций превратить в unuxtime, для удобства машинной обработки.

Всем кода без багов.

🛠 #bash #linux

💬 Bashdays 📲 MAX 🌐 LF 🔵 Blog
Please open Telegram to view this post
VIEW IN TELEGRAM
141
Less и секретный режим мониторинга

Очередные консольные приколы о которых ты не знаешь. Есть такая красота:

tail -f /var/log/nginx/access.log


Эта команда будет в реальном времени выводить строчки из файла по мере их добавления. То есть ты можешь наблюдать за лог файлами как с авто-суфлёра.

НО есть аналог:

less +F /var/log/nginx/access.log


А если кто не знал, то в less можно пользоваться клавишами vim. Например, навигация hjkl.

Сразу видно, что утилиты затачивали под одну экосистему и unixway. Ну а теперь вкуснятина.

После выполнения команды less +F нажимаем CTRL+C и теперь можно передвигаться по выводу как раз теме же клавишами hjkl, что-то поискать /127.0.0.1, g/G, n/N.

На минуточку, в tail ты так быстро ничего не поищешь. Надо тормозить процесс, выгружать в файл, грепать-хуепать.


Ну а чтобы вернуться в режим наблюдения, нажимаем SHIFT+F и less возвращает режим мониторинга, строчки с логами продолжают бежать в реальном времени.

Так что less это не просто про пагинацию, а немного больше.

🛠 #bash #linux

💬 Bashdays 📲 MAX 🌐 LF 🔵 Blog
Please open Telegram to view this post
VIEW IN TELEGRAM
80