Bash: crea un Fifo anónimo


38

Todos lo sabemos mkfifoy las tuberías. El primero crea una tubería con nombre , por lo tanto, uno tiene que seleccionar un nombre, lo más probable es que mktempluego recuerde desvincular. El otro crea una tubería anónima, sin problemas con los nombres y la eliminación, pero los extremos de la tubería se atan a los comandos en la tubería, no es realmente conveniente obtener de alguna manera un control de los descriptores de archivos y usarlos en el resto del guión En un programa compilado, simplemente haría ret=pipe(filedes); en Bash hay, exec 5<>fileasí que uno esperaría algo así "exec 5<> -"o, ¿ "pipe <5 >6"hay algo así en Bash?

Respuestas:


42

Puede desvincular una tubería con nombre inmediatamente después de adjuntarla al proceso actual, lo que prácticamente resulta en una tubería anónima:

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

Si realmente desea evitar las canalizaciones con nombre (por ejemplo, el sistema de archivos es de solo lectura), su idea de "obtener un control de los descriptores de archivos" también funciona. Tenga en cuenta que esto es específico de Linux debido al uso de 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<&-

Puede combinar esto con la búsqueda automática de descriptores de archivo no utilizados: stackoverflow.com/questions/8297415/…
CMCDragonkai

23

Si bien ninguno de los proyectiles que conozco puede fabricar tuberías sin bifurcación, algunos tienen mejores que la tubería básica de proyectiles.

En bash, ksh y zsh, suponiendo que su sistema sea compatible /dev/fd(la mayoría lo hace hoy en día), puede vincular la entrada o la salida de un comando a un nombre de archivo: se <(command)expande a un nombre de archivo que designa una tubería conectada a la salida commandy se >(command)expande a un nombre de archivo que designa una tubería conectada a la entrada de command. Esta característica se llama sustitución de proceso . Su propósito principal es canalizar más de un comando dentro o fuera de otro, por ejemplo,

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

Esto también es útil para combatir algunas de las deficiencias de los tubos básicos. Por ejemplo, command2 < <(command1)es equivalente a command1 | command2, excepto que su estado es el de command2. Otro caso de uso es exec > >(postprocessing), que es equivalente a, pero más legible que, poner todo el resto del script dentro { ... } | postprocessing.


Intenté esto con diff y funcionó, pero con kdiff3 o con emacs, no funcionó. Supongo que el archivo temporal / dev / fd se eliminará antes de que kdiff3 pueda leerlo. ¿O tal vez kdiff3 está intentando leer el archivo dos veces y la tubería solo lo envía una vez?
Eyal

@Eyal Con la sustitución del proceso, el nombre del archivo es una referencia "mágica" a una tubería (o un archivo temporal en variantes de Unix que no admiten estas variantes mágicas). La forma en que se implementa la magia depende del sistema operativo. Linux los implementa como enlaces simbólicos "mágicos" cuyo objetivo no es un nombre de archivo válido (es algo así como pipe:[123456]). Emacs ve que el objetivo del enlace simbólico no es un nombre de archivo existente y eso lo confunde lo suficiente como para que no lea el archivo (puede haber una opción para que lo lea de todos modos, aunque a Emacs no le gusta abrir una tubería como archivo de todos modos).
Gilles 'SO- deja de ser malvado'

10

Bash 4 tiene coprocesos .

Un coproceso se ejecuta de forma asíncrona en un subshell, como si el comando hubiera terminado con el operador de control '&', con una tubería bidireccional establecida entre el shell de ejecución y el coproceso.

El formato para un coproceso es:

coproc [NAME] command [redirections] 

3

A partir de octubre de 2012, esta funcionalidad aún no parece existir en Bash, pero coproc se puede usar si todo lo que necesita para tuberías anónimas / sin nombre es hablar con un proceso secundario. El problema con coproc en este punto es que aparentemente solo se admite uno a la vez. No puedo entender por qué coproc tiene esta limitación. Deberían haber sido una mejora del código de fondo de tareas existente (el & op), pero esa es una pregunta para los autores de bash.


No solo se admite un coproceso. Puede nombrarlos, siempre que no proporcione un comando simple. En su lugar, déle una lista de comandos: coproc THING { dothing; }ahora sus FD están dentro ${THING[*]}y puede ejecutar coproc OTHERTHING { dothing; }y enviar y recibir cosas hacia y desde ambos.
clacke

