Bash: создать анонимный fifo

38

Мы все знаем mkfifoи трубопроводы. Первый создает именованный канал, поэтому нужно выбрать имя, скорее всего, с mktempпоследующим запоминанием, чтобы не связывать его. Другой создает анонимный канал, без проблем с именами и удалением, но концы канала привязываются к командам в конвейере, не очень удобно как-то захватывать файловые дескрипторы и использовать их в остальных сценария. В скомпилированной программе я бы просто сделал ret=pipe(filedes); в Bash есть exec 5<>fileтак, что можно ожидать чего-то подобного, "exec 5<> -"или "pipe <5 >6"- есть ли что-то подобное в Bash?

Адриан Панасюк
источник

Ответы:

42

Вы можете отсоединить именованный канал сразу после присоединения его к текущему процессу, что практически приводит к анонимному каналу:

# create a temporary named pipe
PIPE=$(mktemp -u)
mkfifo $PIPE
# attach it to file descriptor 3
exec 3<>$PIPE
# unlink the named pipe
rm $PIPE
...
# anything we write to fd 3 can be read back from it
echo 'Hello world!' >&3
head -n1 <&3
...
# close the file descriptor when we are finished (optional)
exec 3>&-

Если вы действительно хотите избежать именованных каналов (например, файловая система доступна только для чтения), ваша идея «получить контроль над дескрипторами файлов» также работает. Обратите внимание, что это специфично для Linux из-за использования procfs.

# start a background pipeline with two processes running forever
tail -f /dev/null | tail -f /dev/null &
# save the process ids
PID2=$!
PID1=$(jobs -p %+)
# hijack the pipe's file descriptors using procfs
exec 3>/proc/$PID1/fd/1 4</proc/$PID2/fd/0
# kill the background processes we no longer need
# (using disown suppresses the 'Terminated' message)
disown $PID2
kill $PID1 $PID2
...
# anything we write to fd 3 can be read back from fd 4
echo 'Hello world!' >&3
head -n1 <&4
...
# close the file descriptors when we are finished (optional)
exec 3>&- 4<&-
htamas
источник
Вы можете комбинировать это с автоматическим поиском неиспользуемых файловых дескрипторов: stackoverflow.com/questions/8297415/…
CMCDragonkai
23

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

В bash, ksh и zsh, предполагая, что ваша система поддерживает /dev/fd(большинство в настоящее время поддерживают ), вы можете связать ввод или вывод команды с именем файла: <(command)расширяется до имени файла, обозначающего канал, подключенный к выходу command, и >(command)расширяется к имени файла, который обозначает канал, связанный с входом command. Эта функция называется процессом замены . Его основная цель состоит в том, чтобы направить более одной команды в другую или из нее, например,

diff <(transform <file1) <(transform <file2)
tee >(transform1 >out1) >(transform2 >out2)

Это также полезно для борьбы с некоторыми недостатками основных оболочек труб. Например, command2 < <(command1)эквивалентно command1 | command2, за исключением того, что его статус является статусом command2. Другой вариант использования exec > >(postprocessing), который эквивалентен, но более удобен для чтения, чем помещение всего остального скрипта внутрь { ... } | postprocessing.

Жиль "ТАК - прекрати быть злым"
источник
Я попробовал это с diff, и это сработало, но с kdiff3 или с emacs не сработало. Я предполагаю, что временный файл / dev / fd удаляется до того, как kdiff3 сможет его прочитать. Или, возможно, kdiff3 пытается прочитать файл дважды, а канал отправляет его только один раз?
Eyal
@Eyal В процессе установки имя файла представляет собой «магическую» ссылку на канал (или временный файл в вариантах Unix, которые не поддерживают эти магические варианты). Как реализуется магия, зависит от ОС. Linux реализует их как «магические» символические ссылки, цель которых не является допустимым именем файла (это что-то вроде pipe:[123456]). Emacs видит, что целью символической ссылки не является существующее имя файла, и это настолько запутывает его, что он не читает файл (может быть, в любом случае, есть возможность заставить его прочитать его, хотя Emacs не любит открывать канал как файл в любом случае).
Жиль "ТАК - перестань быть злым"
10

Bash 4 имеет сопроцессы .

Копроцесс выполняется в подоболочке асинхронно, как если бы команда была прервана оператором управления '&' с двусторонним каналом, установленным между исполняющей оболочкой и сопроцессом.

Формат для сопроцесса:

coproc [NAME] command [redirections] 
Приостановлено до дальнейшего уведомления.
источник
3

По состоянию на октябрь 2012 г. эта функциональность еще не существует в Bash, но можно использовать coproc, если все, что вам нужно для безымянных / анонимных каналов, - это поговорить с дочерним процессом. Проблема с coproc на данный момент заключается в том, что, по-видимому, одновременно поддерживается только один. Я не могу понять, почему у coproc такое ограничение. Они должны были улучшить существующий фоновый код задачи (& op), но это вопрос для авторов bash.

