Зачем
Часто команда хочет файл на вход, а у тебя есть команда которая
выводит данные. Классический пример: diff сравнивает два файла, а ты
хочешь сравнить вывод двух команд.
Без process substitution - приходится писать во временные файлы:
ls /etc | sort > /tmp/a.txt
ls /usr | sort > /tmp/b.txt
diff /tmp/a.txt /tmp/b.txt
rm /tmp/a.txt /tmp/b.txt # не забыть убрать
С process substitution - одна строка:
diff <(ls /etc | sort) <(ls /usr | sort)
Как это работает
<(cmd) под капотом - это /dev/fd/N (где N - какой-то FD,
обычно 63 и далее). Bash:
- Запускает
cmdв подпроцессе - Создаёт пайп
- Подставляет в командную строку путь
/dev/fd/63который читает с этого пайпа
Команда (diff) открывает этот «файл» как обычный путь - и читает
вывод подпроцесса. Никаких временных файлов на диске не создаётся.
echo <(ls)
▸/dev/fd/63
cat <(echo hello)
▸hello
Симметричный вариант: >(cmd)
>(cmd) - наоборот, псевдо-файл на запись, в который команда
читает stream:
echo "data" | tee >(wc -c > /tmp/byte-count.txt) >/dev/null
▸tee пишет копию в подпроцесс wc -c
▸wc -c считает байты и сохраняет в файл
Применение - раздать вывод одной команды в несколько обработчиков параллельно:
some_command | tee >(grep ERROR > errors.log) >(grep WARN > warns.log) > /dev/null
Где НЕ работает
- POSIX
/bin/sh- нет process substitution. Только bash и zsh. - Pipe в process substitution - exit-code теряется. Команда
внутри
<(...)упала, а скрипт этого не заметит даже сset -e. Если важен exit-code - пиши в файл и проверяй явно. - Через
ssh-ssh host '<(cmd)'не сработает потому что выполняется на удалённой стороне которая может не быть bash.
Идиомы которые точно встретятся
# Сравнить два каталога по содержимому
diff <(ls -la /etc) <(ls -la /etc.bak)
# Проверить что вывод команды совпадает с файлом
diff <(my_program) expected.txt && echo OK
# Записать STDOUT и STDERR в РАЗНЫЕ файлы (без 2>&1)
cmd > >(tee out.log) 2> >(tee err.log >&2)
# Использовать while read с подсчётом совпадений (НЕ через pipe - иначе
# переменная теряется в subshell)
count=0
while read -r line; do
((count++))
done < <(grep ERROR app.log)
echo "$count" # ← переменная сохранилась, потому что НЕТ pipe
Последний случай - частая ловушка: grep ... | while read; do ((count++)); done
не работает (count останется 0), потому что pipe запускает while в subshell.
Process substitution это лечит.