Bash: créer un fifo anonyme


38

Nous le savons tous mkfifoet les pipelines. Le premier crée un canal nommé , il faut donc sélectionner un nom, très probablement avec mktempet plus tard, n'oubliez pas de dissocier. L'autre crée un canal anonyme, sans tracas avec les noms et la suppression, mais les extrémités du canal sont liées aux commandes dans le pipeline, il n'est pas vraiment pratique de saisir les descripteurs de fichiers et de les utiliser dans le reste du script. Dans un programme compilé, je ferais juste ret=pipe(filedes); dans Bash, il y a exec 5<>filedonc quelque chose à attendre "exec 5<> -"ou "pipe <5 >6"quelque chose comme ça dans Bash?

Réponses:


42

Vous pouvez dissocier un canal nommé immédiatement après l'avoir attaché au processus en cours, ce qui entraîne pratiquement un canal anonyme:

# 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 vous voulez vraiment éviter les canaux nommés (par exemple, le système de fichiers est en lecture seule), votre idée de "saisir les descripteurs de fichiers" fonctionne également. Notez que ceci est spécifique à Linux en raison de l'utilisation 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<&-

Vous pouvez combiner cela avec la recherche automatique des descripteurs de fichiers inutilisés: stackoverflow.com/questions/8297415/…
CMCDragonkai

23

Bien qu'aucun des obus que je connaisse ne puisse fabriquer des tuyaux sans bifurquer, certains ont mieux que le pipeline de base.

En bash, ksh et zsh, en supposant que votre système prend en charge /dev/fd(la plupart le font de nos jours), vous pouvez lier l'entrée ou la sortie d'une commande à un nom de fichier: se <(command)développe en un nom de fichier qui désigne un tuyau connecté à la sortie de command, et se >(command)développe à un nom de fichier qui désigne un tuyau connecté à l'entrée de command. Cette fonctionnalité est appelée substitution de processus . Son objectif principal est de diriger plusieurs commandes vers une autre ou vers une autre, par exemple,

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

Ceci est également utile pour lutter contre certaines des lacunes des tuyaux shell de base. Par exemple, command2 < <(command1)est équivalent à command1 | command2, sauf que son statut est celui de command2. Un autre cas d'utilisation est exec > >(postprocessing), ce qui équivaut à, mais plus lisible que, de mettre tout le reste du script à l'intérieur { ... } | postprocessing.


J'ai essayé avec diff et cela a fonctionné mais avec kdiff3 ou avec emacs, cela n'a pas fonctionné. Je suppose que le fichier temporaire / dev / fd est supprimé avant que kdiff3 ne puisse le lire. Ou peut-être que kdiff3 essaie de lire le fichier deux fois et que le canal ne l'envoie qu'une seule fois?
Eyal

@Eyal Avec le processus de substitution, le nom de fichier est une référence «magique» à un canal (ou un fichier temporaire sur les variantes Unix qui ne prennent pas en charge ces variantes magiques). L'implémentation de la magie dépend du système d'exploitation. Linux les implémente comme des liens symboliques «magiques» dont la cible n'est pas un nom de fichier valide (c'est quelque chose comme pipe:[123456]). Emacs voit que la cible du lien symbolique n'est pas un nom de fichier existant et cela le confond suffisamment pour qu'il ne lise pas le fichier (il peut y avoir une option pour le faire lire de toute façon, bien qu'Emacs n'aime pas ouvrir un tube en tant que fichier quand même).
Gilles 'SO- arrête d'être méchant'

10

Bash 4 a des coprocessus .

Un coprocessus est exécuté de manière asynchrone dans un sous-shell, comme si la commande avait été terminée avec l'opérateur de contrôle «&», avec un canal bidirectionnel établi entre le shell d'exécution et le coprocessus.

Le format d'un coprocessus est:

coproc [NAME] command [redirections] 

3

Depuis octobre 2012, cette fonctionnalité ne semble toujours pas exister dans Bash, mais coproc peut être utilisé si tout ce dont vous avez besoin pour des canaux sans nom / anonymes est de parler à un processus enfant. Le problème avec coproc à ce stade est que, apparemment, un seul est pris en charge à la fois. Je ne peux pas comprendre pourquoi coproc a cette limitation. Ils auraient dû être une amélioration du code d'arrière-plan de tâche existant (le & op), mais c'est une question pour les auteurs de bash.


Un seul coprocessus n'est pas pris en charge. Vous pouvez les nommer tant que vous ne fournissez pas de commande simple. Donnez-lui plutôt une liste de commandes: coproc THING { dothing; }maintenant vos FD sont dedans ${THING[*]}et vous pouvez exécuter coproc OTHERTHING { dothing; }et envoyer et recevoir des choses vers et depuis les deux.
clacke