Раду С
источник
Поддерживается не только один сопроцесс. Вы можете назвать их, пока вы не предоставите простую команду. Вместо этого дайте ему список команд: coproc THING { dothing; }теперь ваши FD находятся в системе, ${THING[*]}и вы можете запускать, coproc OTHERTHING { dothing; }отправлять и получать данные в обе и из них.
щелчок
2
@clacke man bash, под заголовком BUGS, они говорят следующее: за один раз может быть только один активный сопроцесс . И вы получите предупреждение, если начнете второй копрок. Кажется, это работает, но я не знаю, что взрывается на заднем плане.
Раду C
Итак, в настоящее время это работает только по счастливой случайности, а не потому, что это было сделано намеренно. Честное предупреждение, спасибо. :-)
клик
2

Хотя ответ @ DavidAnderson охватывает все основы и предлагает несколько хороших мер защиты, самое важное, что он раскрывает, заключается в том, что получить анонимный канал так же просто <(:), как и до тех пор, пока вы остаетесь в Linux.

Итак, самый короткий и простой ответ на ваш вопрос:

exec 5<> <(:)

В macOS это не будет работать, тогда вам нужно будет создать временный каталог для размещения имени fifo, пока вы не будете перенаправлены на него. Я не знаю о других BSD.

clacke
источник
Вы понимаете, что ваш ответ работает только из-за ошибки в Linux. Эта ошибка не существует в macOS, поэтому требует более сложного решения. Последняя версия, которую я разместил, будет работать в Linux, даже если ошибка в Linux исправлена.
Дэвид Андерсон
@DavidAnderson Похоже, вы знаете об этом глубже, чем я. Почему поведение Linux является ошибкой?
щелчок
1
Если execпередается анонимный fifo, который открывается только для чтения, то execне следует разрешать открытие этого анонимного fifo для чтения и записи с использованием пользовательского дескриптора файла. Вы должны получить -bash: /dev/fd/5: Permission deniedсообщение, которое выдает macOS. Я считаю, что ошибка в том, что Ubuntu не выдает такое же сообщение. Я был бы готов передумать, если бы кто-то мог представить документацию, говорящую, exec 5<> <(:)что объяснено разрешено.
Дэвид Андерсон
@DavidAnderson Ого, это восхитительно. Я предположил, что bash что-то делал внутри, но оказалось, что это Linux, который позволяет просто выполнять open(..., O_RDWR)на одном однонаправленном конце канала, обеспечиваемом заменой, и превращает его в двунаправленный канал в одном FD. Вы, вероятно, правы, что не стоит полагаться на это. :-D Вывод из использования piperw execline для создания канала, а затем его перепрофилирование с помощью bash <>: libranet.de/display/0b6b25a8-195c-84af-6ac7-ee6696661765
clacke
Не то чтобы это имело значение, но если вы хотите увидеть в Ubuntu, что передается exec 5<>, введите fun() { ls -l $1; ls -lH $1; }; fun <(:).
Дэвид Андерсон
1

Следующая функция была протестирована с помощью GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu). Операционная система была Ubuntu 18. Эта функция принимает один параметр, который является желаемым дескриптором файла для анонимного FIFO.

