noclobber: как запретить Bash случайно перезаписать файлОбычный оператор:
echo "new config" > config.txtбез предупреждения удалит старое содержимое
config.txt.Для скриптов, где случайная перезапись критичного файла недопустима, в Bash есть режим
noclobber.set -o noclobber
Теперь:
echo "new config" > config.txt
если файл уже существует, завершится ошибкой:
bash: config.txt: cannot overwrite existing file
set -o | grep noclobber
Или короткая форма:
set -C
Выключить обратно:
set +C
>> продолжает работатьnoclobber защищает именно от перезаписи через >:echo "new line" >> config.txt
добавит данные в конец файла.
Есть специальная конструкция:
echo "new config" >| config.txt
Оператор
>| игнорирует noclobber и разрешает перезапись.Это удобно, когда защита включена глобально, но конкретный участок скрипта действительно должен заменить файл.
Например, скрипт генерирует конфигурацию:
set -Cgenerate_config > /etc/myapp/config.conf
Если файл уже существует, операция остановится вместо того, чтобы молча уничтожить текущую конфигурацию.
noclobber не является полноценной защитой от всех способов изменения файла. Это именно настройка shell для оператора >.Её задача проще: превратить потенциально опасную случайную перезапись в явную ошибку.
В Bash
> выглядит безобидно, но фактически означает «открыть файл на запись и обнулить его». noclobber позволяет добавить дополнительный барьер там, где потеря существующего содержимого недопустима.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
mapfile: как загружать вывод команды в массив без while readЕсли результат команды нужно сохранить построчно, часто используют:
items=()
while IFS= read -r line; do
items+=("$line")
done < <(some_command)
В Bash для этого есть встроенный
mapfile.mapfile -t files < <(find /tmp -type f)
Теперь каждая строка - отдельный элемент массива:
printf '%s\n' "${files[@]}"-t убирает символ переноса строки из каждого элемента.Нет отдельного
while, нет проблемы с subshell и не нужно вручную добавлять элементы:mapfile -t users < <(cut -d: -f1 /etc/passwd)
echo "Users: ${#users[@]}"
Можно обращаться к конкретному элементу:
echo "${users[0]}"mapfile -t lines < config.txt
А затем:
for line in "${lines[@]}"; do
printf 'CONFIG: %s\n' "$line"
doneНапример:
mapfile -t -n 10 lines < access.log
В массив попадут только первые 10 строк.
mapfile ориентирован именно на разделённые строки данные. Если содержимое может содержать \n внутри логического элемента, обычный построчный массив уже не подходит.Для других разделителей можно использовать
-d:mapfile -d ',' -t items < data.txt
Теперь разделителем считается запятая.
Например, получить список systemd-сервисов:
mapfile -t services < <(
systemctl list-units --type=service --state=running --no-legend |
awk '{print $1}'
)
После этого массив можно безопасно передавать дальше:
for service in "${services[@]}"; do
systemctl is-active "$service"
donemapfile - встроенный механизм Bash для преобразования потоковых данных в массив. Он делает код короче и избавляет от целого класса проблем, связанных с while read, пайпами и сохранением переменных.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
BASH_SOURCE и FUNCNAME: как понять, откуда реально вызвали функциюВ больших Bash-скриптах функция может вызываться из другой функции, которая пришла из отдельного файла. Обычный
$0 в такой ситуации мало что объясняет.Для этого у Bash есть специальные массивы:
BASH_SOURCE
FUNCNAME
foo() {
echo "file: ${BASH_SOURCE[0]}"
echo "caller: ${BASH_SOURCE[1]}"
echo "function: ${FUNCNAME[0]}"
}
fooBASH_SOURCE[0] показывает файл, в котором выполняется текущая функция, а BASH_SOURCE[1] - файл, откуда она была вызвана.foo() {
printf '%s\n' "${FUNCNAME[@]}"
}
bar() {
foo
}
barМожно получить примерно:
foo
bar
main
То есть Bash хранит стек функций.
Например:
log() {
printf '[%s] %s: %s\n' \
"$(date '+%F %T')" \
"${FUNCNAME[1]}" \
"$*"
}Теперь:
deploy() {
log "starting deployment"
}
deployможет вывести:
[2026-08-24 12:30:01] deploy: starting deployment
Лог сразу показывает, какая функция создала сообщение.
Допустим:
main.sh
lib.sh
utils.sh
и функции вызывают друг друга через
source.Тогда:
printf '%s\n' "${BASH_SOURCE[@]}"покажет цепочку файлов, через которые прошёл вызов.
$0echo "$0"
обычно показывает имя запущенного скрипта или shell.
А:
echo "${BASH_SOURCE[0]}"показывает конкретный Bash-файл, где находится текущий код.
Это особенно заметно при использовании:
source lib.sh
При отладке больших shell-проектов сообщение вроде:
ERROR: failedпочти бесполезно.
А:
ERROR: deploy() in lib/deploy.shуже позволяет быстро найти источник проблемы.
BASH_SOURCE и FUNCNAME превращают Bash-функции из непрозрачного набора вызовов в трассируемый стек.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
/proc/PID/wchan: где именно завис процессЕсли процесс показывает высокий
D в ps, сам статус ещё не говорит, на чём именно он остановился.Для этого Linux предоставляет
/proc/<PID>/wchan.ps -eo pid,stat,wchan:30,cmd | grep '[D]'
Например:
2418 D io_schedule backup
Здесь
wchan показывает kernel wait channel - функцию ядра, в которой процесс ожидает продолжения.cat /proc/2418/wchan
Можно получить:
io_schedule
Или:
nfs_wait_bit_killable
Во втором случае уже появляется конкретная зацепка: процесс может ждать операции, связанной с NFS.
for p in /proc/[0-9]*; do
pid=${p##*/}
state=$(awk '{print $3}' "$p/stat" 2>/dev/null)
[ "$state" = D ] || continue
printf '%-8s %s\n' \
"$pid" \
"$(cat "$p/wchan" 2>/dev/null)"
done
Получится примерно:
2418 io_schedule
2421 io_schedule
2517 nfs_wait_bit_killable
Если десятки процессов одновременно находятся в одном
wchan, это уже может указывать на общий узкий участок.Для более глубокого разбора:
cat /proc/2418/stack
Можно увидеть цепочку вызовов ядра, в которой находится поток.
Например:
io_schedule
schedule
...
Доступ к
/proc/PID/stack зависит от прав и настроек безопасности.kill -9 иногда не помогаетПроцесс в состоянии
D находится в uninterruptible sleep. Если он ждёт завершения определённой операции ядра, SIGKILL не может мгновенно вывести его из этого состояния.Поэтому вместо бесконечного:
kill -9 2418
полезнее сначала посмотреть:
ps -o pid,stat,wchan,cmd -p 2418
cat /proc/2418/wchan
cat /proc/2418/stack
Так можно понять, почему процесс не реагирует.
ps показывает симптом - D. wchan помогает увидеть причину ожидания на уровне ядра.Для проблем с дисками, NFS, storage и зависшими I/O-операциями это один из самых быстрых способов перейти от «процесс завис» к пониманию, где именно он застрял.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
read -d: как читать Bash-поток не по строкамread обычно ждёт перевод строки:while IFS= read -r line; do
printf '%s\n' "$line"
done < file.txt
Но у
read можно изменить разделитель. Это особенно полезно для данных, где обычный \n не является надёжной границей записи.Например,
find умеет выдавать имена файлов через \0:find /tmp -type f -print0 |
while IFS= read -r -d '' file; do
printf '%s\n' "$file"
done
-d '' означает: читать до NUL-символа.Это позволяет корректно обработать имя вроде:
report
2026.txt
где внутри имени действительно может быть перевод строки.
read здесь опасенТакой вариант:
find /tmp -type f -print0 |
while read -r file; do
...
done
не соответствует формату данных:
find разделяет записи \0, а read по умолчанию ищет \n.Например, прочитать значения через
::
while IFS= read -r -d ':' value; do
printf '<%s>\n' "$value"
done <<< 'one:two:three:'
Получим:
<one>
<two>
<three>
read умеет ещё и ограничивать количество символовIFS= read -r -n 1 char
Теперь Bash прочитает только один символ.
А:
IFS= read -r -N 8 chunk
будет ждать ровно 8 символов, не считая разделитель строки.
read -d особенно хорошо сочетается с командами, поддерживающими NUL-разделители:find ... -print0
git ... -z
sort -z
xargs -0
Получается безопасная цепочка, в которой пробелы, кавычки и даже переводы строк внутри имён файлов не ломают обработку.
Разделитель данных должен совпадать с тем, как эти данные реально генерируются.
read - это не только «прочитать строку»: с -d, -n и -N он может работать с потоками гораздо точнее.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
local с подстановкой команды: почему local result=$(cmd) скрывает ошибкуВ Bash есть тонкая ловушка внутри функций. На первый взгляд эти два варианта выглядят одинаково:
result=$(false)
echo "$?"
Здесь
$? будет 1.Но если написать:
local result=$(false)
echo "$?"
получим:
0
В конструкции:
local result=$(cmd)
выполняются сразу две вещи:
1. Bash выполняет
$(cmd) и получает результат.2.
local объявляет переменную.Проблема в том, что код возврата всей команды определяется
local, а не самой command substitution.local успешно создал переменную - поэтому функция видит 0.Например:
get_data() {
local result=$(curl -fsS https://example.com/api)
echo "status=$?"
}Если
curl завершится с ошибкой, проверка $? после local может показать успешное выполнение.Ошибка потеряна.
Разделите объявление и присваивание:
get_data() {
local result
result=$(curl -fsS https://example.com/api)
local status=$?
echo "status=$status"
}Теперь
$? относится непосредственно к curl.get_data() {
local result
if result=$(curl -fsS https://example.com/api); then
printf '%s\n' "$result"
else
echo "command failed"
return 1
fi
}Здесь условие
if проверяет именно exit code command substitution.localПохожая ситуация возникает с некоторыми встроенными командами, когда результат присваивания объединяется с другой операцией.
Поэтому правило простое:
local result
result=$(cmd)
а не:
local result=$(cmd)
В обычном скрипте такая мелочь может остаться незаметной. В функции, которая используется в цепочке:
deploy || exit 1
потерянный exit code уже превращается в реальную логическую ошибку: функция может сообщить об успехе, хотя команда внутри неё завершилась с ошибкой.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
RuntimeMaxSec: как systemd убивает сервис, который работает слишком долгоИногда сервис не падает и не зависает в классическом смысле - он просто продолжает работать часами, хотя по логике приложения должен завершиться.
В systemd для этого есть
RuntimeMaxSec=.В unit-файле:
[Service]
ExecStart=/usr/local/bin/job.sh
RuntimeMaxSec=30min
После 30 минут с момента запуска systemd остановит сервис.
Для более точного ограничения:
RuntimeMaxSec=1h 30min
Это не просто
kill -9.По умолчанию systemd инициирует обычную остановку сервиса. Если процесс не завершится в течение
TimeoutStopSec=, systemd применит настроенное принудительное завершение.Например:
[Service]
RuntimeMaxSec=30min
TimeoutStopSec=20s
Получается цепочка:
30 мин → остановка
↓
20 сек на graceful shutdown
↓
принудительное завершение
После запуска:
systemctl show myjob.service \
-p RuntimeMaxUSec \
-p TimeoutStopUSec
А текущий статус:
systemctl status myjob.service
RuntimeMaxSec не запрещает сервису запускаться снова.Если unit настроен так:
[Service]
Restart=on-failure
RuntimeMaxSec=30min
важен результат завершения и дополнительные параметры restart policy.
Сервис может быть запущен systemd снова после остановки.
Поэтому
RuntimeMaxSec стоит рассматривать вместе с Restart=, а не как самостоятельный механизм watchdog.Допустим, есть batch-задача:
[Service]
Type=oneshot
ExecStart=/opt/scripts/import.sh
RuntimeMaxSec=2h
Если импорт по какой-то причине зависнет на бесконечной операции, systemd не оставит процесс работать сутки.
RuntimeMaxSec - это лимит общего времени работы, а не проверка того, отвечает ли приложение.Если сервис должен завершаться за 5 минут, но иногда работает 10 минут по штатной причине, такой лимит будет ошибкой конфигурации.
Для контроля «жив ли сервис и отвечает ли он» нужны другие механизмы.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
KillMode=: почему systemctl stop иногда оставляет процессыОстановить сервис через:
systemctl stop myapp.service
не всегда означает, что исчезнет только один процесс. У systemd есть отдельная настройка, определяющая, какие процессы будут остановлены вместе с unit.
Это
KillMode=.[Service]
ExecStart=/opt/myapp/start.sh
KillMode=control-group
По умолчанию systemd работает с control group сервиса. При остановке он может завершить все процессы внутри cgroup, а не только основной PID.
Проверить:
systemctl show myapp.service -p KillMode
process[Service]
KillMode=process
В этом режиме сигнал отправляется только основному процессу сервиса.
Если start.sh породил:
myapp
├─ worker1
└─ worker2
дочерние процессы могут продолжить работу после остановки unit.
control-group[Service]
KillMode=control-group
systemd отслеживает cgroup целиком:
myapp.service
├─ PID 1200
├─ PID 1201
└─ PID 1202
При остановке unit процессы внутри группы также попадут под завершение.
systemctl status myapp.service
или:
systemctl show myapp.service -p ControlGroup
Затем:
systemd-cgls
Можно увидеть дерево процессов по cgroup.
KillMode=mixed
При остановке
SIGTERM получает основной процесс, а при последующем принудительном завершении systemd применяет его ко всей control group.Это позволяет дать главному процессу шанс корректно завершиться, не оставляя дочерние процессы навсегда.
Особенно легко получить «призраков» при запуске приложения через shell-обёртку:
ExecStart=/bin/bash /opt/start.sh
Если скрипт запускает фоновые процессы, нужно понимать, в какой cgroup они окажутся и как
KillMode повлияет на их завершение.systemctl stop - это не просто отправка SIGTERM одному PID. В systemd процесс существует внутри cgroup, и политика KillMode определяет масштаб остановки.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
failglob - почему Bash должен падать при нераскрывшемся glob
Обычно Bash спокойно оставляет glob как есть, если совпадений нет:
Если
Но в автоматизации это может быть проблемой: скрипт продолжит выполнение, хотя вы рассчитывали, что glob что-то нашёл.
▪️ failglob меняет поведение
Если совпадений нет, Bash сам выдаст ошибку:
То есть проблема обнаруживается на этапе раскрытия glob, ещё до запуска команды.
▪️ Особенно полезно в скриптах:
Без
С
▪️ Но есть нюанс
Тогда при отсутствии совпадений массив просто будет пустым.
Итого получается:
Для критичных автоматизаций
BashTex📱 #bash #linux
Обычно Bash спокойно оставляет glob как есть, если совпадений нет:
rm /var/log/app/*.old
Если
.old-файлов нет, команда фактически получит:rm: cannot remove '/var/log/app/*.old': No such file or directory
Но в автоматизации это может быть проблемой: скрипт продолжит выполнение, хотя вы рассчитывали, что glob что-то нашёл.
shopt -s failglob
files=(/var/log/app/*.old)
Если совпадений нет, Bash сам выдаст ошибку:
bash: no match: /var/log/app/*.old
То есть проблема обнаруживается на этапе раскрытия glob, ещё до запуска команды.
#!/usr/bin/env bash
set -e
shopt -s failglob
for file in /backup/*.tar.gz; do
process "$file"
done
Без
failglob цикл может получить буквально строку /backup/*.tar.gz.С
failglob скрипт сразу сигнализирует, что ожидаемых файлов нет.failglob не всегда нужен. Если отсутствие файлов - нормальная ситуация, лучше использовать nullglob:shopt -s nullglob
files=(/backup/*.tar.gz)
for file in "${files[@]}"; do
process "$file"
done
Тогда при отсутствии совпадений массив просто будет пустым.
Итого получается:
failglob → отсутствие совпадения считается ошибкой. nullglob → отсутствие совпадения превращается в пустой список.Для критичных автоматизаций
failglob помогает не пропустить логическую ошибку, замаскированную обычным поведением Bash.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🔥1
PSI: /proc/pressure/* - как понять, чего реально не хватает системе
Для этого в Linux есть PSI - Pressure Stall Information:
Например:
▪️
▪️
Например:
Это уже гораздо интереснее, чем просто увидеть высокий RAM usage.
Если
Если начинает расти
То же самое для I/O:
Если I/O PSI высокий, а CPU почти не испытывает pressure, проблема может быть не в загрузке процессора, а в storage latency.
А CPU PSI позволяет увидеть ситуацию, когда CPU формально загружен не на 100%, но runnable-задачи всё равно регулярно ждут свою очередь.
Проверить всё сразу:
PSI полезен именно как метрика задержки из-за конкуренции за ресурсы, а не просто их потребления.
Поэтому вместо вопроса «CPU/RAM/Disk загружены?» иногда правильнее спросить:
«Какой ресурс заставляет процессы ждать?»
BashTex📱 #perfomance #linux
load average показывает, что в системе есть очередь на выполнение или I/O. Но он плохо отвечает на другой вопрос: насколько пользователи и процессы реально страдают от этого дефицита?Для этого в Linux есть PSI - Pressure Stall Information:
cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io
Например:
full avg10=0.00 avg60=0.01 avg300=0.00 total=...
some - хотя бы часть задач была вынуждена ждать ресурс.full - все задачи соответствующей группы одновременно стояли из-за этого ресурса.avg10, avg60, avg300 - процент времени за последние 10, 60 и 300 секунд, когда происходило такое ожидание.Например:
some avg10=18.5
full avg10=7.2
Это уже гораздо интереснее, чем просто увидеть высокий RAM usage.
Если
memory some растёт - процессы регулярно упираются в нехватку памяти.Если начинает расти
memory full - система уже попадает в периоды, когда все учитываемые задачи одновременно не могут нормально продвигаться из-за memory pressure.То же самое для I/O:
cat /proc/pressure/io
Если I/O PSI высокий, а CPU почти не испытывает pressure, проблема может быть не в загрузке процессора, а в storage latency.
А CPU PSI позволяет увидеть ситуацию, когда CPU формально загружен не на 100%, но runnable-задачи всё равно регулярно ждут свою очередь.
Проверить всё сразу:
for f in /proc/pressure/*; do
echo "=== $f ==="
cat "$f"
done
PSI полезен именно как метрика задержки из-за конкуренции за ресурсы, а не просто их потребления.
Поэтому вместо вопроса «CPU/RAM/Disk загружены?» иногда правильнее спросить:
«Какой ресурс заставляет процессы ждать?»
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9