Когда переменные пропадают
Одна из частых ловушек в bash - переменная изменилась… но снаружи осталась прежней. Причина почти всегда одна - subshell.
▪️ Что такое subshell. Subshell - это дочерний процесс bash. Он получает копию переменных, но изменения не возвращаются назад. Создается, например, здесь:
или в пайпах:
▪️ Классическая проблема
Результат: 0
Почему? Цикл while выполняется в subshell.
▪️ Правильный вариант
Результат: 3
Теперь цикл работает в текущем shell.
▪️ Ещё пример
Результат: 1
Изменение потерялось.
▪️ Когда это полезно. Subshell - это не баг, а инструмент. Например:
внутри меняем каталог, а снаружи остаемся там же
BashTex📱 #scripts
Одна из частых ловушек в bash - переменная изменилась… но снаружи осталась прежней. Причина почти всегда одна - subshell.
( command )
или в пайпах:
command | while read ...
count=0
echo -e "a\nb\nc" | while read -r line; do
((count++))
done
echo "$count"
Результат: 0
Почему? Цикл while выполняется в subshell.
count=0
while read -r line; do
((count++))
done < <(echo -e "a\nb\nc")
echo "$count"
Результат: 3
Теперь цикл работает в текущем shell.
x=1
( x=5 )
echo "$x"
Результат: 1
Изменение потерялось.
( cd /tmp && ls )
внутри меняем каталог, а снаружи остаемся там же
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
mkfifo: обмен данными между процессами через именованные каналыОбычный pipe:
command1 | command2
работает только пока запущены обе команды. Но иногда нужен постоянный канал, к которому разные процессы могут подключаться позже.
Для этого в Linux есть FIFO (named pipe).
mkfifo /tmp/my_pipe
Теперь
/tmp/my_pipe выглядит как файл:ls -l /tmp/my_pipe
Вывод:
prw-r--r-- 1 user user 0 Aug 4 12:00 my_pipe
Буква
p означает pipe.Терминал 1:
cat /tmp/my_pipe
Терминал 2:
echo "hello from process" > /tmp/my_pipe
Первый процесс сразу получит сообщение.
Можно отправлять события из одного скрипта в другой:
mkfifo /tmp/events.pipe
tail -f /var/log/app.log > /tmp/events.pipe &
А отдельный обработчик:
while read line; do
echo "EVENT: $line"
done < /tmp/events.pipe
Теперь обработка логов отделена от источника данных.
FIFO блокирует процесс:
echo "data" > /tmp/my_pipe
будет ждать, пока кто-то откроет канал на чтение.
Поэтому в автоматизации часто используют фоновые процессы:
cat /tmp/my_pipe &
echo "start" > /tmp/my_pipe
Данные в FIFO не сохраняются на диске.
Если никто не читает:
процесс → FIFO → процессданные не складываются в очередь, а ждут получателя.
• связь между Bash-скриптами;
• простые IPC-механизмы;
• обработка потоков данных;
• разделение генератора и обработчика.
mkfifo часто заменяет временные файлы и сложные конструкции с перенаправлениями. Это простой способ построить связь между независимыми процессами прямо средствами Linux без дополнительных сервисов.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9
Почему
Когда нужно убрать дубликаты, многие сразу пишут:
Работает. Но только если понимать, что именно делает каждая команда.
▪️ Как работает
Распространённое заблуждение -
На самом деле он удаляет только соседние дубликаты.
Например:
Если выполнить:
получим тот же самый список - одинаковые строки не стоят рядом.
▪️ Зачем нужен
После сортировки одинаковые строки оказываются соседними:
Результат:
Именно поэтому эти команды почти всегда используются вместе.
▪️ Когда сортировка не подходит
Если порядок строк важен,
Например, при анализе логов или событий по времени это может полностью исказить картину.
В таких случаях лучше использовать:
Команда удаляет дубликаты, но сохраняет исходный порядок строк.
▪️ Найти только повторяющиеся строки
Иногда нужны не уникальные записи, а наоборот - только дубликаты:
Или вывести количество повторений:
Получим:
▪️ Почему это важно
BashTex📱 #sort
sort | uniq не всегда работает так, как ожидаетсяКогда нужно убрать дубликаты, многие сразу пишут:
sort file.txt | uniq
Работает. Но только если понимать, что именно делает каждая команда.
uniqРаспространённое заблуждение -
uniq ищет все повторяющиеся строки.На самом деле он удаляет только соседние дубликаты.
Например:
apple
banana
apple
orange
Если выполнить:
uniq file.txt
получим тот же самый список - одинаковые строки не стоят рядом.
sortПосле сортировки одинаковые строки оказываются соседними:
sort file.txt | uniq
Результат:
apple
banana
orange
Именно поэтому эти команды почти всегда используются вместе.
Если порядок строк важен,
sort изменит его.Например, при анализе логов или событий по времени это может полностью исказить картину.
В таких случаях лучше использовать:
awk '!seen[$0]++'
Команда удаляет дубликаты, но сохраняет исходный порядок строк.
Иногда нужны не уникальные записи, а наоборот - только дубликаты:
sort file.txt | uniq -d
Или вывести количество повторений:
sort file.txt | uniq -c
Получим:
15 nginx
8 sshd
3 cron
uniq кажется простой утилитой, но ошибка в понимании её работы часто приводит к неверным результатам. Перед использованием всегда стоит помнить: она сравнивает только соседние строки, а не весь файл целиком.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
Проверка доступности локальных Unix-сервисов:
Не все сервисы в Linux слушают TCP-порты. Многие приложения работают только через UNIX-сокеты - локальные каналы связи между процессами.
Например:
Снаружи такие сервисы не видны через обычный
▪️ Найти активные UNIX-сокеты
Пример:
Теперь можно проверить, отвечает ли сервис.
▪️ Проверка через
Netcat умеет подключаться к UNIX-сокетам:
Если соединение установлено - сокет доступен.
Для HTTP-сервисов можно отправить запрос вручную:
▪️ Проверка HTTP API через
Многие локальные API работают через UNIX-сокеты:
Здесь
▪️ Пример с Docker
Вместо:
можно напрямую обратиться к API:
Ответ:
означает, что Docker daemon доступен.
▪️ Проверка прав доступа
Даже если сервис работает, доступ может быть ограничен:
Например:
Только пользователи группы
▪️ Почему это важно
UNIX-сокеты часто остаются вне стандартного мониторинга. Администратор проверяет открытые порты, но забывает про локальные API и IPC-каналы.
Проверка через
жив ли сервис;
кто имеет к нему доступ;
отвечает ли локальное API;
нет ли неожиданного открытого канала между процессами.
BashTex📱 #sort
nc -U и curl --unix-socketНе все сервисы в Linux слушают TCP-порты. Многие приложения работают только через UNIX-сокеты - локальные каналы связи между процессами.
Например:
/run/docker.sock
/run/php/php-fpm.sock
/run/containerd/containerd.sock
Снаружи такие сервисы не видны через обычный
nmap, но внутри системы они могут иметь важное значение.ss -xl
Пример:
u_str LISTEN 0 128 /run/docker.sock
Теперь можно проверить, отвечает ли сервис.
ncNetcat умеет подключаться к UNIX-сокетам:
nc -U /run/docker.sock
Если соединение установлено - сокет доступен.
Для HTTP-сервисов можно отправить запрос вручную:
printf "GET / HTTP/1.0\r\n\r\n" | nc -U /run/service.sock
curlМногие локальные API работают через UNIX-сокеты:
curl --unix-socket /run/docker.sock http://localhost/version
Здесь
localhost не используется как сетевой адрес - он нужен только для формирования HTTP-запроса.Вместо:
docker version
можно напрямую обратиться к API:
curl --unix-socket /var/run/docker.sock \
http://localhost/_ping
Ответ:
OK
означает, что Docker daemon доступен.
Даже если сервис работает, доступ может быть ограничен:
ls -l /run/docker.sock
Например:
srw-rw---- root docker docker.sock
Только пользователи группы
docker смогут подключиться.UNIX-сокеты часто остаются вне стандартного мониторинга. Администратор проверяет открытые порты, но забывает про локальные API и IPC-каналы.
Проверка через
ss, nc -U и curl --unix-socket помогает быстро понять:жив ли сервис;
кто имеет к нему доступ;
отвечает ли локальное API;
нет ли неожиданного открытого канала между процессами.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
eval: почему опасен и когда реально нуженВ Bash почти любую строку можно превратить в команду через
eval. Он берёт переданный текст и запускает его как новый Bash-код.Именно поэтому это один из самых спорных инструментов в shell-скриптах.
evalПростой пример:
cmd="ls -la"
eval "$cmd"
Bash сначала получает строку:
ls -la
а затем выполняет её как обычную команду.
Если данные приходят от пользователя:
read inputeval "$input"
Теперь введённый текст становится кодом.
Например, вместо ожидаемого аргумента можно передать дополнительные команды, которые Bash выполнит при обработке строки.
Главная проблема
eval - повторный разбор данных как команд.Например:
file="test.txt"
eval "cat $file"
Пока значение простое - всё работает.
Но если внутри появятся специальные символы:
file="file name.txt"или другие управляющие конструкции, поведение может отличаться от ожидаемого.
evalВместо хранения команды строкой:
cmd="ls -la /tmp"
eval "$cmd"
лучше использовать массив:
cmd=(ls -la /tmp)
"${cmd[@]}"
Теперь Bash хранит команду и аргументы отдельно.
eval действительно нуженИногда без него сложно обойтись. Например, при работе с динамическими именами переменных:
name="USER"eval "echo \$$name"
Но даже здесь часто есть более безопасные варианты:
declare -n ref="$name"
echo "$ref"
Если всё же нужен
eval, данные должны быть полностью контролируемыми:case "$action" in
start|stop|restart)
eval "service_$action"
;;
esac
Здесь значение ограничено заранее известным списком.
eval не является “плохой” командой сам по себе. Проблема появляется, когда данные превращаются в код.В большинстве случаев массивы,
case, функции или declare решают задачу безопаснее. eval стоит оставлять только для ситуаций, где действительно нужен второй проход парсинга Bash.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
Проверка времени отклика сервисов
Когда сервис работает, но пользователи жалуются на медлительность, нужно мерить не аптайм, а отклик. Это легко сделать обычным curl.
▪️ Самый простой замер
Показывает общее время выполнения запроса и быстро понять, тормозит или нет.
▪️ Точнее: только сетевое время
Полезные метрики:
Пример:
▪️ Проверка нескольких сервисов
▪️ Таймаут обязателен
Без таймаута любой мониторинг бесполезен.
BashTex📱 #bash #utils
Когда сервис работает, но пользователи жалуются на медлительность, нужно мерить не аптайм, а отклик. Это легко сделать обычным curl.
time curl -s https://bashtex.com > /dev/null
Показывает общее время выполнения запроса и быстро понять, тормозит или нет.
curl -s -o /dev/null -w "time_total: %{time_total}\n" https://bashtex.com
Полезные метрики:
time_namelookuptime_connecttime_starttransfertime_totalПример:
curl -w "DNS:%{time_namelookup} CONNECT:%{time_connect} TTFB:%{time_starttransfer} TOTAL:%{time_total}\n" \
-o /dev/null -s https://bashtex.com
for url in https://a.ru https://b.ru; do
curl -o /dev/null -s -w "$url %{time_total}\n" "$url"
done
curl --connect-timeout 3 --max-time 5 https://bashtex.com
Без таймаута любой мониторинг бесполезен.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
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
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
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
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 это может иметь значение для поведения процессов.
setsidsetsid ./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
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
Это каталог, относительно которого процесс разрешает относительные пути.
cwdfor 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
если для конкретного сервиса такое поведение не ожидается.
Для unit можно задать:
[Service]
WorkingDirectory=/var/lib/myapp
А фактическое значение уже запущенного процесса проверить через
/proc.Это полезнее, чем тупо смотреть конфигурацию:
/proc/PID/cwd показывает состояние конкретного процесса прямо сейчас.BashTex
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
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 не создаёт обычный 1if 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
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, которая меняет это поведение.lastpipeshopt -s lastpipe
Теперь последняя команда pipeline может выполняться в текущем shell.
count=0
printf '%s\n' a b c |
while read -r line; do
((count++))
done
echo "$count"
При подходящих условиях получим:
3lastpipe работает только когда 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
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
Это легко превращается в ошибку в автоматизации.
nullglobshopt -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
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 -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