Bash: crie um fifo anônimo

38

Todos nós sabemos mkfifoe gasodutos. O primeiro cria um pipe nomeado , portanto é necessário selecionar um nome, provavelmente com mktempe depois lembrar de desvincular. O outro cria um canal anônimo, sem problemas com nomes e remoção, mas as extremidades do canal ficam vinculadas aos comandos no pipeline, não é realmente conveniente, de alguma forma, entender os descritores de arquivo e usá-los no resto do script. Em um programa compilado, eu faria ret=pipe(filedes); no Bash, há exec 5<>filealguém que esperaria algo como "exec 5<> -"ou "pipe <5 >6"- existe algo assim no Bash?

Adrian Panasiuk
fonte

Respostas:

42

Você pode desvincular um canal nomeado imediatamente após anexá-lo ao processo atual, o que praticamente resulta em um canal anônimo:

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

Se você realmente deseja evitar pipes nomeados (por exemplo, o sistema de arquivos é somente leitura), sua idéia de "entender os descritores de arquivos" também funciona. Observe que isso é específico do Linux devido ao 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<&-
htamas
fonte
Você pode combinar isso com a localização automática de descritores de arquivos não utilizados: stackoverflow.com/questions/8297415/…
CMCDragonkai
23

Embora nenhuma das conchas que conheço possa fabricar tubos sem bifurcação, algumas têm melhor do que o oleoduto básico.

No bash, ksh e zsh, assumindo que o seu sistema suporta /dev/fd(a maioria dos dias de hoje), você pode vincular a entrada ou a saída de um comando a um nome de arquivo: <(command)expande para um nome de arquivo que designa um canal conectado à saída commande >(command)expande para um nome de arquivo que designa um canal conectado à entrada de command. Esse recurso é chamado de substituição de processo . Seu objetivo principal é canalizar mais de um comando para dentro ou fora de outro, por exemplo,

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

Isso também é útil para combater algumas das deficiências dos tubos de casca básicos. Por exemplo, command2 < <(command1)é equivalente a command1 | command2, exceto que seu status é o de command2. Outro caso de uso exec > >(postprocessing)é equivalente, mas mais legível do que colocar todo o restante do script { ... } | postprocessing.

Gilles 'SO- parar de ser mau'
fonte
Eu tentei isso com diff e funcionou, mas com o kdiff3 ou com o emacs, não funcionou. Meu palpite é que o arquivo / dev / fd temporário está sendo removido antes que o kdiff3 o leia. Ou talvez o kdiff3 esteja tentando ler o arquivo duas vezes e o pipe apenas o envie uma vez?
Eyal
@Eyal Com a manutenção do processo, o nome do arquivo é uma referência “mágica” a um canal (ou um arquivo temporário nas variantes do Unix que não suportam essas variantes mágicas). Como a mágica é implementada depende do sistema operacional. O Linux os implementa como links simbólicos "mágicos" cujo destino não é um nome de arquivo válido (é algo parecido pipe:[123456]). O Emacs vê que o destino do link simbólico não é um nome de arquivo existente e o confunde o suficiente para não ler o arquivo (pode haver uma opção para fazê-lo ler de qualquer maneira, embora o Emacs não goste de abrir um pipe como um arquivo). de qualquer maneira).
Gilles 'SO- stop be evil'
10

O Bash 4 tem coprocessos .

Um coprocesso é executado de forma assíncrona em um subshell, como se o comando tivesse sido finalizado com o operador de controle '&', com um canal bidirecional estabelecido entre o shell de execução e o coprocesso.

O formato para um coprocesso é:

coproc [NAME] command [redirections] 
Pausado até novo aviso.
fonte
3

Em outubro de 2012, essa funcionalidade ainda não parece existir no Bash, mas o coproc pode ser usado se tudo o que você precisa para pipes não nomeados / anônimos é conversar com um processo filho. O problema com o coproc neste momento é que aparentemente apenas um é suportado por vez. Não consigo descobrir por que a coproc recebeu essa limitação. Eles deveriam ter sido um aprimoramento do código de plano de fundo da tarefa existente (o & op), mas essa é uma pergunta para os autores do bash.