2
@clacke man bash, debajo del título BUGS, dicen esto: puede haber solo un coproceso activo a la vez . Y recibe una advertencia si inicia un segundo coproc. Parece funcionar, pero no sé qué explota en el fondo.
Radu C

Ok, actualmente solo funciona por suerte, no porque fuera intencional. Advertencia justa, gracias. :-)
clacke

2

Si bien la respuesta de @ DavidAnderson cubre todas las bases y ofrece algunas buenas salvaguardas, lo más importante que revela es que tener una tubería anónima es tan fácil como <(:)permanecer en Linux.

Entonces, la respuesta más corta y simple a su pregunta es:

exec 5<> <(:)

En macOS no funcionará, entonces necesitará crear un directorio temporal para alojar el fifo nombrado hasta que lo haya redirigido. No sé sobre otros BSD.


Te das cuenta de que tu respuesta solo funciona debido a un error en Linux. Este error no existe en macOS, por lo que requiere una solución más compleja. La versión final que publiqué funcionará en Linux, incluso si el error en Linux es reparado.
David Anderson

@DavidAnderson Parece que tienes un conocimiento más profundo de esto que yo. ¿Por qué el comportamiento de Linux es un error?
clacke

1
Si execse aprueba un Fifo anónimo que se abre solo para lectura, execno debe permitir que este Fifo anónimo se abra para leer y escribir utilizando un descriptor de archivo personalizado. Debería esperar recibir un -bash: /dev/fd/5: Permission deniedmensaje, que es lo que emite macOS. Creo que el error es que Ubuntu no produce el mismo mensaje. Estaría dispuesto a cambiar de opinión si alguien pudiera presentar documentación diciendo que exec 5<> <(:)se explica lo permitido.
David Anderson

@DavidAnderson Wow, eso es fascinante. Supuse que bash estaba haciendo algo internamente, pero resulta que es Linux que permite simplemente hacer open(..., O_RDWR)en un extremo de tubería unidireccional proporcionado por la sustitución y eso lo convierte en una tubería bidireccional en un FD. Probablemente tenga razón en que uno no debe confiar en esto. :-D Salida del uso de piperw de execline para crear la tubería, luego reutilizándola con bash <>: libranet.de/display/0b6b25a8-195c-84af-6ac7-ee6696661765
clacke

No es que importe, pero si desea ver en Ubuntu lo que se le pasa exec 5<>, ingrese fun() { ls -l $1; ls -lH $1; }; fun <(:).
David Anderson

1

La siguiente función se probó usando GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu). El sistema operativo era Ubuntu 18. Esta función toma un único parámetro que es el descriptor de archivo deseado para el FIFO anónimo.

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
}

La siguiente función se probó usando GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17). El sistema operativo era macOS High Sierra. Esta función comienza creando un FIFO con nombre en un directorio temporal conocido solo por el proceso que lo creó . A continuación, el descriptor de archivo se redirige a FIFO. Finalmente, el FIFO se desvincula del nombre de archivo eliminando el directorio temporal. Esto hace que el FIFO sea anónimo.

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"; }
}

Las funciones anteriores se pueden combinar en una sola función que funcionará en ambos sistemas operativos. A continuación se muestra un ejemplo de dicha función. Aquí, se intenta crear una FIFO verdaderamente anónima. Si no tiene éxito, se crea un FIFO con nombre y se convierte en un FIFO anónimo.

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"; }
}

Aquí hay un ejemplo de cómo crear una FIFO anónima y luego escribir un texto en la misma FIFO.

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

A continuación se muestra un ejemplo de lectura de todo el contenido del FIFO anónimo.

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

Esto produce la siguiente salida.

Now is the
time for all
good men

El siguiente comando cierra el FIFO anónimo.

eval exec "$fd>&-"

Referencias:
Crear una tubería anónima para su uso posterior Los
archivos en directorios que se pueden escribir públicamente son peligrosos.
Shell Script Security


0

Usando la excelente y brillante respuesta de htamas, la modifiqué un poco para usarla en un solo revestimiento, aquí está:

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

77
No puedo evitar notar que su línea tiene más de una línea.
Dmitry Grigoryev
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.