MakeFIFO() {
    local "MakeFIFO_upper=$(ulimit -n)" 
    if [[ $# -ne 1 || ${#1} -gt ${#MakeFIFO_upper} || -n ${1%%[0-9]*} || 10#$1 -le 2
        || 10#$1 -ge MakeFIFO_upper ]] || eval ! exec "$1<> " <(:) 2>"/dev/null"; then
        echo "$FUNCNAME: $1: Could not create FIFO" >&2
        return "1"
    fi
}

Следующая функция была протестирована с помощью GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17). Операционная система была macOS High Sierra. Эта функция запускается созданием именованного FIFO во временном каталоге, известном только процессу, который его создал . Затем файловый дескриптор перенаправляется в FIFO. Наконец, FIFO отсоединяется от имени файла путем удаления временного каталога. Это делает FIFO анонимным.

MakeFIFO() {
    MakeFIFO.SetStatus() {
        return "${1:-$?}"
    }
    MakeFIFO.CleanUp() {
        local "MakeFIFO_status=$?"
        rm -rf "${MakeFIFO_directory:-}"    
        unset "MakeFIFO_directory"
        MakeFIFO.SetStatus "$MakeFIFO_status" && true
        eval eval "${MakeFIFO_handler:-:}'; true'" 
    }
    local "MakeFIFO_success=false" "MakeFIFO_upper=$(ulimit -n)" "MakeFIFO_file=" 
    MakeFIFO_handler="$(trap -p EXIT)"
    MakeFIFO_handler="${MakeFIFO_handler#trap -- }"
    MakeFIFO_handler="${MakeFIFO_handler% *}"
    trap -- 'MakeFIFO.CleanUp' EXIT
    until "$MakeFIFO_success"; do
        [[ $# -eq 1 && ${#1} -le ${#MakeFIFO_upper} && -z ${1%%[0-9]*}
        && 10#$1 -gt 2 && 10#$1 -lt MakeFIFO_upper ]] || break
        MakeFIFO_directory=$(mktemp -d) 2>"/dev/null" || break
        MakeFIFO_file="$MakeFIFO_directory/pipe"
        mkfifo -m 600 $MakeFIFO_file 2>"/dev/null" || break
        ! eval ! exec "$1<> $MakeFIFO_file" 2>"/dev/null" || break
        MakeFIFO_success="true"
    done
    rm -rf "${MakeFIFO_directory:-}"
    unset  "MakeFIFO_directory"
    eval trap -- "$MakeFIFO_handler" EXIT
    unset  "MakeFIFO_handler"
    "$MakeFIFO_success" || { echo "$FUNCNAME: $1: Could not create FIFO" >&2; return "1"; }
}

Вышеуказанные функции могут быть объединены в одну функцию, которая будет работать в обеих операционных системах. Ниже приведен пример такой функции. Здесь делается попытка создать действительно анонимный FIFO. В случае неудачи именованный FIFO создается и преобразуется в анонимный FIFO.

MakeFIFO() {
    MakeFIFO.SetStatus() {
        return "${1:-$?}"
    }
    MakeFIFO.CleanUp() {
        local "MakeFIFO_status=$?"
        rm -rf "${MakeFIFO_directory:-}"    
        unset "MakeFIFO_directory"
        MakeFIFO.SetStatus "$MakeFIFO_status" && true
        eval eval "${MakeFIFO_handler:-:}'; true'" 
    }
    local "MakeFIFO_success=false" "MakeFIFO_upper=$(ulimit -n)" "MakeFIFO_file=" 
    MakeFIFO_handler="$(trap -p EXIT)"
    MakeFIFO_handler="${MakeFIFO_handler#trap -- }"
    MakeFIFO_handler="${MakeFIFO_handler% *}"
    trap -- 'MakeFIFO.CleanUp' EXIT
    until "$MakeFIFO_success"; do
        [[ $# -eq 1 && ${#1} -le ${#MakeFIFO_upper} && -z ${1%%[0-9]*}
        && 10#$1 -gt 2 && 10#$1 -lt MakeFIFO_upper ]] || break
        if eval ! exec "$1<> " <(:) 2>"/dev/null"; then
            MakeFIFO_directory=$(mktemp -d) 2>"/dev/null" || break
            MakeFIFO_file="$MakeFIFO_directory/pipe"
            mkfifo -m 600 $MakeFIFO_file 2>"/dev/null" || break
            ! eval ! exec "$1<> $MakeFIFO_file" 2>"/dev/null" || break
        fi
        MakeFIFO_success="true"
    done
    rm -rf "${MakeFIFO_directory:-}"
    unset  "MakeFIFO_directory"
    eval trap -- "$MakeFIFO_handler" EXIT
    unset  "MakeFIFO_handler"
    "$MakeFIFO_success" || { echo "$FUNCNAME: $1: Could not create FIFO" >&2; return "1"; }
}

Вот пример создания анонимного FIFO, а затем записи некоторого текста в тот же FIFO.

fd="6"
MakeFIFO "$fd"
echo "Now is the" >&"$fd"
echo "time for all" >&"$fd"
echo "good men" >&"$fd"

Ниже приведен пример чтения всего содержимого анонимного FIFO.

echo "EOF" >&"$fd"
while read -u "$fd" message; do
    [[ $message != *EOF ]] || break
    echo "$message"
done

Это производит следующий вывод.

Now is the
time for all
good men

Команда ниже закрывает анонимный FIFO.

eval exec "$fd>&-"

Ссылки:
Создание анонимных труб для последующего использования
файлов в Публично записи каталогов опасен
Shell Script Security

Дэвид Андерсон
источник
0

Используя великолепный и яркий ответ от htamas, я немного изменил его, чтобы использовать его в один слой, вот он:

# create a temporary named pipe
PIPE=(`(exec 0</dev/null 1</dev/null; (( read -d \  e < /proc/self/stat ; echo $e >&2 ; exec tail -f /dev/null 2> /dev/null ) | ( read -d \  e < /proc/self/stat ; echo $e  >&2 ; exec tail -f /dev/null 2> /dev/null )) &) 2>&1 | for ((i=0; i<2; i++)); do read e; printf "$e "; done`)
# attach it to file descriptors 3 and 4
exec 3>/proc/${PIPE[0]}/fd/1 4</proc/${PIPE[1]}/fd/0
...
# kill the temporary pids
kill ${PIPE[@]}
...
# anything we write to fd 3 can be read back from fd 4
echo 'Hello world!' >&3
head -n1 <&4
...
# close the file descriptor when we are finished (optional)
exec 3>&- 4<&-
Луис Фелипе Сильва
источник
7
Не могу не заметить, что у твоей однострочной строки более одной строки.
Дмитрий Григорьев