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

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

Реклама: @dad_admin
Download Telegram
PIPESTATUS: как узнать, какая команда в пайпе реально упала

В Bash есть неприятная особенность: после пайпа $? показывает статус только последней команды.

Например:

false | grep test

echo $?


Здесь grep успешно отработал и вернул 0, поэтому ошибка false потерялась.

▪️На помощь приходит PIPESTATUS

Сразу после выполнения пайпа:

false | grep test

echo "${PIPESTATUS[@]}"


Получим:

1 1


Каждое значение соответствует своей команде.

false       → 1
grep test → 1


▪️Более наглядный пример

cat /missing/file | grep ERROR | sort

echo "${PIPESTATUS[@]}"


Можно получить:

1 1 0


То есть cat завершился с ошибкой, grep ничего не получил, а sort технически выполнился успешно.

▪️Важный нюанс

PIPESTATUS очень легко потерять:

false | true

echo "${PIPESTATUS[@]}"


работает.

Но если между пайпом и проверкой выполнить другую команду:

false | true

echo "check"
echo "${PIPESTATUS[@]}"


теперь PIPESTATUS относится уже к echo "check".

Поэтому сохраняйте его сразу:

false | true

status=("${PIPESTATUS[@]}")

echo "${status[@]}"


▪️А если нужен общий статус пайпа?

Есть:

set -o pipefail


Теперь:

false | true

echo $?


вернёт ненулевой код.

Но pipefail отвечает только на вопрос «пайп завершился успешно?», а PIPESTATUS позволяет понять «какая именно команда вернула ошибку?».

▪️Почему это важно

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

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👨‍💻5
/proc/PID/fd: как узнать, что на самом деле держит процесс

ps показывает процесс, lsof умеет искать открытые файлы. Но иногда быстрее посмотреть непосредственно в /proc.

У каждого процесса есть каталог:

ls -l /proc/1234/fd


Внутри находятся символические ссылки на все открытые файловые дескрипторы процесса.

▪️Что можно увидеть

Например:

0 -> /dev/null
1 -> /var/log/app.log
2 -> /var/log/app.err
5 -> /tmp/data.db


0, 1 и 2 — стандартные stdin, stdout и stderr. Остальные дескрипторы приложение открыло самостоятельно.

▪️Найти удалённые файлы

Особенно полезен такой поиск:

ls -l /proc/1234/fd | grep deleted


Можно обнаружить:

7 -> /var/log/app.log (deleted)


Файл уже отсутствует в каталоге, но процесс всё ещё держит его открытым.

▪️Посмотреть тип дескриптора

readlink /proc/1234/fd/7


Результат может быть:

socket:[48192]


или:

pipe:[72831]


или:

/run/app.sock


То есть через один каталог можно увидеть не только обычные файлы, но и сокеты, pipes и другие объекты ядра.

▪️Найти все открытые дескрипторы процесса

for fd in /proc/1234/fd/*; do
printf '%s -> %s\n' "${fd##*/}" "$(readlink "$fd")"
done


Получается компактная карта того, с какими объектами взаимодействует процесс.

▪️Почему это важно

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

/proc/PID/fd помогает быстро найти удалённые логи, неожиданные файлы, UNIX-сокеты и каналы IPC - причём без отдельного инструмента анализа.

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10🔥1
/proc/PID/environ: какие переменные реально получил процесс

env показывает окружение текущего shell. Но для диагностики сервиса этого недостаточно: после запуска процесс мог получить совсем другой набор переменных.
Linux хранит окружение каждого процесса здесь:

cat /proc/1234/environ


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

tr '\0' '\n' < /proc/1234/environ


▪️Найти конкретную переменную

tr '\0' '\n' < /proc/1234/environ | grep '^PATH='


Или проверить настройки прокси:

tr '\0' '\n' < /proc/1234/environ |
grep -Ei 'proxy|http_proxy|https_proxy'


▪️Сравнить окружение двух процессов

Например, приложение запущено вручную и через systemd:

tr '\0' '\n' < /proc/1234/environ | sort > /tmp/app.env
tr '\0' '\n' < /proc/5678/environ | sort > /tmp/service.env

diff -u /tmp/app.env /tmp/service.env


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

▪️Почему env может вводить в заблуждение

Вы выполняете:

echo "$PATH"


и видите одно значение.
Но процесс, запущенный несколько часов назад, мог получить старый PATH. Изменение конфигурации shell не меняет окружение уже работающего процесса.

То же касается:

HTTP_PROXY
LD_LIBRARY_PATH
LANG
HOME
JAVA_HOME


▪️Важный момент

Чтение /proc/PID/environ требует соответствующих прав доступа. Для чужих процессов доступ может быть ограничен настройками безопасности системы.
И главное: вывод нельзя бездумно отправлять в логи. В окружении могут находиться токены, пароли и другие секреты.