2
@clacke in man bash, sous le titre BUGS, ils disent ceci: Il ne peut y avoir qu'un seul coprocessus actif à la fois . Et vous obtenez un avertissement si vous démarrez un deuxième coproc. Cela semble fonctionner, mais je ne sais pas ce qui explose en arrière-plan.
Radu C

Ok, donc cela ne fonctionne actuellement que par chance, pas parce que c'était intentionnel. Juste avertissement, merci. :-)
clacke

2

Alors que la réponse de @ DavidAnderson couvre toutes les bases et offre de belles garanties, la chose la plus importante qu'elle révèle est qu'il est aussi facile de mettre la main sur un canal anonyme <(:), tant que vous restez sur Linux.

La réponse la plus courte et la plus simple à votre question est donc:

exec 5<> <(:)

Sur macOS, cela ne fonctionnera pas, alors vous devrez créer un répertoire temporaire pour héberger le fifo nommé jusqu'à ce que vous y soyez redirigé. Je ne connais pas les autres BSD.


Vous réalisez que votre réponse ne fonctionne qu'à cause d'un bogue dans Linux. Ce bogue n'existe pas sous macOS, nécessitant ainsi la solution la plus complexe. La version finale que j'ai publiée fonctionnera sous linux même si le bogue dans linux est corrigé.
David Anderson

@DavidAnderson On dirait que vous en avez une connaissance plus approfondie que moi. Pourquoi le comportement Linux est-il un bug?
clacke

1
Si execest passé et fifo anonyme qui est ouvert uniquement pour la lecture, alors execne devrait pas permettre à ce fifo anonyme d'être ouvert pour la lecture et l'écriture à l'aide d'un descripteur de fichier personnalisé. Vous devez vous attendre à recevoir un -bash: /dev/fd/5: Permission deniedmessage, ce qui est le problème de macOS. Je pense que le bogue est qu'Ubuntu ne produit pas le même message. Je serais prêt à changer d'avis si quelqu'un pouvait produire des documents disant que les exec 5<> <(:)explications sont autorisées.
David Anderson

@DavidAnderson Wow, c'est fascinant. J'ai supposé que bash faisait quelque chose en interne, mais il s'avère que c'est Linux qui permet simplement de le faire open(..., O_RDWR)sur une extrémité de tuyau unidirectionnelle fournie par la substitution et qui le transforme en tuyau bidirectionnel dans un FD. Vous avez probablement raison de ne pas vous fier à cela. :-D Sortie de l'utilisation du piperw d'execline pour créer le tuyau, puis le réaffecter avec bash <>: libranet.de/display/0b6b25a8-195c-84af-6ac7-ee6696661765
clacke

Ce n'est pas important, mais si vous voulez voir sous Ubuntu ce qui est passé exec 5<>, entrez fun() { ls -l $1; ls -lH $1; }; fun <(:).
David Anderson

1

La fonction suivante a été testée en utilisant GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu). Le système d'exploitation était Ubuntu 18. Cette fonction prend un seul paramètre qui est le descripteur de fichier souhaité pour le FIFO anonyme.

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 fonction suivante a été testée en utilisant GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17). Le système d'exploitation était macOS High Sierra. Cette fonction démarre en créant un FIFO nommé dans un répertoire temporaire connu uniquement du processus qui l'a créé . Ensuite, le descripteur de fichier est redirigé vers le FIFO. Enfin, le FIFO est dissocié du nom de fichier en supprimant le répertoire temporaire. Cela rend le FIFO anonyme.

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

Les fonctions ci-dessus peuvent être combinées en une seule fonction qui fonctionnera sur les deux systèmes d'exploitation. Voici un exemple d'une telle fonction. Ici, une tentative est faite pour créer un FIFO vraiment anonyme. En cas d'échec, un FIFO nommé est créé et converti en FIFO anonyme.

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

Voici un exemple de création d'un FIFO anonyme, puis d'écriture de texte dans le même FIFO.

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

Vous trouverez ci-dessous un exemple de lecture de l'intégralité du contenu de la FIFO anonyme.

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

Cela produit la sortie suivante.

Now is the
time for all
good men

La commande ci-dessous ferme le FIFO anonyme.

eval exec "$fd>&-"

Références: la
création d'un canal anonyme pour une utilisation ultérieure Les
fichiers dans les répertoires accessibles en écriture sont une sécurité de script shell dangereuse


0

En utilisant la bonne et brillante réponse de htamas, je l'ai un peu modifiée pour l'utiliser dans une doublure, la voici:

# 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
Je ne peux pas m'empêcher de remarquer que votre one-liner a plus d'une ligne.
Dmitry Grigoryev
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.