Radu C
fonte
Não é apenas um coprocesso suportado. Você pode nomeá-los, desde que não forneça um comando simples. Em vez disso, forneça uma lista de comandos: coproc THING { dothing; }agora seus FDs estão ${THING[*]}e você pode executar, coproc OTHERTHING { dothing; }enviar e receber itens para e de ambos.
clacke 9/03
2
@clacke in man bash, sob o título BUGS, eles dizem o seguinte: Pode haver apenas um coprocesso ativo por vez . E você recebe um aviso se iniciar um segundo coproc. Parece funcionar, mas não sei o que explode em segundo plano.
Radu C
Ok, então atualmente funciona apenas por sorte, não porque foi intencional. Aviso justo, obrigado. :-)
clacke 17/04
2

Embora a resposta da @ DavidAnderson cubra todas as bases e ofereça algumas boas salvaguardas, a coisa mais importante que ela revela é que colocar as mãos em um cano anônimo é tão fácil quanto <(:), desde que você permaneça no Linux.

Portanto, a resposta mais curta e simples à sua pergunta é:

exec 5<> <(:)

No macOS, ele não funcionará; será necessário criar um diretório temporário para abrigar o fifo nomeado até que você o redirecione. Eu não sei sobre outros BSDs.

clacke
fonte
Você percebe que sua resposta só funciona por causa de um bug no linux. Esse bug não existe no macOS, exigindo, portanto, a solução mais complexa. A versão final que eu publiquei funcionará no linux, mesmo que o bug no linux seja corrigido.
David Anderson
@DavidAnderson Parece que você tem um conhecimento mais profundo disso do que eu. Por que o comportamento do Linux é um bug?
clacke 9/03
1
Se execfor passado e um anônimo anônimo que é aberto apenas para leitura, execnão deve permitir que esse anônimo seja aberto para leitura e gravação usando um descritor de arquivo personalizado. Você deve receber uma -bash: /dev/fd/5: Permission deniedmensagem, que é o que o macOS emite. Eu acredito que o bug é que o Ubuntu não produz a mesma mensagem. Eu estaria disposto a mudar de idéia se alguém pudesse produzir documentação dizendo que exec 5<> <(:)é explicitamente permitido.
David Anderson
@DavidAnderson Uau, isso é fascinante. Eu assumi que o bash estava fazendo algo internamente, mas acontece que o Linux permite simplesmente fazer open(..., O_RDWR)em uma extremidade de tubo unidirecional fornecida pela substituição e que o transforma em um tubo bidirecional em um FD. Você provavelmente está certo de que não se deve confiar nisso. :-D Saída usando o piperw do execline para criar o pipe e, em seguida, redirecionando-o com o bash <>: libranet.de/display/0b6b25a8-195c-84af-6ac7-ee6696661765
clacke 10/03
Não que isso importe, mas se você quiser ver no Ubuntu o que é passado exec 5<>, digite fun() { ls -l $1; ls -lH $1; }; fun <(:).
David Anderson
1

A seguinte função foi testada usando GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu). O sistema operacional era o Ubuntu 18. Essa função usa um único parâmetro, que é o descritor de arquivo desejado para o 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
}

A seguinte função foi testada usando GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17). O sistema operacional era o macOS High Sierra. Essa função inicia criando um FIFO nomeado em um diretório temporário conhecido apenas pelo processo que o criou . Em seguida, o descritor de arquivo é redirecionado para o FIFO. Finalmente, o FIFO é desvinculado do nome do arquivo excluindo o diretório temporário. Isso torna o 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
        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"; }
}

As funções acima podem ser combinadas em uma única função que funcionará nos dois sistemas operacionais. Abaixo está um exemplo dessa função. Aqui, é feita uma tentativa de criar um FIFO verdadeiramente anônimo. Se malsucedido, um FIFO nomeado será criado e convertido em um 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"; }
}

Aqui está um exemplo de criação de um FIFO anônimo e, em seguida, escrevendo algum texto no mesmo FIFO.

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

Abaixo está um exemplo de leitura de todo o conteúdo do FIFO anônimo.

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

Isso produz a seguinte saída.

Now is the
time for all
good men

O comando abaixo fecha o FIFO anônimo.

eval exec "$fd>&-"

Referências:
Criar um canal anônimo para uso posterior
Arquivos em diretórios publicamente graváveis ​​são perigosos para a
segurança de scripts do shell

David Anderson
fonte
0

Usando a ótima e brilhante resposta de htamas, modifiquei-a um pouco para usá-la em uma linha, aqui 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<&-
Luiz Felipe Silva
fonte
7
Não posso deixar de notar que sua linha única tem mais de uma linha.
Dmitry Grigoryev