▪️Почему это важно

Когда сервис работает “вручную”, но ломается под systemd, cron или другим менеджером процессов, часто проверяют конфигурационные файлы, но забывают про окружение.

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

BashTex 📱 #bash #linux
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6👍1
setsid: как действительно отделить процесс от SSH-сессии

nohup часто используют, чтобы процесс пережил закрытие терминала:
nohup ./backup.sh &

Но nohup в основном защищает от SIGHUP. Сам процесс всё ещё может оставаться связанным с текущей session.
Для полного создания новой сессии существует setsid.

▪️Что происходит обычно

После подключения по SSH можно посмотреть:

ps -o pid,ppid,sid,tty,cmd


У процессов будут общие SID и управляющий терминал.

При закрытии SSH это может иметь значение для поведения процессов.

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

setsid ./backup.sh


Процесс становится лидером новой session и получает новый SID.

Проверить:

ps -o pid,ppid,sid,tty,cmd -C backup.sh


Можно увидеть, что SID процесса больше не связан с вашей SSH-сессией.

▪️Почему одного & недостаточно

./backup.sh &


Фоновый процесс всё равно является частью текущей shell-сессии.
& лишь не блокирует терминал ожиданием процесса.

▪️А nohup?

nohup ./backup.sh &


Это уже лучше для сценария с закрытием терминала, но nohup и setsid решают разные задачи.

nohup изменяет обработку SIGHUP и обычно перенаправляет стандартный вывод.
setsid создаёт новую session и отделяет процесс от текущей session.

▪️Комбинация

Для простого запуска полностью отдельно:

setsid ./backup.sh >/tmp/backup.log 2>&1 < /dev/null &


Теперь процесс не зависит от стандартного ввода текущего терминала и находится в отдельной session.

▪️Где это полезно

• запуск долгих задач из SSH;
• анализ поведения daemon-like процессов;
• shell-обвязки для сервисов;
• понимание PID, PPID, SID и TTY.

▪️Почему это важно

&, nohup и setsid часто воспринимают как взаимозаменяемые способы “запустить в фоне”. На самом деле они меняют разные свойства процесса.

Если нужно понять, почему программа продолжает зависеть от SSH или терминала, смотреть стоит не только на PID, но и на SID, PPID и управляющий TTY.

BashTex 📱 #bash #SSH
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8🔥2
/proc/PID/cwd: когда процесс работает не из того каталога

У каждого Linux-процесса есть текущий рабочий каталог. Обычно мы видим его через shell, но для уже запущенного процесса можно посмотреть напрямую через /proc.

readlink /proc/1234/cwd


Например:

/var/lib/app


Это каталог, относительно которого процесс разрешает относительные пути.

▪️Найти процессы с неожиданным cwd

