Как можно различать два конвейера в Bash?

143

Как можно различать два конвейера без использования временных файлов в Bash? Допустим, у вас есть два командных конвейера:

foo | bar
baz | quux

И вы хотите найти diff в их выходах. Очевидно, одно из решений:

foo | bar > /tmp/a
baz | quux > /tmp/b
diff /tmp/a /tmp/b

Можно ли сделать это без использования временных файлов в Bash? Вы можете избавиться от одного временного файла, подключив один из конвейеров к diff:

foo | bar > /tmp/a
baz | quux | diff /tmp/a -

Но вы не можете передать оба конвейера в diff одновременно (по крайней мере, не очевидным образом). Есть ли какой-нибудь хитрый трюк, /dev/fdчтобы сделать это без использования временных файлов?

Адам Розенфилд
источник

Ответы:

146

Однострочный файл с 2 файлами tmp (не то, что вы хотите) будет:

 foo | bar > file1.txt && baz | quux > file2.txt && diff file1.txt file2.txt

Однако с помощью bash вы можете попробовать:

 diff <(foo | bar) <(baz | quux)

 foo | bar | diff - <(baz | quux)  # or only use process substitution once

Вторая версия будет более четко напоминать вам, какой вход был какой, показывая
-- /dev/stdinvs. ++ /dev/fd/63или что-то в этом роде, вместо двух пронумерованных файловых файлов.


В файловой системе не появится даже именованный канал, по крайней мере, в операционных системах, где bash может реализовать подстановку процессов, используя имена файлов, например, /dev/fd/63получение имени файла, которое команда может открыть и прочитать, чтобы фактически прочитать из уже открытого файлового дескриптора, установленного bash вверх перед выполнением команды. (т.е. bash использует pipe(2)перед fork, а затем dup2для перенаправления с выводаquux с дескриптора входного файла diffна fd 63.)

В системе без «магических» /dev/fdили/proc/self/fd bash может использовать именованные каналы для реализации подстановки процессов, но он, по крайней мере, сам будет управлять ими, в отличие от временных файлов, и ваши данные не будут записаны в файловую систему.

Вы можете проверить, как bash реализует замену процесса, с помощью echo <(true)печати имени файла вместо чтения из него. Он печатается /dev/fd/63в типичной системе Linux. Или для получения более подробной информации о том, какие именно системные вызовы использует bash, эта команда в системе Linux будет отслеживать системные вызовы файлов и файловых дескрипторов.

strace -f -efile,desc,clone,execve bash -c '/bin/true | diff -u - <(/bin/true)'

Без bash можно было бы создать именованный канал . Используйте, -чтобы указать, что diffнужно читать один ввод из STDIN и использовать именованный канал в качестве другого:

mkfifo file1_pipe.txt
foo|bar > file1_pipe.txt && baz | quux | diff file1_pipe.txt - && rm file1_pipe.txt

Обратите внимание, что вы можете направить только один выход на несколько входов с помощью команды tee:

ls *.txt | tee /dev/tty txtlist.txt 

Приведенная выше команда отображает вывод ls * .txt на терминал и выводит его в текстовый файл txtlist.txt.

Но с заменой процесса вы можете использовать teeдля подачи одних и тех же данных в несколько конвейеров:

cat *.txt | tee >(foo | bar > result1.txt)  >(baz | quux > result2.txt) | foobar
VonC
источник
5
даже без bash, вы можете использовать временные mkfifo a; cmd >a& cmd2|diff a -; rm a
фифо
Вы можете использовать обычную трубу для одного из аргументов: pipeline1 | diff -u - <(pipeline2). Тогда выходные данные будут более четко напоминать вам, какой вход был какой, показывая -- /dev/stdinvs. ++ /dev/fd/67или что-то в этом роде, вместо двух пронумерованных файловых файлов.
Питер Кордес
замена процесса ( foo <( pipe )) не изменяет файловую систему. Труба анонимная ; у него нет имени в файловой системе . Для pipeего создания оболочка использует системный вызов, а не mkfifo. Используйте strace -f -efile,desc,clone,execve bash -c '/bin/true | diff -u - <(/bin/true)'для отслеживания системных вызовов файлов и файловых дескрипторов, если хотите в этом убедиться. В Linux /dev/fd/63это часть /procвиртуальной файловой системы; в нем автоматически есть записи для каждого дескриптора файла, и он не является копией содержимого. Так что вы не можете называть это «временным файлом», если не foo 3<bar.txtсчитать
Питер Кордес
@PeterCordes Хорошие отзывы. Я включил ваш комментарий в ответ для большей наглядности.
VonC
1
@PeterCordes Я оставлю вам любые правки: это то, что делает Stack Overflow интересным: любой может "исправить" ответ.
VonC
128

В bash вы можете использовать подоболочки для индивидуального выполнения командных конвейеров, заключив конвейер в круглые скобки. Затем вы можете добавить к ним префикс <, чтобы создать анонимные именованные каналы, которые затем можно передать в diff.

Например:

diff <(foo | bar) <(baz | quux)

Анонимные именованные каналы управляются bash, поэтому они создаются и уничтожаются автоматически (в отличие от временных файлов).

BenM
источник
1
Намного более подробно, чем моя редакция того же решения - анонимной партии -. +1
VonC
4
В Bash это называется подстановкой процесса .
Франклин Ю,
5

Некоторые люди, попадающие на эту страницу, могут искать построчное сравнение, для которого commилиgrep -f должны использоваться вместо этого.

Следует отметить, что во всех примерах ответов различие фактически не запускается, пока оба потока не закончатся. Проверьте это, например:

comm -23 <(seq 100 | sort) <(seq 10 20 && sleep 5 && seq 20 30 | sort)

Если это проблема, вы можете попробовать sd (stream diff), который не требует сортировки (как это commделает) или подстановки процессов, как в приведенных выше примерах, на порядки или величины быстрее, чем grep -f и поддерживает бесконечные потоки.

Предлагаемый мной тестовый пример будет записан так sd:

seq 100 | sd 'seq 10 20 && sleep 5 && seq 20 30'

Но разница в том, seq 100что сразу разобрались бы seq 10. Обратите внимание, что если один из потоков - это a tail -f, различие не может быть выполнено с заменой процесса.

Вот пост блоге, который я написал о различии потоков на терминале, который представляет sd.

млг
источник