// заметки
pkill -f убивает сам себя: шаблон совпадает с командой
pkill -f и pgrep -f сопоставляют шаблон со всей командной строкой, включая обёртку bash -c. Разбираю, почему команда убивает себя, и какие фильтры помогают
Скрипт обрывается на полуслове с [killed by signal: SIGTERM], хотя kill по собственному PID в нём нет. pgrep -f сопоставляет шаблон не с именем программы, а со всей командной строкой процесса, и в неё попадает текст обёртки bash -c, внутри которой этот скрипт запущен
Ниже — диагностика, разбор причины и три замены для -f
Симптом
Команда завершается без трейсбека: вывод обрывается на середине, в логе остаётся только маркер сигнала
$ ./restart-agent.sh
останавливаю старый процесс
[killed by signal: SIGTERM]
$ echo $?
143Код 143 — это 128 + 15, то есть SIGTERM. OOM killer сюда не подходит: он присылает SIGKILL и код 137. Значит, сигнал отправил кто-то из своих же команд в том же скрипте
Характерная деталь: руками в интерактивной оболочке скрипт отрабатывает, а из обёртки bash -c падает. Так команду запускают раннеры CI, инструментальные shell и системы сборки: весь скрипт приходит одним аргументом
Диагностика
Начните с того, кого реально выбирает шаблон. Флаг -a заставляет pgrep печатать PID и полную командную строку каждого совпадения
$ pgrep -a -f 'authentication-agent'
4242 bash -c PAT='authentication-agen[t]'; old=$(pgrep -f "$PAT"); ...
4301 /usr/lib/polkit-kde-authentication-agent-1В списке есть обёртка bash -c с тем же текстом, что и в шаблоне. Это и есть процесс, который получает сигнал. Свою командную строку можно посмотреть напрямую
$ echo $$
4242
$ tr '\0' ' ' < /proc/$$/cmdline; echo
bash -c PAT='authentication-agen[t]'; old=$(pgrep -f "$PAT"); ...Файл /proc/<pid>/cmdline хранит аргументы через нулевой байт, pgrep склеивает их пробелами и применяет шаблон как расширенное регулярное выражение. Отдельной проверки, что совпадение пришлось на имя программы, при -f нет
Причина
pgrep -f обходит все процессы, читает /proc/<pid>/cmdline и проверяет шаблон против всей строки аргументов. Для процесса, запущенного как bash -c '<скрипт>', эта строка содержит скрипт целиком: и присваивание PAT='authentication-agen[t]', и путь /usr/lib/polkit-kde-authentication-agent-1
Приём с квадратными скобками рассчитан на один случай. Шаблон authentication-agen[t] — это регулярное выражение, где [t] обозначает класс из одного символа: он совпадает с текстом authentication-agent, но не совпадает с текстом authentication-agen[t]
Пока шаблон встречается в командной строке только сам по себе, самоматча нет. Здесь authentication-agent есть второй раз, внутри пути к бинарю, и от этого вхождения скобки не защищают
$ printf '%s\n' "PAT='authentication-agen[t]'" "/usr/lib/polkit-kde-authentication-agent-1" \
| grep -E 'authentication-agen[t]'
/usr/lib/polkit-kde-authentication-agent-1Следующая ловушка — попытка исключить себя сравнением с $$. В bash $$ — это PID оболочки, которая стартовала, и в подоболочке он не меняется: в ( ... ), в конвейере cmd | while read и в подстановке $( ) переменная остаётся прежней. Реальный PID текущего процесса отдаёт $BASHPID
$ echo "оболочка: $$, текущий: $BASHPID"
оболочка: 4242, текущий: 4242
$ ( echo "оболочка: $$, текущий: $BASHPID" )
оболочка: 4242, текущий: 4388Если проверка [[ "$p" == "$$" ]] && continue выполнялась в подоболочке, она пропускает родителя, но не сам текущий процесс, и kill бьёт по нему
Решение
Надёжнее опираться не на текст аргументов, а на путь к реально исполняемому файлу. readlink /proc/<pid>/exe возвращает его, причём без обрезки
Цикл отбрасывает свои PID и убивает только процесс, чей exe указывает на агента. Сначала сухой прогон: вместо kill стоит echo
AGENT=/usr/lib/polkit-kde-authentication-agent-1
for p in $(pgrep -f 'authentication-agent' 2>/dev/null); do
[[ "$p" == "$$" || "$p" == "$BASHPID" ]] && continue
[[ "$(readlink /proc/$p/exe 2>/dev/null)" == "$AGENT" ]] && echo kill "$p"
doneСначала посмотрите список, потом уберите echo. В рабочем варианте добавляется проверка владельца: сигнал уйдёт только своему процессу
AGENT=/usr/lib/polkit-kde-authentication-agent-1
for p in $(pgrep -f 'authentication-agent' 2>/dev/null); do
[[ "$p" == "$$" || "$p" == "$BASHPID" ]] && continue
[[ "$(readlink /proc/$p/exe 2>/dev/null)" == "$AGENT" ]] || continue
[[ "$(stat -c %u /proc/$p)" == "$(id -u)" ]] && kill "$p"
doneДля чужого пользователя readlink вернёт пустую строку, а stat покажет другой UID, так что такой PID не пройдёт условие. Это безопасный отказ: лучше не убить, чем убить не то
Если имя программы уникально, хватит pkill -x. Без -f он сопоставляет шаблон с comm (имя исполняемого файла, как его видит ядро), а не с аргументами, и собственная обёртка в выборку не попадает
pkill -u "$USER" -x swaync 2>/dev/null || true
pkill -u "$USER" -SIGUSR2 waybar 2>/dev/null || trueswaync и waybar — панели Wayland, а SIGUSR2 для waybar означает «перечитать конфиг»
comm обрезается до 15 символов. Для /usr/lib/polkit-kde-authentication-agent-1 в /proc/<pid>/comm лежит polkit-kde-auth, поэтому pkill -x polkit-kde-authentication-agent-1 не найдёт ничего
Третий случай — проверить, жив ли процесс, который запустил сам скрипт. Поиск по имени для этого не нужен: PID последнего фонового задания лежит в $!
cmd & AP=$!
sleep 1
kill -0 "$AP" 2>/dev/null && echo жив || echo умерkill -0 не отправляет сигнал, а только проверяет, что процесс существует и доступен. На чужом PID он тоже вернёт ошибку, так что как проверка «жив ли» этот тест годится только для своих процессов
Проверка
Проверки идут от общего к частному: кто совпадает с шаблоном, есть ли среди них обёртка и что вернул pgrep
# 1) кто совпадает с шаблоном
pgrep -a -f 'authentication-agent'
# 2) попала ли в выборку обёртка
pgrep -a -f 'authentication-agent' | grep -q 'bash -c' \
&& echo 'обёртка в выборке' || echo 'обёртки нет'
# 3) код возврата pgrep
pgrep -f 'authentication-agent'; echo "rc=$?"Код возврата pgrep: 0 — совпадения есть, 1 — совпадений нет; единица не ошибка, а нормальный ответ. Проверьте также сам фильтр по exe на живом процессе
$ readlink /proc/4301/exe
/usr/lib/polkit-kde-authentication-agent-1Воспроизводите сценарий в той же обёртке, в которой он падал, через bash -c '...'. В интерактивной оболочке командная строка короче, и матч может не появиться
Подводные камни
commобрезается до 15 символов, поэтомуpkill -xпо длинному имени молча не находит ничего: сверяйтесь с/proc/<pid>/commперед тем, как писать шаблонpkill -fзадевает посторонние процессы с похожим текстом в аргументах: редактор с открытым скриптом,grepпо нему,tail -fлога- Пустой результат
pgrepи «процесса нет» неразличимы в цепочкеpgrep ... && kill: код 1 молча пропускает всю логику, и ошибка вылезает позже - Между
pgrepиkillPID может достаться другому процессу. Для короткой паузы риск мал, для длинной берите$!или pid-файл - Сравнение с
$$в подоболочке не исключает текущий процесс: добавляйте$BASHPID - Если список PID пуст,
kill $oldбез проверки на пустоту печатает usage и возвращает 2, а подset -eэто обрывает скрипт - Если матч явно чужой и непонятно, откуда взялся процесс, помогает ход по родительскому PID: Кто вызывает unshare
Итог
-f в pgrep и pkill означает «искать по всей командной строке», а не «по имени программы». Команда, запущенная через bash -c, содержит свой текст в аргументах и попадает в собственный шаблон, а квадратные скобки помогают только при единственном вхождении. Для остановки по имени берите pkill -x с обрезанным comm, для точной цели — readlink /proc/<pid>/exe, для своего процесса — $! и kill -0
Похожие заметки
Linux: «команда не найдена» — PATH, ifconfig, pip
command not found в Linux: как найти бинарь по PATH, почему sudo и login shell видят другой путь и что поставить вместо ifconfig и pip — разбор с командами
Кто вызывает unshare: перехват бинаря через PATH
Команда падает и не называет виновника. Показываю приём: подменить бинарь логирующим скриптом через PATH и найти вызывающего без трассировки и прав root
Nix на CachyOS: установка невозможна — rootless-запуск
Nix не ставится, если корень read-only, sudo сломан, а путь /nix зашит в ELF. Разбор трёх ограничений и rootless-запуск через mount-namespace и chroot