for p in /proc/[0-9]*; do
pid=${p##*/}
cwd=$(readlink "$p/cwd" 2>/dev/null) || continue
printf '%-8s %s\n' "$pid" "$cwd"
done


Теперь можно быстро увидеть, какие процессы находятся в /tmp, /dev/shm, домашних каталогах или других необычных местах.

▪️Почему это может быть важно

Приложение может запускаться из:

/opt/myapp


но после работы оказаться в другом каталоге из-за chdir().

Особенно интересны процессы с:

/tmp
/dev/shm
/home/user


если для конкретного сервиса такое поведение не ожидается.

▪️Связь с systemd

Для unit можно задать:

[Service]
WorkingDirectory=/var/lib/myapp


А фактическое значение уже запущенного процесса проверить через /proc.

Это полезнее, чем тупо смотреть конфигурацию: /proc/PID/cwd показывает состояние конкретного процесса прямо сейчас.

BashTex 📱 #bash #proc
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
return против exit: как не завершить весь скрипт внутри функции

В Bash функция может вернуть управление вызывающему коду или полностью завершить текущий shell. Эти два случая часто путают.

▪️exit завершает весь скрипт

check_config() {
if [ ! -f config.conf ]; then
echo "Config not found"
exit 1
fi
}

check_config

echo "Продолжаем работу"


Если config.conf нет, выполнение закончится прямо внутри функции. Строка Продолжаем работу никогда не выполнится.

▪️return завершает только функцию

check_config() {
if [ ! -f config.conf ]; then
echo "Config not found"
return 1
fi
}

check_config

echo "Продолжаем работу"


Теперь функция завершилась с кодом 1, но сам скрипт продолжил выполнение.

Именно поэтому return обычно используют внутри функций, а exit - на уровне самого скрипта.

▪️Код возврата можно обработать

if check_config; then
echo "Config OK"
else
echo "Config error"
fi


Или сохранить результат:

check_config
status=$?

if [ "$status" -ne 0 ]; then
echo "Ошибка: $status"
fi


▪️Важный нюанс

return можно использовать только внутри функции или в контексте, где Bash допускает возврат из текущего sourced-файла.

Например:

return 1


в обычном запускаемом скрипте верхнего уровня приведёт к ошибке:

return: can only `return' from a function


▪️Типичная ошибка

deploy() {
check_config || exit 1
start_service
}


Если deploy вызывается из другого скрипта как функция, exit завершит уже весь shell.

Чаще правильнее:

deploy() {
check_config || return 1
start_service
}


А решение о завершении оставить вызывающему коду:

deploy || exit 1


Так функция сообщает об ошибке, а не решает за весь скрипт, что делать дальше.

BashTex 📱 #bash #linux
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
$? после &&, || и if: почему код возврата Bash бывает неочевидным

В Bash $? содержит код завершения последней выполненной команды. Но конструкции &&, || и if меняют то, какие команды вообще выполняются.

▪️Простая цепочка

false && echo "OK"
echo $?


echo после && не запускается, потому что false вернула 1.
Итоговый $? - 1.

А здесь:

true && echo "OK"
echo $?


последней командой становится echo "OK", поэтому $? уже равен 0.

▪️С || ситуация аналогичная

false || echo "fallback"
echo $?


false запускается, затем выполняется echo, потому что первая команда завершилась ошибкой.

В итоге $? будет 0 - код echo, а не false.

Если нужен исходный результат:

false
status=$?

if [ "$status" -ne 0 ]; then
echo "Command failed: $status"
fi


▪️Почему if не создаёт обычный 1

if false; then
echo "success"
fi

echo $?


Здесь if возвращает статус последней выполненной команды в выбранной ветке. Если ни одна ветка не выполнялась, результатом становится код проверки условия.

▪️Частая ошибка

command || echo "failed"
status=$?


status здесь содержит результат echo, а не command.

Если важно сохранить код ошибки:

if ! command; then
status=$?
fi


Но здесь есть ещё один нюанс: ! инвертирует статус, поэтому для получения исходного кода лучше сохранять его непосредственно после команды.

Например:

command
status=$?

if [ "$status" -ne 0 ]; then
echo "failed: $status"
fi


▪️Почему это важно

В сложных Bash-скриптах $? легко прочитать слишком поздно. Любая следующая команда - даже echo, printf или test - может заменить предыдущий код возврата.

Поэтому если результат команды важен, сохраняйте его сразу.

BashTex 📱 #bash #linux
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
lastpipe: как Bash меняет поведение последней команды в pipeline

Одна из особенностей Bash - команды в pipeline обычно выполняются в отдельных subshell. Из-за этого переменные, изменённые внутри while, после цикла могут не сохраниться.

Например:

count=0

printf '%s\n' a b c |
while read -r line; do
((count++))
done

echo "$count"


В некоторых случаях результатом будет 0.

Но у Bash есть настройка lastpipe, которая меняет это поведение.

▪️Включаем lastpipe

shopt -s lastpipe


Теперь последняя команда pipeline может выполняться в текущем shell.

count=0

printf '%s\n' a b c |
while read -r line; do
((count++))
done

echo "$count"


При подходящих условиях получим: 3

▪️Есть важное условие

lastpipe работает только когда job control отключён:

set +m
shopt -s lastpipe


Именно поэтому поведение может отличаться между интерактивным Bash и обычным скриптом.

▪️Проверить настройку

shopt lastpipe


Получим:

lastpipe        off


или:

lastpipe        on


▪️Почему это не универсальная замена

Вместо изменения поведения shell часто проще использовать редирект:

while read -r line; do
((count++))
done < <(printf '%s\n' a b c)


Так код не зависит от настройки lastpipe.

▪️Где lastpipe действительно интересен

Это полезно знать при разборе чужих Bash-скриптов: одна и та же конструкция с pipeline может вести себя по-разному в зависимости от настроек shell и режима job control.

lastpipe - хороший пример того, как небольшая опция Bash меняет фундаментальное поведение pipeline и область видимости изменений.

BashTex 📱 #bash #linux
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
nullglob: когда *.log внезапно превращается в строку

Bash умеет раскрывать glob-шаблоны:

*.log


Но если файлов с таким расширением нет, по умолчанию шаблон может остаться обычной строкой.
Например:

for file in /var/log/*.log; do
echo "$file"
done


Если .log файлов нет, цикл может получить буквально:

/var/log/*.log


Это легко превращается в ошибку в автоматизации.

▪️Включаем nullglob

shopt -s nullglob


Теперь шаблон без совпадений исчезает:

files=(/var/log/*.log)

echo "${#files[@]}"


Если файлов нет: 0
А не 1 с буквальным *.log.

▪️Почему это важно для массивов

Без nullglob:

files=(/tmp/*.tmp)


может создать массив из одного элемента:

/tmp/*.tmp


После:

shopt -s nullglob


получаем действительно пустой массив.

▪️nullglob влияет только на текущий shell

Его можно включить локально внутри функции:

check_logs() {
local old_nullglob
shopt -q nullglob
old_nullglob=$?

shopt -s nullglob

files=(/var/log/*.log)

((${#files[@]})) && printf '%s\n' "${files[@]}"

((old_nullglob == 0)) || shopt -u nullglob
}


Но для простых скриптов обычно достаточно включить настройку в начале.

▪️А если отсутствие файлов должно быть ошибкой?

Тогда существует противоположная настройка:

shopt -s failglob


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

▪️Почему это важно

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

nullglob позволяет отличить настоящий список файлов от строки с нераскрытым шаблоном - особенно полезно при работе с массивами, логами и автоматической обработкой каталогов.

BashTex 📱 #bash #linux
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4🔥2
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 -C

generate_config > /etc/myapp/config.conf


Если файл уже существует, операция остановится вместо того, чтобы молча уничтожить текущую конфигурацию.

▪️Важный нюанс

noclobber не является полноценной защитой от всех способов изменения файла. Это именно настройка shell для оператора >.
Её задача проще: превратить потенциально опасную случайную перезапись в явную ошибку.

▪️Почему это важно

В Bash > выглядит безобидно, но фактически означает «открыть файл на запись и обнулить его». noclobber позволяет добавить дополнительный барьер там, где потеря существующего содержимого недопустима.

BashTex 📱 #bash #linux
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"
done


▪️Почему это важно

mapfile - встроенный механизм Bash для преобразования потоковых данных в массив. Он делает код короче и избавляет от целого класса проблем, связанных с while read, пайпами и сохранением переменных.

BashTex 📱 #bash #linux
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]}"
}

foo


BASH_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[@]}"


покажет цепочку файлов, через которые прошёл вызов.

▪️Чем это отличается от $0

echo "$0"


обычно показывает имя запущенного скрипта или shell.

А:

echo "${BASH_SOURCE[0]}"


показывает конкретный Bash-файл, где находится текущий код.

Это особенно заметно при использовании:

source lib.sh


▪️Почему это важно

При отладке больших shell-проектов сообщение вроде:

ERROR: failed

почти бесполезно.

А:

ERROR: deploy() in lib/deploy.sh

уже позволяет быстро найти источник проблемы.
BASH_SOURCE и FUNCNAME превращают Bash-функции из непрозрачного набора вызовов в трассируемый стек.

BashTex 📱 #bash #linux
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 📱 #bash #linux
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
Please open Telegram to view this post
VIEW IN TELEGRAM
😁8
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 📱 #bash #linux
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 📱 #bash #linux
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 📱 #bash #linux
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
tee + process substitution: один поток и несколько получателей

Иногда нужно одновременно: увидеть вывод команды в терминале, записать его в файл и передать на обработку другой программе Если делать это по очереди - потеряется поток данных. Здесь помогает связка tee + process substitution.

▪️ Что делает tee. tee дублирует поток:


command | tee file.log

вывод остается в терминале и одновременно пишется в file.log

▪️ Несколько получателей. tee может писать сразу в несколько файлов:


command | tee out1.log out2.log

Но иногда нужно не просто файл, а другую команду.

▪️ Process substitution. Bash позволяет подставить вывод команды как файл:


>(command)

Это называется process substitution.

▪️ Комбинируем


command | tee >(grep ERROR > errors.log)

command генерирует поток
tee дублирует его

Одна копия идет в терминал, а другая в grep. Если найден ERROR, запись попадет в errors.log.

▪️ Более реальный пример. Допустим, идет сбор логов:


journalctl -f | tee >(grep ERROR >> errors.log)

Теперь: полный поток остается в терминале, ошибки автоматически сохраняются

▪️ Можно делать несколько обработчиков


journalctl -f | tee \
>(grep ERROR >> errors.log) \
>(grep WARN >> warn.log)

Один поток и сразу несколько фильтров.

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6👍1
Please open Telegram to view this post
VIEW IN TELEGRAM
😁7
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 📱 #bash #linux
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
failglob - почему Bash должен падать при нераскрывшемся glob

Обычно Bash спокойно оставляет glob как есть, если совпадений нет:

rm /var/log/app/*.old


Если .old-файлов нет, команда фактически получит:

rm: cannot remove '/var/log/app/*.old': No such file or directory


Но в автоматизации это может быть проблемой: скрипт продолжит выполнение, хотя вы рассчитывали, что glob что-то нашёл.

▪️failglob меняет поведение

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 📱 #bash #linux
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🔥1