Bash: створити анонімну фіфо


38

Ми всі знаємо mkfifoі трубопроводи. Перший створює іменовану трубку, таким чином, потрібно вибрати ім'я, швидше за все, mktempі пізніше пам'ятати, щоб від’єднатись. Інший створює анонімну трубку, без клопоту з іменами та видаленням, але кінці труби прив’язуються до команд у конвеєрі, це не дуже зручно якось отримати зчеплення дескрипторів файлів та використовувати їх у іншому сценарію. У складеній програмі я просто зробив би ret=pipe(filedes); у Баша є exec 5<>fileтаке, що можна було б очікувати на щось подібне "exec 5<> -"або-чи "pipe <5 >6"є щось подібне у Баша?

Відповіді:


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<&-

Ви можете поєднати це з автоматичним виведенням невикористаних дескрипторів файлів: 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 У процесі заміщення процесу ім'я файлу - це "магічна" посилання на трубу (або тимчасовий файл для варіантів Unix, які не підтримують ці магічні варіанти). Як реалізується магія, залежить від ОС. Linux реалізує їх як "магічні" символічні посилання, метою яких не є дійсне ім'я файлу (це щось на кшталт pipe:[123456]). Emacs бачить, що цільовим символом посилання є не існуюче ім'я файлу, і це досить заплутує його, щоб він не читав файл (можливо, є можливість змусити його прочитати його все одно, хоча Emacs не любить відкривати трубу як файл все одно).
Жил "ТАК - перестань бути злим"

10

Bash 4 має копроцеси .

Копроцес виконується асинхронно в підклітині, як ніби команда була припинена з оператором управління '&', з двосторонньою трубою, встановленою між оболонкою виконавця та копроцесом.

Формат копроцесу:

coproc [NAME] command [redirections] 

3

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


Не підтримується лише один копроцес. Ви можете назвати їх, доки ви не надасте просту команду. Надайте замість цього список команд: coproc THING { dothing; }Тепер ваші FD є, ${THING[*]}і ви можете запускати coproc OTHERTHING { dothing; }та надсилати та отримувати речі з обох.
clacke

2
@clacke man bash, під заголовком BUGS, вони говорять так: Одночасно може бути лише один активний копроцес . І ви отримаєте попередження, якщо запустите другий копрок. Це, здається, працює, але я не знаю, що вибухає на задньому плані.
Radu C

Гаразд, на даний момент це працює лише на удачу, а не тому, що це було навмисно. Справедливе попередження, дякую. :-)
клак

2

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

Отже, найкоротший і простий відповідь на ваше запитання:

exec 5<> <(:)

У macOS він не працюватиме, тоді вам потрібно буде створити тимчасовий каталог для розміщення названого fifo, поки ви не будете перенаправлені на нього. Я не знаю про інші BSD.


Ви розумієте, що ваша відповідь працює лише через помилку в Linux. Ця помилка не існує в macOS, що вимагає більш складного рішення. Остаточна версія, яку я розмістив, працюватиме в Linux, навіть якщо помилка в Linux виправлена.
Девід Андерсон

@DavidAnderson Здається, що ви глибше знаєте про це, ніж я. Чому поведінка Linux є помилкою?
clacke

1
Якщо execпередається та анонімна фіфо, відкрита лише для читання, execне повинна дозволяти відкривати цю анонімну фіфо для читання та запису за допомогою спеціального дескриптора файлів. Ви повинні розраховувати на отримання -bash: /dev/fd/5: Permission deniedповідомлення, яке саме питання видає macOS. Я вважаю, що помилка в тому, що Ubuntu не видає те саме повідомлення. Я хотів би змінити свою думку, якби хтось міг виготовити документацію, в якій говориться, що exec 5<> <(:)це дозволено.
Девід Андерсон

@DavidAnderson Нічого собі, це захоплююче. Я припускав, що bash щось робив всередині, але виявляється, що Linux дозволяє просто робити open(..., O_RDWR)на одному односпрямованому кінці труби, що забезпечується заміною, і що перетворює це на двонаправлену трубку в одному FD. Ви, мабуть, праві, що не варто на це покладатися. :-D Вихід з використання piperw execline для створення труби, а потім перестановки її на bash <>: librarynet.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>&-"

Посилання:
Створення анонімної труби для подальшого використання
файлів у загальнодоступних каталогах небезпечно для
скриптів оболонки


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
Я не можу не зауважити , що ваш один вкладиш має більше за один рядок.
Дмитро Григор’єв
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.